Files
continuum-app/apps/web/composables/useApi.ts
T
octoturge c7f85870db Add a Jobs page: upload a sliced file, watch it queue, delete it
The console could watch and control printers but not give them work. The
Jobs page uploads a .3mf/.gcode: it creates the job (POST /jobs), PUTs the
file straight to the presigned storage URL with a progress bar, and lists
the farm's jobs, polling every 5s since there is no job push channel yet.
The scheduler then starts it on the next idle printer, so there is no Start
button - a queued job IS the request.

Deliberate details: the PUT carries no Authorization header (the URL is
presigned and an uncovered header makes storage reject it) and the same
Content-Type the URL was signed for; a failed upload deletes the job row
it just created, so no queued job waits for a file that never arrives;
only queued/uploading jobs offer Delete, matching what the backend allows.

JobUpload and JobList live in @continuum/ui and take the API calls as
props, like the printer card. A shared apiErrorMessage() replaces the
inline copy on the fleet page.

Checked in real Chrome against the live stack with job dispatch OFF: upload
to the real bucket (57 bytes landed intact), listed as queued, still queued
after a scheduler tick, Delete clears it, no page errors; the test object
was removed from the bucket afterwards. pnpm -r typecheck is clean.
2026-10-29 14:39:37 +00:00

54 lines
2.3 KiB
TypeScript

/**
* The one way this app calls `continuum-backend`.
*
* Auth is Supabase throughout: the backend verifies access tokens
* asymmetrically against the project's published JWKS and holds no signing
* key of its own (`continuum-backend/src/auth/verifier.ts`), and
* `requireUser` wants that token in `Authorization: Bearer`. So this attaches
* it, and that is the whole of the client's side of authentication.
*
* What this replaces: `useRequestHeaders(["cookie"])` on each call site.
* Forwarding a cookie is what you do when a Nuxt *server* is proxying a
* browser's request, and this app sets `ssr: false` so that it can ship the
* same static bundle to the browser and to the Tauri shell - there is no
* server to forward anything, so the header arrived empty and every call got
* a 401. The token was already to hand the whole time; the telemetry store
* has been reading it for its socket.
*
* The token is read inside the fetcher rather than captured when this is
* called, because supabase-js refreshes the session in the background about
* once an hour. A token captured at setup would work until it quietly
* stopped.
*/
export function useApiFetch() {
const config = useRuntimeConfig();
const session = useSupabaseSession();
return <T>(path: string, options: Parameters<typeof $fetch>[1] = {}) => {
const token = session.value?.access_token;
return $fetch<T>(`${config.public.apiBase}${path}`, {
...options,
headers: {
// Omitted rather than sent empty when there is no session. An empty
// Bearer is a malformed credential the backend rejects with a
// message about the header's shape, which reads like a client bug;
// no header at all is the honest "not signed in". The Supabase
// module's own redirect is what handles getting the user a session.
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options?.headers,
},
});
};
}
/**
* A sentence for a failed API call. The backend's `{ error: { message } }`
* body is what explains a 403 (a viewer cannot do this), a 409 or a 502, so
* it is surfaced instead of a bare "fetch failed".
*/
export function apiErrorMessage(err: unknown, fallback: string): string {
const body = (err as { data?: { error?: { message?: string } } }).data;
return body?.error?.message ?? fallback;
}