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..12184ff 100644
--- a/src/lib/server-utils.ts
+++ b/src/lib/server-utils.ts
@@ -1,6 +1,54 @@
+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");
+ 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.`,
+ );
}