Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(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);
}, []);

Expand All @@ -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);
}
};

Expand Down Expand Up @@ -79,9 +91,10 @@ export default function HomePage() {
</button>
))}
</div>
<Button onClick={createBoard} variant="outline" className="mt-4">
Create another board
<Button onClick={createBoard} variant="outline" className="mt-4" disabled={creating}>
{creating ? "Creating…" : "Create another board"}
</Button>
{error && <p className="mt-3 text-sm text-destructive">{error}</p>}
</div>
</div>
</div>
Expand All @@ -104,9 +117,10 @@ export default function HomePage() {
</p>
</div>

<Button onClick={createBoard} size="lg">
Create your first board
<Button onClick={createBoard} size="lg" disabled={creating}>
{creating ? "Creating…" : "Create your first board"}
</Button>
{error && <p className="text-sm text-destructive">{error}</p>}

<div className="text-left">
<p className="text-sm text-muted-foreground mb-2">Or create via API:</p>
Expand Down
50 changes: 49 additions & 1 deletion src/lib/server-utils.ts
Original file line number Diff line number Diff line change
@@ -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. `<os.tmpdir()>/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.`,
);
}