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..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.`, + ); }