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() {
))}
-
-
- Create your first board
+
+ {creating ? "Creating…" : "Create your first board"}
+ {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;
}