M9 S2: fold raw multipart fetches into api.postForm
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 31s

Both notes-store uploads (uploadAttachment, importNotes) hand-rolled the
same fetch + resp.json() + !ok error parsing that api.client already does.
Add `api.postForm<T>(path, form)`: request() now detects a FormData body
and lets the browser set the multipart Content-Type (skipping the JSON
header + stringify), reusing the shared error handling — so the two
uploads gain network-error handling and the 5xx infra toast they lacked.
A too-large import returns 413 (< 500), so it still throws for inline
display rather than toasting.

DRY: net -13 lines; no raw fetch() remains in the stores.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-23 21:18:37 -04:00
co-authored by Claude Opus 4.8
parent 3a4c3c8164
commit e8e0d86413
2 changed files with 9 additions and 22 deletions
+3 -20
View File
@@ -205,16 +205,7 @@ export const useNotesStore = defineStore("notes", () => {
async function uploadAttachment(id: string, file: File): Promise<void> {
const form = new FormData();
form.append("file", file);
const resp = await fetch(`/api/notes/${id}/attachments`, { method: "POST", credentials: "include", body: form });
const data: unknown = await resp.json().catch(() => ({}));
if (!resp.ok) {
const message =
typeof data === "object" && data !== null && "error" in data
? String((data as { error: unknown }).error)
: "Upload failed.";
throw { error: message, status: resp.status };
}
reconcile(data as Note);
reconcile(await api.postForm<Note>(`/api/notes/${id}/attachments`, form));
}
async function deleteAttachment(id: string, attId: string): Promise<void> {
@@ -232,18 +223,10 @@ export const useNotesStore = defineStore("notes", () => {
async function importNotes(file: File): Promise<{ source: string; imported: number; skipped: number }> {
const form = new FormData();
form.append("file", file);
const resp = await fetch("/api/notes/import", { method: "POST", credentials: "include", body: form });
const data: unknown = await resp.json().catch(() => ({}));
if (!resp.ok) {
const message =
typeof data === "object" && data !== null && "error" in data
? String((data as { error: unknown }).error)
: "Import failed.";
throw { error: message, status: resp.status };
}
const data = await api.postForm<{ source: string; imported: number; skipped: number }>("/api/notes/import", form);
// Refresh the current lens so imported notes appear (labels reloaded by caller).
await load(view.value, activeLabel.value, activeFacets.value);
return data as { source: string; imported: number; skipped: number };
return data;
}
async function fetchOne(id: string): Promise<Note | null> {