"use client"; import { useCallback, useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { Card, Button, Input, Badge } from "@/shared/components"; export default function ObsidianSourceCard() { const t = useTranslations("endpoint"); const DEFAULT_URL = "http://127.0.0.1:27123"; const [connected, setConnected] = useState(false); const [token, setToken] = useState(""); const [baseUrl, setBaseUrl] = useState(DEFAULT_URL); const [busy, setBusy] = useState(false); const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); const [expanded, setExpanded] = useState(false); const [webdavEnabled, setWebdavEnabled] = useState(false); const [webdavUsername, setWebdavUsername] = useState(null); const [webdavPassword, setWebdavPassword] = useState(null); const [vaultPath, setVaultPath] = useState(""); const [webdavBusy, setWebdavBusy] = useState(false); const [showPassword, setShowPassword] = useState(false); const fetchConfig = useCallback(async () => { try { const res = await fetch("/api/settings/obsidian"); if (res.ok) { const data = await res.json(); setConnected(data.connected); if (data.baseUrl) setBaseUrl(data.baseUrl); if (data.vaultPath) setVaultPath(data.vaultPath); } } catch { // Non-critical } }, []); const fetchWebdavStatus = useCallback(async () => { try { const res = await fetch("/api/settings/obsidian/webdav"); if (res.ok) { const data = await res.json(); setWebdavEnabled(data.webdavEnabled); setWebdavUsername(data.webdavUsername); setWebdavPassword(data.webdavPassword); if (data.vaultPath) setVaultPath(data.vaultPath); } } catch { // Non-critical } }, []); useEffect(() => { void fetchConfig(); void fetchWebdavStatus(); }, [fetchConfig, fetchWebdavStatus]); useEffect(() => { if (message) { const timer = setTimeout(() => setMessage(null), 5000); return () => clearTimeout(timer); } }, [message]); const handleSaveToken = async () => { if (!token.trim()) { setMessage({ type: "error", text: "Please enter an Obsidian API token" }); return; } setBusy(true); setMessage(null); try { const body: Record = { token: token.trim() }; if (baseUrl.trim() && baseUrl.trim() !== DEFAULT_URL) { body.baseUrl = baseUrl.trim(); } const res = await fetch("/api/settings/obsidian", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json(); if (res.ok) { setConnected(true); setMessage({ type: "success", text: data.message }); } else { setMessage({ type: "error", text: data.error ?? "Failed to connect" }); setConnected(false); } } catch (err) { setMessage({ type: "error", text: err instanceof Error ? err.message : "Connection failed" }); } finally { setBusy(false); } }; const handleDisconnect = async () => { setBusy(true); setMessage(null); try { const res = await fetch("/api/settings/obsidian", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setConnected(false); setToken(""); setBaseUrl(DEFAULT_URL); setMessage({ type: "success", text: data.message }); } else { setMessage({ type: "error", text: data.error ?? "Failed to disconnect" }); } } catch (err) { setMessage({ type: "error", text: err instanceof Error ? err.message : "Disconnect failed" }); } finally { setBusy(false); } }; const handleEnableWebdav = async () => { if (!vaultPath.trim()) { setMessage({ type: "error", text: "Please enter the vault directory path" }); return; } setWebdavBusy(true); setMessage(null); try { const res = await fetch("/api/settings/obsidian/webdav", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vaultPath: vaultPath.trim() }), }); const data = await res.json(); if (res.ok) { setWebdavEnabled(true); setWebdavUsername(data.username); setWebdavPassword(data.password); setMessage({ type: "success", text: "WebDAV sync enabled. Configure your mobile device below." }); } else { setMessage({ type: "error", text: data.error ?? "Failed to enable WebDAV" }); } } catch (err) { setMessage({ type: "error", text: err instanceof Error ? err.message : "Failed to enable WebDAV" }); } finally { setWebdavBusy(false); } }; const handleDisableWebdav = async () => { setWebdavBusy(true); setMessage(null); try { const res = await fetch("/api/settings/obsidian/webdav", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setWebdavEnabled(false); setWebdavUsername(null); setWebdavPassword(null); setMessage({ type: "success", text: "WebDAV sync disabled" }); } else { setMessage({ type: "error", text: data.error ?? "Failed to disable WebDAV" }); } } catch (err) { setMessage({ type: "error", text: err instanceof Error ? err.message : "Failed to disable WebDAV" }); } finally { setWebdavBusy(false); } }; const getWebdavUrl = (): string => { if (typeof window === "undefined") return "/api/v1/webdav/"; // Inherit the page protocol (http on localhost, https behind a TLS proxy) // instead of hard-coding http. const { protocol, hostname, port } = window.location; return `${protocol}//${hostname}:${port}/api/v1/webdav`; }; return (
{expanded && (
{message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)} {!connected ? (
setToken(e.target.value)} placeholder="Obsidian API key" disabled={busy} className="font-mono text-sm flex-1" />
setBaseUrl(e.target.value)} placeholder={DEFAULT_URL} disabled={busy} className="font-mono text-sm" /> {baseUrl.includes(":27124") && (
warning Port 27124 is the MCP endpoint (HTTPS, self-signed cert). {" "}The REST API uses HTTP on port 27123.
)}

Default: {DEFAULT_URL}. For remote vaults, enter the Tailscale IP + {" "}port (e.g., http://100.x.x.x:27123). Enable the Local REST API {" "}plugin on the machine running Obsidian.

) : (
Token configured. Obsidian tools are available via MCP.
Vault Sync (WebDAV)

Sync your vault to Obsidian mobile using WebDAV over Tailscape. Obsidian mobile has built-in WebDAV support — no plugins needed.

{!webdavEnabled ? (
setVaultPath(e.target.value)} placeholder="/Users/you/Documents/Obsidian" disabled={webdavBusy} className="font-mono text-sm flex-1" />
) : (
cloud_sync

WebDAV sync enabled

{getWebdavUrl()}

Configure Obsidian Mobile

In Obsidian mobile: Settings → Sync → WebDAV → enter the following:

{getWebdavUrl()}
{webdavUsername ?? "—"}
{showPassword ? (webdavPassword ?? "—") : "••••••••••••"}

Use your Tailscale IP instead of localhost if connecting from mobile. {" "}Both devices must be on the same Tailscale network.

)}
)}
)}
); }