M2 attachments: image upload + owner-scoped media serving
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 34s

- Migration 0007: note_attachments (path/mime/size). Upload POST
  /api/notes/<id>/attachments (multipart, png/jpeg/gif/webp, 12MB cap via
  MAX_CONTENT_LENGTH) stored under Config.media_root() (first use of DATA_DIR);
  owner/ACL-scoped GET serves the file (nosniff); DELETE removes row + file.
  Note responses include attachments[].
- Frontend: notes store uploadAttachment (FormData)/deleteAttachment; editor
  image button + paste-to-upload + thumbnail grid with remove; card shows the
  first image as a cover.

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-19 22:21:26 -04:00
co-authored by Claude Opus 4.8
parent 31be66ac60
commit e4c898cd1b
10 changed files with 278 additions and 3 deletions
+28
View File
@@ -18,6 +18,12 @@ export interface ChecklistItem {
position: number;
}
export interface Attachment {
id: string;
url: string;
mime: string;
}
export interface Note {
id: string;
title: string | null;
@@ -29,6 +35,7 @@ export interface Note {
trashed: boolean;
labels: NoteLabel[];
items: ChecklistItem[];
attachments: Attachment[];
created_at: string | null;
updated_at: string | null;
}
@@ -113,6 +120,25 @@ export const useNotesStore = defineStore("notes", () => {
reconcile(await api.del<Note>(`/api/notes/${id}/items/${itemId}`));
}
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);
}
async function deleteAttachment(id: string, attId: string): Promise<void> {
reconcile(await api.del<Note>(`/api/notes/${id}/attachments/${attId}`));
}
async function trash(id: string): Promise<void> {
reconcile(await api.post<Note>(`/api/notes/${id}/trash`));
}
@@ -143,6 +169,8 @@ export const useNotesStore = defineStore("notes", () => {
addItem,
updateItem,
deleteItem,
uploadAttachment,
deleteAttachment,
trash,
restore,
deleteForever,