From 8444c991f5f2440966be1b5d7b5b2465ad23ddb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 23:31:07 +0000 Subject: [PATCH 1/2] Fix board creation failing silently on read-only filesystems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creating a board did nothing in production (agentboard.sh on Vercel). Two compounding bugs: 1. Storage writes failed on read-only filesystems. getDataDir() defaulted to ~/.agentboard/data, which is not writable on serverless platforms like Vercel (only the temp dir is). Every write (createBoard, createTask, ...) threw EROFS/EACCES, so POST /api/boards returned 500. Reproduced locally: pointing the data dir at an unwritable path throws ENOTDIR on mkdir. Now getDataDir() falls back to /agentboard/data when the home dir isn't writable (resolved once and cached), with an explicit AGENTBOARD_DATA_DIR override still winning. A warning notes that the fallback is ephemeral on serverless and a durable path is needed for persistence. 2. The UI swallowed the failure. The landing page only navigated `if (data.ok)`, so a 500 produced no feedback at all — "does nothing." createBoard now surfaces the server error message (or a network error), shows a disabled "Creating…" state, and clears on retry. Verified locally: POST /api/boards returns 201 and GET returns 200; with an unwritable home dir, createBoard now succeeds against the temp fallback. https://claude.ai/code/session_014r13EsBf8Kz8YGfD6VxDtS --- src/app/page.tsx | 40 +++++++++++++++++++++++++------------ src/lib/server-utils.ts | 44 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index c701f64..3cc125e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -14,12 +14,13 @@ export default function HomePage() { // with the dev default so the server render and first client render match, // then corrected to the real origin after mount. const [baseUrl, setBaseUrl] = useState("http://localhost:4040"); + const [error, setError] = useState(null); + const [creating, setCreating] = useState(false); const router = useRouter(); useEffect(() => { // window.location is only available client-side; syncing after mount keeps // the SSR/first-client render in agreement (no hydration mismatch). - // eslint-disable-next-line react-hooks/set-state-in-effect setBaseUrl(window.location.origin); }, []); @@ -36,14 +37,25 @@ export default function HomePage() { const createBoard = async () => { const name = prompt("Board name:"); if (!name) return; - const res = await fetch("/api/boards", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name }), - }); - const data = await res.json(); - if (data.ok) { - router.push(`/boards/${data.data.id}`); + setError(null); + setCreating(true); + try { + const res = await fetch("/api/boards", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name }), + }); + const data = await res.json().catch(() => null); + if (res.ok && data?.ok) { + router.push(`/boards/${data.data.id}`); + return; + } + // Surface the failure instead of silently doing nothing. + setError(data?.error?.message || `Could not create board (HTTP ${res.status}). Please try again.`); + } catch { + setError("Could not reach the server. Check your connection and try again."); + } finally { + setCreating(false); } }; @@ -79,9 +91,10 @@ export default function HomePage() { ))} - + {error &&

{error}

} @@ -104,9 +117,10 @@ export default function HomePage() {

- + {error &&

{error}

}

Or create via API:

diff --git a/src/lib/server-utils.ts b/src/lib/server-utils.ts index da876f8..d1a486f 100644 --- a/src/lib/server-utils.ts +++ b/src/lib/server-utils.ts @@ -1,6 +1,48 @@ +import fs from "fs"; import path from "path"; import os from "os"; +let cachedDataDir: string | null = null; + +function ensureWritableDir(dir: string): boolean { + try { + fs.mkdirSync(dir, { recursive: true }); + fs.accessSync(dir, fs.constants.W_OK); + return true; + } catch { + return false; + } +} + +/** + * Resolves the directory used for file-system JSON storage. + * + * Resolution order: + * 1. `AGENTBOARD_DATA_DIR` if set — explicit config always wins. + * 2. `~/.agentboard/data` when that location is writable (local/self-hosted). + * 3. `/agentboard/data` as a fallback when the home dir is + * read-only — e.g. serverless platforms like Vercel, where only the temp + * dir is writable. Without this, every write (createBoard, createTask, …) + * throws EROFS/EACCES, the API returns 500, and the UI appears to "do + * nothing." NOTE: the temp dir is ephemeral and per-instance on serverless, + * so data is not durable there — a real backend is needed for persistence. + */ export function getDataDir(): string { - return process.env.AGENTBOARD_DATA_DIR || path.join(os.homedir(), ".agentboard", "data"); + if (process.env.AGENTBOARD_DATA_DIR) return process.env.AGENTBOARD_DATA_DIR; + if (cachedDataDir) return cachedDataDir; + + const preferred = path.join(os.homedir(), ".agentboard", "data"); + if (ensureWritableDir(preferred)) { + cachedDataDir = preferred; + return cachedDataDir; + } + + const fallback = path.join(os.tmpdir(), "agentboard", "data"); + fs.mkdirSync(fallback, { recursive: true }); + console.warn( + `[agentboard] data dir ${preferred} is not writable; falling back to ${fallback}. ` + + `Data is ephemeral here (e.g. serverless) — set AGENTBOARD_DATA_DIR to a durable path for persistence.`, + ); + cachedDataDir = fallback; + return cachedDataDir; } From d0911480a3b6a7f22e58812bb17a44bc80df030d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 23:38:25 +0000 Subject: [PATCH 2/2] Verify fallback data dir is writable before caching Use ensureWritableDir for the temp fallback path, matching the preferred path check, so an existing but unwritable directory is not cached. --- src/lib/server-utils.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lib/server-utils.ts b/src/lib/server-utils.ts index d1a486f..12184ff 100644 --- a/src/lib/server-utils.ts +++ b/src/lib/server-utils.ts @@ -38,11 +38,17 @@ export function getDataDir(): string { } const fallback = path.join(os.tmpdir(), "agentboard", "data"); - fs.mkdirSync(fallback, { recursive: true }); - console.warn( - `[agentboard] data dir ${preferred} is not writable; falling back to ${fallback}. ` + - `Data is ephemeral here (e.g. serverless) — set AGENTBOARD_DATA_DIR to a durable path for persistence.`, + if (ensureWritableDir(fallback)) { + console.warn( + `[agentboard] data dir ${preferred} is not writable; falling back to ${fallback}. ` + + `Data is ephemeral here (e.g. serverless) — set AGENTBOARD_DATA_DIR to a durable path for persistence.`, + ); + cachedDataDir = fallback; + return cachedDataDir; + } + + throw new Error( + `[agentboard] no writable data directory (tried ${preferred} and ${fallback}). ` + + `Set AGENTBOARD_DATA_DIR to a writable path.`, ); - cachedDataDir = fallback; - return cachedDataDir; }