import { useCallback, useEffect, useRef, useState } from 'react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; /** * Where this install's Karakeep is, and whether it answers. * * Was the whole plugin for an afternoon, which was the mistake: a settings screen with nothing behind it * is a screen you cannot leave. It is a disclosure inside the lists panel now — reachable when * something is wrong, and out of the way when it is not. * * ── Two addresses, and they are not the same one ── * * The field below is the address the SIDECAR dials, which on a machine running both Officer and * Karakeep is loopback. It is NOT the address a browser or the extension is given — that is * `NEXTAUTH_URL` inside Karakeep's own container, and it has to be reachable from wherever the client * is. Measured on this install: the name Karakeep advertises resolves to 127.0.1.1 on the host it runs * on, where nothing is bound, so a sidecar told to use it dials a name that answers from everywhere * except the one place that matters. */ type Config = { url: string | null; enabled: boolean; updatedAt: string | null }; type Health = { ok: boolean; upstream?: number; reason?: string }; export const SettingsForm = () => { const client = useClient(); // `useClient()` rebuilds its verbs every render, so they must never sit in a dependency list. const clientRef = useRef(client); clientRef.current = client; const [config, setConfig] = useState(null); const [url, setUrl] = useState(''); const [adminApiKey, setAdminToken] = useState(''); const [health, setHealth] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const load = useCallback(async () => { try { const next = await clientRef.current.get('/karakeep/config'); setConfig(next); setUrl(next.url ?? ''); setError(null); } catch { setError('Could not read the configuration.'); } }, []); useEffect(() => { void load(); }, [load]); const check = useCallback(async () => { setHealth(null); try { setHealth(await clientRef.current.get('/karakeep/config/health')); } catch { setHealth({ ok: false, reason: 'the check itself failed' }); } }, []); const save = async () => { setBusy(true); try { // The token is sent only when one was typed: leaving a write-only field alone means "unchanged", // not "clear it". await clientRef.current.put('/karakeep/config', { url: url.trim() || null, ...(adminApiKey.trim() ? { adminApiKey: adminApiKey.trim() } : {}), }); setAdminToken(''); setError(null); await load(); await check(); } catch (failure) { setError(failure instanceof Error ? failure.message : 'Could not save that.'); } finally { setBusy(false); } }; return (
{error &&

{error}

}
setUrl(event.target.value)} placeholder="http://127.0.0.1:3000" className="h-8 text-xs" />
setAdminToken(event.target.value)} placeholder={config?.url ? 'unchanged' : 'Karakeep → Settings → Access Tokens'} className="h-8 text-xs" />
{health && (

{health.ok ? ( Answering{health.upstream ? ` (${health.upstream})` : ''}. ) : ( /* Three distinct reasons on purpose — unconfigured, unreachable, sidecar not running are three different next actions, and one word for all of them sends somebody to check the wrong thing. */ Not answering — {health.reason ?? 'no reason given'}. )}

)}
); };