API additions (dev-api.py split across this and next commit):
- /api/jobs/{job_id}/qa GET/PATCH/suggest — Interview Prep answer storage + LLM suggestions
- /api/settings/ui-preference POST — persist streamlit/vue preference to user.yaml
- cancel_task() added to scripts/db.py (per-task cancel for Danger Zone)
Vue / UI:
- AppNav: "⚡ Classic" button to switch back to Streamlit UI (writes cookie + persists to user.yaml)
- ApplyWorkspace: Resume Highlights panel (collapsible skills/domains/keywords with job-match highlighting)
- SettingsView: hide Data tab in cloud mode (showData guard)
- ResumeProfileView: minor improvements
- useApi.ts: error handling improvements
Infra:
- compose.cloud.yml: add api service (uvicorn dev_api running in cloud container)
- docker/web/nginx.conf: proxy /api/* to api service in cloud mode
- README.md: Vue SPA now listed as Free tier feature
53 lines
1.5 KiB
TypeScript
53 lines
1.5 KiB
TypeScript
export type ApiError =
|
|
| { kind: 'network'; message: string }
|
|
| { kind: 'http'; status: number; detail: string }
|
|
|
|
// Strip trailing slash so '/peregrine/' + '/api/...' → '/peregrine/api/...'
|
|
const _apiBase = import.meta.env.BASE_URL.replace(/\/$/, '')
|
|
|
|
export async function useApiFetch<T>(
|
|
url: string,
|
|
opts?: RequestInit,
|
|
): Promise<{ data: T | null; error: ApiError | null }> {
|
|
try {
|
|
const res = await fetch(_apiBase + url, opts)
|
|
if (!res.ok) {
|
|
const detail = await res.text().catch(() => '')
|
|
return { data: null, error: { kind: 'http', status: res.status, detail } }
|
|
}
|
|
const data = await res.json() as T
|
|
return { data, error: null }
|
|
} catch (e) {
|
|
return { data: null, error: { kind: 'network', message: String(e) } }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open an SSE connection. Returns a cleanup function.
|
|
* onEvent receives each parsed JSON payload.
|
|
* onComplete is called when the server sends a {"type":"complete"} event.
|
|
* onError is called on connection error.
|
|
*/
|
|
export function useApiSSE(
|
|
url: string,
|
|
onEvent: (data: Record<string, unknown>) => void,
|
|
onComplete?: () => void,
|
|
onError?: (e: Event) => void,
|
|
): () => void {
|
|
const es = new EventSource(_apiBase + url)
|
|
es.onmessage = (e) => {
|
|
try {
|
|
const data = JSON.parse(e.data) as Record<string, unknown>
|
|
onEvent(data)
|
|
if (data.type === 'complete') {
|
|
es.close()
|
|
onComplete?.()
|
|
}
|
|
} catch { /* ignore malformed events */ }
|
|
}
|
|
es.onerror = (e) => {
|
|
onError?.(e)
|
|
es.close()
|
|
}
|
|
return () => es.close()
|
|
}
|