Originally created by: adityaharishch
A new Plan page (/plan, nav key 8) where a developer declares their daily working set each morning via a drag-and-drop two-column UI:
Click any card to open a full-ticket dialog (description, acceptance criteria, epic, due/start date, priority, points, open-in-tracker).
Suggested → Confirm → (locked) → Edit plan → Save / Cancel → (locked)
Confirm locks the plan (no accidental drags); an explicit Edit plan unlocks it, with Save changes / Cancel. No silent writes.
daily_plan + daily_plan_meta tables. Intent only, local meridian.db; nothing is pushed to a tracker./api/plan — confirm/set/add/remove/reorder/skip/reopen, all idempotent UPSERT/DELETE (same pattern as triage)./api/plan/task — full ticket detail for the dialog.lib/daily-plan.ts — server-side candidate scoring/ranking only (no classification).@hello-pangea/dnd.The committed plan is intended to lead the classifier's candidate set as today's declared focus — a boost, never a filter, so recall isn't compromised. That run_task_linker_mlx.py wiring lands in a separate PR.
npm run build ✓ compiled clean. Pre-push suite (fmt + clippy + cargo test + UI build + UI tests) passed.
🤖 Generated with Claude Code
Originally posted by: Akarsh-Hegde
🔍 Code review — daily "today's plan" page
High-effort recall pass (7 finder angles, every survivor verified against source). Solid PR — no crash- or data-corruption-class bugs survived verification. Findings are about input-validation depth, error visibility, and some efficiency/cleanup. Nothing blocking; [#1] and [#2] are worth doing first.
Findings (ranked)
1. ⚠️ Write actions silently coerce bad input → wrong-day writes / plan wipe —
ui/app/api/plan/route.ts:27(validDate) +:111(keysFromBody)Both helpers swallow malformed input and feed write paths:
validDate(bad) → todayString()— aset/add/removewith a malformeddatesilently mutates today's plan instead of returning 400.keysFromBody()returns[]whentask_keysisn't an array. Thesetaction (:121) then runsreplacePlan([]), whose prune loop deletes every committed row for the day and returns200. A client serialization bug or stray request wipes the dev's plan with no error.Suggest: 400 on a missing/non-array
task_keysforconfirm/set(distinguish an explicit empty array — "clear my plan" — from a malformed body), and 400 on an unparseabledateinstead of defaulting. (Trips the "validate all input at system boundaries" rule.)2. GET masks DB/server errors as a valid empty day —
ui/app/api/plan/route.ts:35-41The
catchreturns200with emptyplan/suggestions/available.PlanViewonly setsloadFailedon a fetch rejection (network), not on this 200-with-empty-shape — so a DB failure renders as a normal "nothing planned today" screen. Return500(or anerrorfield the client checks) so a broken backend is distinguishable from an empty day.3. Full board re-scored on every poll and twice per write —
ui/lib/daily-plan.ts:310(buildPlanResponse→buildAvailable)buildAvailable()scans all ofpm_tasks+ runscarryoverKeys+recentWorkedKeysand scores every row. It runs on every GET (incl. the 30s background poll inPlanView.load()), and each POST runs it twice (lazily viaoriginForatroute.ts:78, then in the closingbuildPlanResponseat:162). Cheapest win: haveoriginForreuse theavailablelistbuildPlanResponsealready computes (or accept origin from the client onadd), so a write scores the board once.4. Redundant index —
src/migrations/041_daily_plan.sql:25idx_daily_plan_date ON daily_plan(plan_date)duplicates the leftmost prefix of PK(plan_date, task_key); everyWHERE plan_date = ?already uses the PK index. Drop it — pure write/storage overhead. (Schema is otherwise good: idempotent UPSERTs, correctON CONFLICT, header present.)5. Duplicated helpers —
ui/lib/daily-plan.ts:101&ui/app/api/plan/task/route.ts:30(dueDaysFrom), plusnowIso/tableExistsrepeated across routes. Two near-identicaldueDaysFrommeans a future timezone fix must land in both or they diverge. Consolidate into a shared util.Candidates checked and cleared (so they aren't re-raised)
replacePlan(keys)/repos(keys)are transaction factories never invoked" — false;db.transaction(fn)returns a callable, calling it runs the transaction.dueDaysFromparses dates as UTC" — false;new Date('YYYY-MM-DDT00:00:00')(noZ) is local and it compares local Y/M/D both sides.PlanItemmissingreasonbreaks the card" — false;fromPlanfalls backa?.reason ?? REASON[p.origin] ?? 'Added'.nowIsoregex over-matches" — false;toISOString()always emits 3 fractional digits.getWriteDbwrite thengetDbread is stale" — false; better-sqlite3 commits synchronously, WAL reader sees committed data.🤖 Assisted review via Claude Code.
Related
Tickets:
#1Tickets:
#2Originally posted by: adityaharishch
Thanks for the review — addressed in d92e93c:
dateon a write now returns 400 (no more silent wrong-day write).confirm/setrequiretask_keysto be an array — an explicit[]still means "clear the plan", but a missing/malformed body 400s instead of wiping the day viareplacePlan([]).PlanView.loadtreats a non-OK response as a load failure — a DB error renders the retry state, not a false "nothing planned today".availablelist betweenoriginForandbuildPlanResponse(added optionaldb/availableparams).idx_daily_plan_date—plan_dateis the PK's leftmost column.dueDaysFromconsolidated intolib/daily-plan(the task route imports it now).Per-poll re-scoring (part of [#3]) is left as-is — a fresh GET genuinely needs current board state, and it's a cheap query on a small table; only the redundant per-write second score was removed. The remaining
nowIso/tableExistsduplication is minor and left for a follow-up.Related
Tickets:
#3Ticket changed by: adityaharishch
Originally posted by: adityaharishch
🎉 This PR is included in version 1.53.0 🎉
The release is available on:
v1.53.0Your semantic-release bot 📦🚀