feat(ui): a note's check is editable, dated and sweepable (#3167, milestone 317 step 4)
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
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.
This commit is contained in:
@@ -19,7 +19,10 @@ from scribe.services.notes import (
|
||||
get_note_for_user,
|
||||
get_or_create_note_by_title,
|
||||
list_notes,
|
||||
mark_note_verified,
|
||||
notes_due_for_verification,
|
||||
update_note,
|
||||
verification_row,
|
||||
)
|
||||
from scribe.services.note_drafts import upsert_draft, get_draft, delete_draft
|
||||
from scribe.services import dedup as dedup_svc
|
||||
@@ -499,3 +502,62 @@ async def graph_route():
|
||||
shared_tags = request.args.get("shared_tags", "false").lower() == "true"
|
||||
graph = await build_note_graph(uid, project_id=project_id, include_shared_tags=shared_tags)
|
||||
return jsonify(graph)
|
||||
|
||||
|
||||
# ── The staleness sweep (milestone 317) ──────────────────────────────────────
|
||||
# The web half of notes_due_for_verification / mark_note_verified. Same
|
||||
# contract as the MCP door and the rules routes beside it — the service holds
|
||||
# the behaviour, these two just parse and serialise.
|
||||
|
||||
@notes_bp.route("/due-for-verification", methods=["GET"])
|
||||
@login_required
|
||||
async def notes_due_route():
|
||||
"""Notes that carry a check, oldest verification first, never-checked top.
|
||||
|
||||
Query params: older_than_days, project_id, never_only. A note with no
|
||||
`verify_with` never appears — it is a decision, not a fact.
|
||||
"""
|
||||
uid = get_current_user_id()
|
||||
args = request.args
|
||||
try:
|
||||
older = int(args.get("older_than_days", 0) or 0)
|
||||
project = int(args.get("project_id", 0) or 0)
|
||||
except ValueError:
|
||||
return jsonify({"error": "older_than_days and project_id must be integers"}), 400
|
||||
try:
|
||||
notes = await notes_due_for_verification(
|
||||
uid,
|
||||
older_than_days=older,
|
||||
project_id=project or None,
|
||||
never_only=args.get("never_only", "").lower() in ("1", "true", "yes"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
# A 400, not a silently narrowed result: a filter that quietly answers
|
||||
# a different question is the failure this whole surface exists to
|
||||
# catch.
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
return jsonify({
|
||||
"notes": [verification_row(n) for n in notes],
|
||||
"total": len(notes),
|
||||
})
|
||||
|
||||
|
||||
@notes_bp.route("/<int:note_id>/verify", methods=["POST"])
|
||||
@login_required
|
||||
async def mark_note_verified_route(note_id: int):
|
||||
"""Record that the note's check was run. Body: {"still_true": bool}.
|
||||
|
||||
`still_true: false` writes nothing — a note whose check failed is wrong,
|
||||
not in a recordable state — so it keeps its place at the top of the sweep.
|
||||
"""
|
||||
data = await request.get_json() or {}
|
||||
uid = get_current_user_id()
|
||||
still_true = bool(data.get("still_true", True))
|
||||
note = await mark_note_verified(note_id, uid, still_true)
|
||||
if note is None:
|
||||
return jsonify({
|
||||
"error": "note not found, not writable by you, or carries no verify_with"
|
||||
}), 404
|
||||
payload = verification_row(note)
|
||||
payload["verified"] = still_true
|
||||
return jsonify(payload)
|
||||
|
||||
Reference in New Issue
Block a user