import type { ServiceConnection } from 'officerdev'; import { useEffect, useState } from 'react'; import { CheckCircle2, Loader2, Plug, Trash2, TriangleAlert } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { serviceErrorMessage, useServiceConnection, useServiceConnectionActions, useServiceHealth, } from 'officerdev'; // Connecting a slskd daemon to Officer, from the app. // // Both the setup wizard and the permanent settings page: SoulseekView renders it in place of whatever // section the nav asked for while nothing is connected, and the Connection section renders it for good. // One component, so re-pointing at a different daemon later goes through exactly the code path that // stored the first one. // // The API key is write-only — the GET it reads has no field that could carry one back, so the input is // always blank and an empty input means "keep the stored key". const HINT = 'text-[11px] leading-relaxed text-muted-foreground'; /** slskd's own default HTTP port. Officer dials it from the server, not from this browser. */ const DEFAULT_URL = 'http://localhost:5030'; const URL_HINT = "The daemon's base URL. Officer reaches it from the server, not from this browser — so localhost here " + 'means the machine Officer runs on.'; const KEY_HINT = 'An API key from slskd.yml (web.authentication.api_keys). Officer sends it as X-API-Key on every call.'; type SaveInput = Record & { url: string }; export const SoulseekConnection = () => { const { data, isLoading } = useServiceConnection('soulseek'); const { data: health } = useServiceHealth('soulseek'); const { save, forget } = useServiceConnectionActions('soulseek'); const connection = data?.connection ?? null; const [url, setUrl] = useState(''); const [apiKey, setApiKey] = useState(''); const [error, setError] = useState(''); // Seed from the stored row once it arrives. Keyed on its id so a save (same id) doesn't stomp what the // owner is still typing, while forgetting and re-adding does reset the form. useEffect(() => { setUrl(connection?.url ?? ''); setApiKey(''); }, [connection?.id]); const submit = async (ev: React.FormEvent) => { ev.preventDefault(); setError(''); try { await save.mutateAsync({ url: url.trim(), apiKey: apiKey.trim() }); setApiKey(''); } catch (err) { setError(serviceErrorMessage(err)); } }; const remove = async () => { setError(''); try { await forget.mutateAsync(); } catch (err) { setError(serviceErrorMessage(err)); } }; if (isLoading) { return (
Loading…
); } return (

slskd daemon

{connection ? 'Where Officer talks to slskd. Saving re-checks the daemon before storing anything.' : 'Point Officer at your slskd daemon. It needs the URL and one API key.'}

{connection && }
{error && (
{error}
)}
{connection && ( )}
); }; type StatusRowProps = { connection: ServiceConnection; health: { ok: boolean; error?: string } | undefined }; const StatusRow = ({ connection, health }: StatusRowProps) => (
{health?.ok ? ( ) : ( )}
{health?.ok ? 'Connected' : 'Not responding'}
{connection.url} {connection.version ? ` · slskd ${connection.version}` : ''}
{!health?.ok && health?.error &&
{health.error}
}
);