diff --git a/frontend/src/views/KnowledgeView.vue b/frontend/src/views/KnowledgeView.vue index 46ad3ed..281da8b 100644 --- a/frontend/src/views/KnowledgeView.vue +++ b/frontend/src/views/KnowledgeView.vue @@ -24,7 +24,7 @@ const router = useRouter(); interface KnowledgeItem { id: number; - note_type: "note" | "task" | "process"; + note_type: "note" | "task" | "process" | "snippet"; title: string; snippet: string; tags: string[]; @@ -42,9 +42,34 @@ interface KnowledgeItem { task_kind?: TaskKind; } +// ─── The facet vocabulary ───────────────────────────────────────────────────── +// Mirrors services/knowledge._FACETS, which is where it is defined for real. +// A facet spans BOTH typing axes — a record TYPE (note / process / snippet) or +// a task KIND (`task` for any, else issue / spike) — because that is what this +// feed actually holds. +// +// `plan` is still a valid facet at the API, for the 90 legacy plan-tasks, but +// it has no chip: retired in 0066, it kept a chip of its own for longer than +// `issue` — 17% of every task here — went without one (#3128). Those rows are +// still reachable under Tasks, wearing a Plan badge. +type Facet = "" | "note" | "task" | "issue" | "spike" | "snippet" | "process"; + +// The facets that select TASKS. Kinds are subsets of `task`, so any of them +// means the duplicate report should be comparing tasks. +const TASK_FACETS = new Set(["task", "issue", "spike"]); + +const FACET_CHIPS: [Exclude, string][] = [ + ["note", "Notes"], + ["task", "Tasks"], + ["issue", "Issues"], + ["spike", "Spikes"], + ["snippet", "Snippets"], + ["process", "Processes"], +]; + // ─── Filter state ───────────────────────────────────────────────────────────── -const activeType = ref<"" | "note" | "task" | "plan" | "process">(""); +const activeType = ref(""); const activeTag = ref(""); const sortMode = ref<"modified" | "created" | "alpha" | "type">("modified"); const searchQuery = ref(""); @@ -70,9 +95,10 @@ const dupGroups = ref([]); const dupSuggestion = ref(""); const dupLoading = ref(false); const dupChecked = ref(false); -// The report follows the type filter: viewing tasks checks tasks. Anything -// else (all / plan / process) checks notes — the kind with the most to find. -const dupKind = computed(() => (activeType.value === "task" ? "task" : "note")); +// The report follows the type filter: viewing tasks — under ANY task facet, +// including a single kind — checks tasks. Everything else checks notes, the +// kind with the most to find. +const dupKind = computed(() => (TASK_FACETS.has(activeType.value) ? "task" : "note")); async function loadDuplicates() { dupLoading.value = true; @@ -96,8 +122,11 @@ watch(dupKind, () => { dupChecked.value = false; dupGroups.value = []; }); // ─── Type counts ────────────────────────────────────────────────────────────── -interface KnowledgeCounts { note: number; task: number; plan: number; process: number; total: number } -const typeCounts = ref({ note: 0, task: 0, plan: 0, process: 0, total: 0 }); +// One number per facet, plus the grand total. Partial because the server sends +// a key only for a facet it has rows for. Kinds are subsets of `task` and are +// deliberately absent from `total` — including them would count an issue twice. +type KnowledgeCounts = Partial, number>> & { total: number }; +const typeCounts = ref({ total: 0 }); async function fetchCounts() { try { @@ -274,9 +303,18 @@ function isOverdue(item: KnowledgeItem): boolean { return new Date(item.due_date) < new Date(new Date().toDateString()); } +// Each record kind opens in ITS OWN editor. A snippet used to fall through to +// /notes/:id, whose save is a plain PATCH of the body — which left the snippet's +// derived `data` mirror describing the previous version (#3128). The service now +// recomposes the mirror either way, so this is no longer the guard; it is simply +// that the note editor cannot edit a snippet's signature, language or locations, +// and offering it as the way in was always wrong. Processes stay here on +// purpose: they have no editor of their own and the note editor knows the type. function openItem(item: KnowledgeItem) { if (item.note_type === 'task') { router.push(`/tasks/${item.id}`); + } else if (item.note_type === 'snippet') { + router.push(`/snippets/${item.id}`); } else { router.push(`/notes/${item.id}`); } @@ -380,14 +418,14 @@ onUnmounted(() => { {{ typeCounts.total }} @@ -501,6 +539,7 @@ onUnmounted(() => { Note {{ item.task_kind === 'plan' ? 'Plan' : 'Task' }} Process + Snippet