CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m34s
CI & Build / Build & push image (push) Successful in 34s
.sweep was the one rule still written identically in both sweep panes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
5.2 KiB
Vue
151 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* The staleness sweep for NOTES: notes that assert a fact, oldest first.
|
|
*
|
|
* Sibling of RuleSweepPane, not a shared component — the two read differently
|
|
* enough that merging them would mean a prop for every difference (a rule has
|
|
* a tier and a statement; a note has a project and opens at a route). What
|
|
* they share is the SHAPE of the judgement, and that is worth copying
|
|
* deliberately rather than abstracting: the ordering carries urgency, "never"
|
|
* is categorically different from a date, and a failed check writes nothing.
|
|
*
|
|
* Lives in the Knowledge view rather than beside the rules sweep (operator's
|
|
* call, milestone 317 step 4): notes stay where notes live. The cost, accepted
|
|
* knowingly, is that there is no single screen showing every record anyone has
|
|
* left unconfirmed — /rules keeps its own.
|
|
*/
|
|
import { onMounted, ref } from "vue";
|
|
import { apiGet, apiPost } from "@/api/client";
|
|
import { useToastStore } from "@/stores/toast";
|
|
|
|
interface DueNote {
|
|
id: number;
|
|
title: string;
|
|
project_id: number | null;
|
|
verify_with: string;
|
|
expires_when: string;
|
|
last_verified: string | null;
|
|
days_since_verified: number | null;
|
|
}
|
|
|
|
const emit = defineEmits<{ "open-note": [id: number] }>();
|
|
|
|
const toast = useToastStore();
|
|
const rows = ref<DueNote[]>([]);
|
|
const loading = ref(false);
|
|
const neverOnly = ref(false);
|
|
const busyId = ref<number | null>(null);
|
|
|
|
async function reload() {
|
|
loading.value = true;
|
|
try {
|
|
const p = new URLSearchParams();
|
|
if (neverOnly.value) p.set("never_only", "1");
|
|
const data = await apiGet<{ notes: DueNote[] }>(
|
|
`/api/notes/due-for-verification?${p}`,
|
|
);
|
|
rows.value = data.notes;
|
|
} catch {
|
|
toast.show("Could not load the sweep", "error");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function verify(id: number, stillTrue: boolean) {
|
|
busyId.value = id;
|
|
try {
|
|
await apiPost(`/api/notes/${id}/verify`, { still_true: stillTrue });
|
|
if (stillTrue) {
|
|
// It has been confirmed, so it leaves the list — the sweep shows what
|
|
// still needs looking at, and leaving it in place would invite a second
|
|
// stamp nobody earned.
|
|
rows.value = rows.value.filter((r) => r.id !== id);
|
|
toast.show("Recorded — checked today");
|
|
} else {
|
|
// It stays. A failed check writes nothing on purpose: the note is wrong
|
|
// rather than in a state worth recording, so it keeps its place until
|
|
// someone corrects, supersedes, or unhooks it.
|
|
toast.show("Recorded as no longer true — the note keeps its place here");
|
|
}
|
|
} catch {
|
|
toast.show("Could not record that", "error");
|
|
} finally {
|
|
busyId.value = null;
|
|
}
|
|
}
|
|
|
|
onMounted(reload);
|
|
defineExpose({ reload });
|
|
</script>
|
|
|
|
<template>
|
|
<section class="sweep">
|
|
<header>
|
|
<h2>Due for verification</h2>
|
|
<p class="sweep-lede">
|
|
Notes that assert a fact about something outside your control — what a
|
|
service does, how a tool behaves. Most notes are decisions and never
|
|
appear here; they have no truth value to go stale.
|
|
</p>
|
|
</header>
|
|
|
|
<div class="sweep-filters">
|
|
<label class="sweep-filter">
|
|
<input v-model="neverOnly" type="checkbox" @change="reload" />
|
|
<span>Never checked only</span>
|
|
</label>
|
|
</div>
|
|
|
|
<p v-if="loading" class="sweep-state">Loading…</p>
|
|
|
|
<!-- An empty sweep is GOOD NEWS and must not read like a broken page. -->
|
|
<p v-else-if="!rows.length" class="sweep-state empty">
|
|
Nothing to check.
|
|
{{ neverOnly
|
|
? "Every note that carries a check has been confirmed at least once."
|
|
: "No note carries a check yet — add one to a note that asserts a fact." }}
|
|
</p>
|
|
|
|
<ol v-else class="sweep-rows">
|
|
<li v-for="n in rows" :key="n.id" class="sweep-row">
|
|
<div class="sweep-row-head">
|
|
<button class="sweep-row-title" @click="emit('open-note', n.id)">{{ n.title }}</button>
|
|
<span class="sweep-age" :class="{ unchecked: n.days_since_verified === null }">
|
|
{{ n.days_since_verified === null
|
|
? "never checked"
|
|
: `${n.days_since_verified}d ago` }}
|
|
</span>
|
|
</div>
|
|
|
|
<dl class="sweep-check">
|
|
<dt>Check</dt>
|
|
<dd>{{ n.verify_with }}</dd>
|
|
<template v-if="n.expires_when">
|
|
<dt>Ends when</dt>
|
|
<dd>{{ n.expires_when }}</dd>
|
|
</template>
|
|
</dl>
|
|
|
|
<div class="sweep-actions">
|
|
<button :disabled="busyId === n.id" @click="verify(n.id, true)">Still true</button>
|
|
<button :disabled="busyId === n.id" @click="verify(n.id, false)">No longer true</button>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
|
|
<p v-if="rows.length" class="sweep-footnote">
|
|
Record a result only after actually running the check. “No longer true” stores nothing
|
|
on purpose — the note is wrong rather than in a state worth recording, so it keeps its
|
|
place here until you correct it, supersede it, or remove its check.
|
|
</p>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
h2 { margin: 0; font-size: 1.05rem; }
|
|
.sweep-lede { margin-top: 0.35rem; }
|
|
/* A check can be a long URL or command with no break point. */
|
|
.sweep-check dd { overflow-wrap: anywhere; }
|
|
</style>
|