Menu

#288 feat(ui): daily "today's plan" page with declared working set

closed
nobody
released (243)
2026-06-16
2026-06-15
Anonymous
No

Originally created by: adityaharishch

What

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:

  • Today — a pre-filled, scored column of what looks active (carryover / in-progress / due-soon, due-date weighted). Drag to reorder, drag out to remove.
  • Your tasks — a searchable, sortable board of assigned tickets; drag or + Add into Today.

Click any card to open a full-ticket dialog (description, acceptance criteria, epic, due/start date, priority, points, open-in-tracker).

Flow

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.

Implementation

  • Migration 041daily_plan + daily_plan_meta tables. Intent only, local meridian.db; nothing is pushed to a tracker.
  • /api/planconfirm/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).
  • White & blue theme scoped to the page; app-like layout (no page-level scrollbar); 30s background poll refreshes the board without clobbering local edits or an active drag.
  • Uses @hello-pangea/dnd.

Follow-up (not in this PR)

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.

Test

npm run build ✓ compiled clean. Pre-push suite (fmt + clippy + cargo test + UI build + UI tests) passed.

🤖 Generated with Claude Code

Related

Tickets: #289
Tickets: #290
Tickets: #294

Discussion

  • Anonymous

    Anonymous - 2026-06-15

    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 wipeui/app/api/plan/route.ts:27 (validDate) + :111 (keysFromBody)
    Both helpers swallow malformed input and feed write paths:

    • validDate(bad) → todayString() — a set/add/remove with a malformed date silently mutates today's plan instead of returning 400.
    • keysFromBody() returns [] when task_keys isn't an array. The set action (:121) then runs replacePlan([]), whose prune loop deletes every committed row for the day and returns 200. A client serialization bug or stray request wipes the dev's plan with no error.

    Suggest: 400 on a missing/non-array task_keys for confirm/set (distinguish an explicit empty array — "clear my plan" — from a malformed body), and 400 on an unparseable date instead of defaulting. (Trips the "validate all input at system boundaries" rule.)

    2. GET masks DB/server errors as a valid empty dayui/app/api/plan/route.ts:35-41
    The catch returns 200 with empty plan/suggestions/available. PlanView only sets loadFailed on a fetch rejection (network), not on this 200-with-empty-shape — so a DB failure renders as a normal "nothing planned today" screen. Return 500 (or an error field the client checks) so a broken backend is distinguishable from an empty day.

    3. Full board re-scored on every poll and twice per writeui/lib/daily-plan.ts:310 (buildPlanResponsebuildAvailable)
    buildAvailable() scans all of pm_tasks + runs carryoverKeys + recentWorkedKeys and scores every row. It runs on every GET (incl. the 30s background poll in PlanView.load()), and each POST runs it twice (lazily via originFor at route.ts:78, then in the closing buildPlanResponse at :162). Cheapest win: have originFor reuse the available list buildPlanResponse already computes (or accept origin from the client on add), so a write scores the board once.

    4. Redundant indexsrc/migrations/041_daily_plan.sql:25
    idx_daily_plan_date ON daily_plan(plan_date) duplicates the leftmost prefix of PK (plan_date, task_key); every WHERE plan_date = ? already uses the PK index. Drop it — pure write/storage overhead. (Schema is otherwise good: idempotent UPSERTs, correct ON CONFLICT, header present.)

    5. Duplicated helpersui/lib/daily-plan.ts:101 & ui/app/api/plan/task/route.ts:30 (dueDaysFrom), plus nowIso/tableExists repeated across routes. Two near-identical dueDaysFrom means 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.
    • "dueDaysFrom parses dates as UTC"false; new Date('YYYY-MM-DDT00:00:00') (no Z) is local and it compares local Y/M/D both sides.
    • "PlanItem missing reason breaks the card"false; fromPlan falls back a?.reason ?? REASON[p.origin] ?? 'Added'.
    • "nowIso regex over-matches"false; toISOString() always emits 3 fractional digits.
    • "getWriteDb write then getDb read is stale"false; better-sqlite3 commits synchronously, WAL reader sees committed data.

    🤖 Assisted review via Claude Code.

     

    Related

    Tickets: #1
    Tickets: #2

  • Anonymous

    Anonymous - 2026-06-15

    Originally posted by: adityaharishch

    Thanks for the review — addressed in d92e93c:

    1. Input validation at the write boundary. Malformed date on a write now returns 400 (no more silent wrong-day write). confirm/set require task_keys to be an array — an explicit [] still means "clear the plan", but a missing/malformed body 400s instead of wiping the day via replacePlan([]).
    2. GET masking errors. GET now returns 500 on a backend error (was 200 + empty shape), and PlanView.load treats a non-OK response as a load failure — a DB error renders the retry state, not a false "nothing planned today".
    3. Double board-scoring per write. The POST now scores the board once and shares the available list between originFor and buildPlanResponse (added optional db/available params).
    4. Redundant index dropped. Removed idx_daily_plan_dateplan_date is the PK's leftmost column.
    5. Duplicated dueDaysFrom consolidated into lib/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/tableExists duplication is minor and left for a follow-up.

     

    Related

    Tickets: #3

  • Anonymous

    Anonymous - 2026-06-16

    Ticket changed by: adityaharishch

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-06-16

    Originally posted by: adityaharishch

    🎉 This PR is included in version 1.53.0 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

     

Log in to post a comment.