CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 1m12s
CI & Build / integration (push) Successful in 1m41s
CI & Build / Build & push image (push) Successful in 1m1s
Rule 27: no UI, no ship. Three surfaces. THE EDITOR ASKS, but only where the answer can be saved: the fields appear for a plain note and not for a task or a snippet, matching the service gate from step 2 so the form never offers a write the save would reject. The labels are phrased as the QUESTION rather than the field name — "how would someone check this is still true?" and, underneath, "could this become false without anyone editing it?". "Verify with" gets filled in on every note; the question gets filled in on the few that can go stale. `expires_when` appears only once a check exists, and asks for a state rather than a date in the placeholder itself. THE NOTE SHOWS ITS AGE beside the field — "checked 2026-08-28" or "never checked", italic, and nothing at all when no check exists. No red/amber ramp, matching RuleSweepPane: a colour scale would restate the sweep's ordering and force an invented staleness threshold. "Never" is marked because it is categorically different from a date, not a worse one. THE SWEEP is a pane in the Knowledge view, not beside the rules sweep — operator's call, taken over a unified "everything due" surface and over a second pane under /rules. Notes stay where notes live. The cost, accepted knowingly: no single screen shows every unconfirmed record. It REPLACES the feed rather than filtering it, because a facet answers "show me this kind" and this answers "show me what nobody has confirmed" — a question the type chips cannot narrow without under-reporting. Two REST routes for it, since step 3 built only the service and the MCP door. Along the way: NoteEditorView spelled its write payload out at three call sites (save, create, auto-save), so every new field had to be added three times — which is how one of them ends up not carrying it. Now one `payload()` and one `snapshot()`. Known and filed, not fixed: NoteSweepPane copies ~12 scoped CSS rules from RuleSweepPane (#3207). The clean extraction needs prefixed names, because `.age`, `.row-title`, `.lede` and `.actions` all exist scoped in other components and an unscoped global would leak into them — which means editing the shipped rules sweep, blind, inside a step whose acceptance is the operator looking at a different surface.
142 lines
3.7 KiB
TypeScript
142 lines
3.7 KiB
TypeScript
import { ref } from "vue";
|
|
import { defineStore } from "pinia";
|
|
import { apiGet, apiPost, apiPut, apiDelete } from "@/api/client";
|
|
import { useToastStore } from "@/stores/toast";
|
|
import type { Note } from "@/types/note";
|
|
|
|
// Single-note + mutation surface. The list/filter/sort/pagination surface that
|
|
// backed the removed /notes list view was dropped in the 2026-06-02 drift-audit
|
|
// cleanup (no consumers — KnowledgeView and the editors use single-entity and
|
|
// mutation methods only).
|
|
export const useNotesStore = defineStore("notes", () => {
|
|
const currentNote = ref<Note | null>(null);
|
|
const loading = ref(false);
|
|
|
|
async function fetchNote(id: number) {
|
|
loading.value = true;
|
|
try {
|
|
currentNote.value = await apiGet<Note>(`/api/notes/${id}`);
|
|
} catch (e) {
|
|
useToastStore().show("Failed to load note", "error");
|
|
throw e;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function createNote(data: {
|
|
title: string;
|
|
body: string;
|
|
tags?: string[];
|
|
project_id?: number | null;
|
|
milestone_id?: number | null;
|
|
note_type?: string;
|
|
verify_with?: string;
|
|
expires_when?: string;
|
|
}): Promise<Note> {
|
|
try {
|
|
return await apiPost<Note>("/api/notes", data);
|
|
} catch (e) {
|
|
useToastStore().show("Failed to create note", "error");
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function updateNote(
|
|
id: number,
|
|
data: Partial<Pick<
|
|
Note,
|
|
"title" | "body" | "tags" | "project_id" | "milestone_id" | "note_type"
|
|
| "verify_with" | "expires_when"
|
|
>>
|
|
): Promise<Note> {
|
|
try {
|
|
const note = await apiPut<Note>(`/api/notes/${id}`, data);
|
|
if (currentNote.value?.id === id) {
|
|
currentNote.value = note;
|
|
}
|
|
return note;
|
|
} catch (e) {
|
|
useToastStore().show("Failed to update note", "error");
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function deleteNote(id: number) {
|
|
try {
|
|
await apiDelete(`/api/notes/${id}`);
|
|
if (currentNote.value?.id === id) {
|
|
currentNote.value = null;
|
|
}
|
|
} catch (e) {
|
|
useToastStore().show("Failed to delete note", "error");
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function resolveTitle(title: string): Promise<Note> {
|
|
return await apiPost<Note>("/api/notes/resolve-title", { title });
|
|
}
|
|
|
|
async function convertToTask(noteId: number): Promise<Note> {
|
|
try {
|
|
const note = await apiPost<Note>(
|
|
`/api/notes/${noteId}/convert-to-task`,
|
|
{}
|
|
);
|
|
if (currentNote.value?.id === noteId) {
|
|
currentNote.value = note;
|
|
}
|
|
return note;
|
|
} catch (e) {
|
|
useToastStore().show("Failed to convert to task", "error");
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function convertToNote(noteId: number): Promise<Note> {
|
|
try {
|
|
const note = await apiPost<Note>(
|
|
`/api/notes/${noteId}/convert-to-note`,
|
|
{}
|
|
);
|
|
if (currentNote.value?.id === noteId) {
|
|
currentNote.value = note;
|
|
}
|
|
return note;
|
|
} catch (e) {
|
|
useToastStore().show("Failed to convert to note", "error");
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function fetchBacklinks(
|
|
noteId: number
|
|
): Promise<{ type: string; id: number; title: string }[]> {
|
|
const data = await apiGet<{
|
|
backlinks: { type: string; id: number; title: string }[];
|
|
}>(`/api/notes/${noteId}/backlinks`);
|
|
return data.backlinks;
|
|
}
|
|
|
|
async function fetchAllTags(q?: string): Promise<string[]> {
|
|
const params = q ? `?q=${encodeURIComponent(q)}` : "";
|
|
const data = await apiGet<{ tags: string[] }>(`/api/notes/tags${params}`);
|
|
return data.tags;
|
|
}
|
|
|
|
return {
|
|
currentNote,
|
|
loading,
|
|
fetchNote,
|
|
createNote,
|
|
updateNote,
|
|
deleteNote,
|
|
resolveTitle,
|
|
convertToTask,
|
|
convertToNote,
|
|
fetchBacklinks,
|
|
fetchAllTags,
|
|
};
|
|
});
|