From 5e54a32fa89e09e6b1dc4edae9208333e89b562f Mon Sep 17 00:00:00 2001 From: aksops Date: Sun, 26 Apr 2026 06:46:31 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(ui):=20make=20Feed=20tab=20clickable=20?= =?UTF-8?q?=E2=80=94=20add=20/feed=20route=20+=20tabFromPath=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 1e92e7d ("move Pane to first tab") flipped the URL default from feed → pane but missed two follow-ups: 1. App.tsx has explicit routes for every tab except `/s/:name/feed`. When the user clicked Feed, navigate("/s//feed") matched the catch-all `` and yanked them back to the dashboard — the visible symptom was "Feed tab unclickable". 2. tabFromPath had no `endsWith("/feed")` branch (it relied on the implicit default that used to be feed). Even if the route had existed, the tab indicator would have stayed on Pane. Add the route and the branch. No other changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/App.tsx | 1 + ui/src/routes/SessionDetail.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 1d38e53..06cdb86 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -26,6 +26,7 @@ import { AuthGate } from "@/routes/AuthGate"; const router = createBrowserRouter([ { path: "/", element: }, { path: "/s/:name", element: }, + { path: "/s/:name/feed", element: }, { path: "/s/:name/checkpoints", element: }, { path: "/s/:name/pane", element: }, { path: "/s/:name/subagents", element: }, diff --git a/ui/src/routes/SessionDetail.tsx b/ui/src/routes/SessionDetail.tsx index 339fe92..2356d62 100644 --- a/ui/src/routes/SessionDetail.tsx +++ b/ui/src/routes/SessionDetail.tsx @@ -41,6 +41,7 @@ type TabKey = | "meta"; function tabFromPath(pathname: string): TabKey { + if (pathname.endsWith("/feed")) return "feed"; if (pathname.endsWith("/checkpoints")) return "checkpoints"; if (pathname.endsWith("/pane")) return "pane"; if (pathname.endsWith("/subagents")) return "subagents"; From bd6adb6b2b70905250fa0cdedfbc72d2b9b2968f Mon Sep 17 00:00:00 2001 From: aksops Date: Sun, 26 Apr 2026 06:52:42 +0000 Subject: [PATCH 2/2] fix(store): mkdir cost-DB parent dir on OpenCostStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mattn/go-sqlite3 surfaces a missing parent dir as the unhelpful "unable to open database file: no such file or directory" error. Production opens ctm.db at ~/.config/ctm/ctm.db — on a fresh install or a CI runner with no pre-existing config dir, the parent doesn't exist and OpenCostStore fails before doing anything useful. Add a defensive os.MkdirAll(filepath.Dir(path), 0o700) at the top of OpenCostStore. Skipped for the in-memory path (":memory:" → Dir returns "." which is a no-op anyway, but explicit guard reads better). Mkdir errors are swallowed — if mkdir fails we let sql.Open surface the real problem instead of masking it. Side benefit: server_test.go's TestHealthz* / TestAuth* etc. were flakily passing or failing depending on whether earlier-running test packages (alphabetical: internal/config, …) created ~/.config/ctm/ as a side effect. CI runs without a populated test cache hit this regularly. Now they're independent. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/serve/store/cost_store.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/serve/store/cost_store.go b/internal/serve/store/cost_store.go index 8056fd7..d5b6daa 100644 --- a/internal/serve/store/cost_store.go +++ b/internal/serve/store/cost_store.go @@ -34,6 +34,8 @@ import ( "errors" "fmt" "net/url" + "os" + "path/filepath" "sync" "time" @@ -92,6 +94,18 @@ type sqliteCostStore struct { // keeps the handler-side Writer from erroring under light contention // with the quota-subscriber goroutine. func OpenCostStore(path string) (CostStore, error) { + // Ensure the parent directory exists. Production opens the DB at + // ~/.config/ctm/ctm.db; on a fresh install (or in CI runners + // without a pre-existing config dir) the parent might not exist + // yet, and mattn/go-sqlite3 surfaces that as + // "unable to open database file". Cheap and idempotent — no-op + // for ":memory:" (filepath.Dir returns "."). Errors here are + // non-fatal: if mkdir fails we let sql.Open surface the real + // problem. + if path != ":memory:" { + _ = os.MkdirAll(filepath.Dir(path), 0o700) + } + // DSN tuning: ?_busy_timeout=5000 waits out brief writer locks; // ?_journal=WAL enables concurrent readers; ?_sync=NORMAL pairs // with WAL for an acceptable durability-vs-speed trade.