This file is a running backlog of ideas, cleanups, and future improvements.
Actionable TODOs must use one of the following forms (do not include backticks):
TODO+(P1)throughTODO+(P5)— prioritized work
-
TODO(continual) Keep
/docs/README.mdup to date as new design docs are added -
TODO(continual) Manual QA regression sweep (pre-showcase)
- Auth + redirects:
- Deep link to a protected page → confirm redirect to
/login?redirect=...works and sanitization prevents external redirects - Sign out from a deep-linked page → confirm app returns to a safe public route (and no loops)
- Deep link to a protected page → confirm redirect to
- Multi-tab:
- Sign out in Tab A → Tab B should not show stale private data after navigation/refresh
- Persistence hygiene:
- Switch accounts without a hard reload → confirm no cross-user flash (tasks, inbox indicators, favorites, user UI)
- Use DevPage "Clear all user caches" → confirm stores rehydrate cleanly on next refresh
- Demo-only UX:
- Start demo mode → confirm confirmation dialog + one-time tour appear
- Fail demo request (simulate by turning off network) → confirm demo session flag is cleared and error is visible
- Auth + redirects:
- TODO(P1) Basic UX polish passes: keyboard navigation + focus states (accessibility), mobile/responsive review, and loading/error empty-state consistency across pages.
- Status (Feb 2026): most “no-layout-risk” a11y fixes shipped (focus-visible rings, skip-to-content, icon-button aria-labels, remove nested interactive header controls, form label/id/name hygiene).
- Tester script/checklist:
docs/TESTER_SCRIPT.md - MVP-critical manual validation (3 demo-critical flows)
- Flow 1: Sign in → Inbox triage → edit task
- Keyboard-only: tab order makes sense; focus is visible; no traps; Esc closes dialogs; Enter/Space activate buttons; toast notifications don’t steal focus.
- Screen-reader-name sniff test: icon-only buttons and collapsibles announce sensible names.
- Mobile viewport spot-check: no horizontal scroll; action buttons still reachable.
- Flow 2: Lists → open pane stack → navigate stack
- Keyboard-only: task rows are reachable; stack panes don’t create focus traps; Back/close behaviors are predictable.
- Responsive: base breakpoint doesn’t overflow; panes usable on small widths.
- Flow 3: Settings
- Keyboard-only: selects/switches operable; focus ring visible; no missing labels.
- Autofill sanity: inputs/selects have stable labels and names.
- Flow 1: Sign in → Inbox triage → edit task
- Loading/error/empty-state audit (MVP)
- Standardize primary routes’ error states + Retry (Today/Week/Month/Tasks/Updates/Lists/Favorites/Settings)
- Ensure every page has: loading (spinner), error (message + Retry), empty (clear text + next action when possible).
- Confirm no raw/un-styled error dumps (console-only errors are OK; UI should still show a friendly error block).
- Stretch (optional, but great for employer confidence)
- Add automated a11y checks (Playwright + axe) for the 3 flows’ key routes.
- Add a small responsive smoke (Playwright setViewportSize + screenshot diff is optional).
--[DONE]--
-
TODO(P1) Manual sanity (demo-visible):- Create 3 tasks: due yesterday, due today, due tomorrow → confirm Today badges/sections match expectations.
- Change a task’s due date via Edit Task → confirm it moves between Overdue/Due today immediately.
- Timezone drift check (Chrome DevTools → More tools → Sensors → Timezone override):
- Override to a negative offset (e.g.
Pacific/Honolulu) then a positive offset (e.g.Pacific/Kiritimati) and refresh. - Confirm the same task stays classified as overdue vs due-today vs future (no day shifting).
- Override to a negative offset (e.g.
- Confirm “Someday/Anytime” tasks never appear in overdue/due-today counts.
-
TODO(P2) Performance: investigate bundle size + add route-level code splitting- Vite warning: main JS chunk > 500 kB
- Likely contributors: Chakra UI, AWS Amplify (Auth + API), large route components
- Status (Feb 2026): route-level code splitting added for the biggest pages + basic
manualChunksvendor splitting; mainindexchunk is now much smaller, though Vite may still warn about a large vendor chunk. - Route-level
React.lazy()(highest impact first):- ListDetailsPage / list task stack route
- UpdatesPage
- TasksPage
- InboxPage
- Lazy-load later (lower impact / smaller):
- ListsPage
- FavoritesPage
- ProfilePage / SettingsPage / MonthPage
- DevPage (already lazy-loaded)
- Suspense fallback:
- Place the primary
<Suspense fallback={...}>in AppShell’s main content area wrapping the<Outlet />region - Keep the sidebar/topbar always-rendered so navigation remains responsive while a page chunk loads
- Place the primary
- Error handling / boundaries:
- Preserve existing ErrorBoundary behavior for route renders (don’t move the boundary inside a lazy-loaded component)
- Ensure the ErrorBoundary still wraps whatever renders the lazy route element so dynamic import failures and render errors surface consistently
- Optional: consider
manualChunksinvite.config.tsif needed
-
TODO(P2) Minimal automated smoke coverage: even 1–2 Playwright tests (“sign in → seed → see Today/Inbox/Tasks render”) gives huge confidence for a showcase.- Run:
npm run test:e2e - Optional HTML report:
npm run test:e2e:htmlthennpm run test:e2e:report
- Run:
-
TODO(P2) Talk about how the demo-mode "script" works, and:- Is there still more work that needs to be done?
- How are you supposed to open it after closing to do a step?
- We either need to add more info about how to use demo mode including:
- How to clear demo-mode when done, etc.
- Use modal or create a demo page for this?
-
TODO(P2) Normalize all time-related features by initializing and using the current time zone as the base for all times- (foundation):
src/services/dateTime.tscentralizes timezone detection (getUserTimeZone) + day-key helpers for comparisons/labels. - (comparisons): overdue/due-soon logic uses day-key semantics in triage/views.
- (consistency): removed remaining ad-hoc ISO/date conversions in UI and standardized due-date encoding/decoding.
- Added
isoToDateInputValue()and used day-key extraction (no timezone-dependentDateparsing fordueAt). - Made
normalizeDateInputToIso()strict (reject invalid rollover dates) and consistently storeYYYY-MM-DDT00:00:00.000Z.
- Added
- (foundation):
-
TODO(P2) CI: enforcenpm run lint+npm run buildon PRs- Goal: keep the UI-layer API import restrictions enforced and prevent accidental regressions
-
TODO(P2) Harden owner-based GraphQL auth rules- Prevent clients from reassigning the
ownerfield on @model types - Apply field-level auth or remove
ownerfrom client-writable inputs - Ensure:
- owners can CRUD only their own records
- Admin group can read/write across users
- ownership cannot be transferred via mutation payloads
- Context:
- Amplify warning: “owners may reassign ownership”
- Deferred intentionally for MVP speed
- Prevent clients from reassigning the
-
TODO(P3) Revisit Vite/Rollup
manualChunks(safe vendor splitting)- Context: hit Rollup error
Circular chunk: vendor-floating -> vendor -> vendor-floatingduring deploy. - Goal: keep a “nice” vendor split without chunk cycles (prefer broader buckets; keep UI positioning deps together).
- Validate:
npm run build+npm run preview, and confirm Netlify build/deploy succeeds.
- Context: hit Rollup error
-
TODO(P3) Mitigation for deploy-time “stale chunk” failures (Netlify + Vite lazy routes)
-
Problem:
- With CI/CD (GitHub → Netlify), a user can be running an older HTML/JS that references chunk filenames that no longer exist after a deploy.
- Symptoms: navigation or lazy-route loads crash with:
- “Failed to fetch dynamically imported module”
- “Loading chunk … failed” / “ChunkLoadError”
- “Importing a module script failed”
- Vite preload failures (vite:preloadError)
-
UX goal:
- Catch this globally.
- Show a single non-spammy toast:
- “A new version of TaskMaster is available. Refresh to update.”
- Button: Refresh
- Optional “auto-recover once”: reload automatically a single time, then fall back to toast.
-
Implementation (recommended “easy + solid”):
-
Add a global install function that:
- Listens to:
- window 'unhandledrejection'
- window 'error'
- window 'vite:preloadError' (Vite)
- Detects chunk-ish errors by message matching (case-insensitive)
- Dedupe so it only triggers once per session (sessionStorage flag)
- Optionally auto-reloads once, then shows the toast on subsequent failures
- Listens to:
-
Install it once during app bootstrap (src/main.tsx is best).
-
-
Example code:
-
File: src/runtime/staleChunkGuard.ts
type Notify = (opts: { title: string; description?: string; actionLabel?: string; onAction?: () => void }) => void; const SESSION_KEY = "taskmaster:stale-chunk-guard-fired"; const AUTO_RELOAD_KEY = "taskmaster:stale-chunk-guard-auto-reloaded"; function normalizeMessage(err: unknown): string { if (!err) return ""; if (typeof err === "string") return err; const anyErr = err as any; return String(anyErr?.message ?? anyErr?.reason ?? anyErr?.error ?? ""); } function isStaleChunkError(err: unknown): boolean { const msg = normalizeMessage(err).toLowerCase(); return ( msg.includes("failed to fetch dynamically imported module") || msg.includes("loading chunk") || msg.includes("chunkloaderror") || msg.includes("importing a module script failed") || msg.includes("failed to fetch") && msg.includes(".js") // last-resort heuristic ); } function didFireThisSession(): boolean { return sessionStorage.getItem(SESSION_KEY) === "1"; } function markFired(): void { sessionStorage.setItem(SESSION_KEY, "1"); } function autoReloadOnce(): boolean { const already = sessionStorage.getItem(AUTO_RELOAD_KEY) === "1"; if (already) return false; sessionStorage.setItem(AUTO_RELOAD_KEY, "1"); window.location.reload(); return true; } export function installStaleChunkGuard(notify: Notify, opts?: { autoReloadOnce?: boolean }) { const autoReload = Boolean(opts?.autoReloadOnce); const handle = (err: unknown) => { if (!isStaleChunkError(err)) return; // If we haven't attempted an auto-reload yet, do it once. if (autoReload) { const reloaded = autoReloadOnce(); if (reloaded) return; } // Otherwise: show a toast once per session. if (didFireThisSession()) return; markFired(); notify({ title: "Update available", description: "A new version of TaskMaster is available. Refresh to update.", actionLabel: "Refresh", onAction: () => window.location.reload(), }); }; window.addEventListener("unhandledrejection", (e) => handle((e as PromiseRejectionEvent).reason)); window.addEventListener("error", (e) => handle((e as ErrorEvent).error ?? (e as ErrorEvent).message)); window.addEventListener("vite:preloadError", (e) => handle(e)); }
-
Install location: src/main.tsx (preferred)
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { installStaleChunkGuard } from "./runtime/staleChunkGuard"; import { toast } from "./components/ui/Toaster"; // use whatever you already export // Adapt this to your Toaster API. const notify = ({ title, description, actionLabel, onAction }: any) => { toast({ title, description, action: actionLabel ? { label: actionLabel, onClick: onAction } : undefined, // optional: duration, closable, etc. }); }; installStaleChunkGuard(notify, { autoReloadOnce: true }); ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <App /> </React.StrictMode> );
-
-
“Pro” approach (defer unless this becomes a real multi-user app):
- Version marker (VITE_APP_VERSION from git SHA) + optional
version.json. - On chunk failure, fetch the marker to confirm a version change, then prompt refresh.
- Only consider SW/Workbox when you actually need offline or true update orchestration.
- Version marker (VITE_APP_VERSION from git SHA) + optional
-
-
TODO(P3) AdminPage: refactor filter controls to use shared UI patterns (tooltip labels + FormSelect) and remove raw HTML selects
-
TODO(P3) Settings persistence: versioned blob + migrations + validation
- Candidate home:
UserProfile.settings/settingsVersion(preferred) vs a separate Settings model - Migration path: bring existing local settings (e.g. dueSoonWindowDays) + tip dismissals into the settings blob
- Persisted settings helper:
- a single place to read/write user settings (local-first now, UserProfile-backed later)
- versioned settings schema + forward-only migrations
- safe defaults + normalization
- easy to add new settings without scattering localStorage keys
- Runtime validators + normalizers for
settingsandonboardingblobs- validate shape aggressively before using values in UI
- default-fill + version migrations (forward-only)
- Stretch: persist settings + tips in GraphQL (AppSync) instead of localStorage once migrations + UX are stable
- Candidate home:
-
TODO(P3) Add “Snooze” for overdue (and due-soon) tasks
- Goal: temporarily hide tasks from overdue/due-soon indicators and sections without losing them
- UX ideas:
- Add a Snooze button on TaskRow and/or in Inbox triage sections
- Provide quick presets: 1 hour, later today, tomorrow morning, next week
- Optional: “Bump due date” quick actions (e.g. +1 day / +3 days / +1 week)
- Show a “Snoozed until …” chip + an “Unsnooze” action
- Options (implementation):
- A) Local-only (fastest): add a user-scoped persisted map like
snoozedUntilByTaskId: Record<string, string>(ISO) ininboxStoreorlocalSettingsStore; triage views ignore tasks untilnow >= snoozedUntil. - B) Task-backed (cross-device): add a nullable GraphQL field on Task (e.g.
snoozedUntil) and treat it like a soft visibility rule in Today/Week/Inbox. - C) Due-date mutation (aka “bump due date”; simplest but changes meaning): implement snooze by moving
dueAtforward (e.g. +1 day / +3 days) and optionally recording the prior due date in a local-only “original due” map.
- A) Local-only (fastest): add a user-scoped persisted map like
- Open questions:
- Should snoozed tasks be hidden everywhere, or only from Inbox/Triage + overdue badges?
- Should snooze apply only to open tasks, and should it clear automatically when a task is completed?
- Do we want “bump due date” as a distinct action (explicitly edits dueAt) vs treating it as one Snooze strategy?
-
TODO(P3) Replace the tick/refresh() pattern everywhere (after migration)
- (prereq): Zustand store actions already call
refreshAll()after mutations. - (refactor): remove remaining
refreshprop threading and redundantrefresh()calls in pages/components.
- (prereq): Zustand store actions already call
-
TODO(P3) Replace
any[]pagination infetchAllTasksForListwith a structural “API-like” type- Goal: better editor help + safer mapping in
toTaskUI
- Goal: better editor help + safer mapping in
-
TODO(P3) Add a manual QA checklist for auth/cache hygiene- Sign in as User A → verify tasks + user metadata
- Sign out → sign in as User B → verify no cross-user flashes
- Verify DevPage "Clear all user caches" produces a clean, correct state
- Status: validated Feb 2026
-
TODO(P3) Add a dark/light toggle mode
- TODO(P4) ESLint guardrail: if we introduce additional Amplify enums used by UI (beyond TaskStatus/TaskPriority), expand the allowlist
- Update
eslint.config.js(no-restricted-imports→ allowImportNames) so UI can import enums from../APIwithout importing generated models
- Update
- TODO(P5) Offline mode (follow
/docs/offline-mode-design.md)- Introduce IndexedDB-backed cache
- Add offline mutation queue + replay logic
- Add sync status UI (offline / syncing / error states)
- GraphQL CRUD is stable
- Zustand is the primary client cache
- MVP UX is complete
-
TODO(stretch) Inbox triage: decide whether “dismiss” should exist long-term and how it fits in with future features and how it differs from UpdatesPage.
- Inbox triage supports “dismiss”.
- define long-term intent (dismiss vs snooze), and ensure dismissed tasks remain discoverable (show dismissed and/or add a system inbox list route).
-
TODO(stretch) Add deploy-time CSP + baseline security headers
- Goal: make XSS impact much smaller even if a bug slips in
- Add via hosting config:
- Amplify Hosting: custom headers (recommended) or a CloudFront response headers policy
- Netlify:
_headersfile inpublic/(ornetlify.toml)
- Safe starter CSP (works for most SPAs; validate against Amplify UI + Vite build output and iterate as needed):
default-src 'self'base-uri 'self'object-src 'none'frame-ancestors 'none'img-src 'self' data: https:font-src 'self' data: https:style-src 'self' 'unsafe-inline' https:script-src 'self'connect-src 'self' https:
- Notes / iteration knobs:
- If auth or API calls fail, extend
connect-srcwith the specific Amplify/AppSync/Cognito endpoints your deployed app uses. - If you introduce analytics or third-party widgets, whitelist only their specific origins.
- Keep
script-srcstrict (avoid'unsafe-inline'/'unsafe-eval'unless you have a proven need).
- If auth or API calls fail, extend
- Also consider:
X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy - Document final headers in
docs/SECURITY_CHECKLIST.md
-
TODO(stretch) Backend validation for field constraints (don’t rely on client)
- GraphQL schema enforces type/required fields, but not max string lengths
- Enforce title/name/description max length + date semantics in AppSync resolvers (VTL/JS) or a Lambda wrapper
- Reject invalid inputs with clear errors; keep client normalization for UX only
-
TODO(stretch) Add more contextual Tips across the app (including reusable component-level tips)
- Audit existing Tips for relevance and remove any that feel redundant
- Candidate components: task/list headers, empty states, filter bars
-
TODO(stretch) Admin: add “Select all across pagination” for list selection
- In Admin flow, Lists tab currently supports “Select all loaded”
- Stretch: load remaining list pages for the selected account, then select all
- Useful for power admins; keep optional to avoid extra AppSync load by default
-
TODO(stretch) Admin: enable editing/deleting items from the Admin console
- Scope: allow Admins to update/delete Tasks and TaskLists across users from
/admin - Add explicit UX guardrails (confirmations, clear labels, read-only defaults)
- Ensure mutations are protected by GraphQL
@authgroup rules (Admin override) and cannot reassign ownership - Consider safe-mode behavior for legacy records (required fields / partial rows)
- Intentionally deferred for now so the new Admin UX ships without risky write paths
- Scope: allow Admins to update/delete Tasks and TaskLists across users from
-
TODO(stretch) Make Updates logging “perfect” by comparing before/after task status
- Today:
taskmasterApi.updateTask()infers completed/reopened vs updated based on which fields are present in the payload - Ideal: compare previous task state vs next task state (e.g., status transition) before emitting
task_completed/task_reopened - Options:
- Pass
prevStatus(or a snapshot) from UI to the API wrapper as metadata - Fetch the task before update (extra round-trip; be careful about latency)
- Pass
- Today:
-
TODO(stretch) Persist Inbox + Updates in DynamoDB per user
- Goal: Make Inbox “dismissed/new/due-soon” state + Updates feed/read markers follow the user across devices.
- Current: Stored in localStorage only (per-device).
- Proposed:
- Add models (one of these approaches):
- A) UserUXState (1 row per user) containing:
- inbox: dismissedTaskIds[], lastViewedAt, dueSoonWindowDays
- updates: events[] OR readMarkers + a capped events list
- B) Separate models:
- InboxState @model (owner) 1 row per user
- UpdateEvent @model (owner) many rows per user (with query by occurredAt)
- A) UserUXState (1 row per user) containing:
- Keep @auth owner rules; Admin group can read/debug.
- Implement:
- On sign-in: fetch user UX state -> hydrate stores
- On state changes: write-through (debounced) to GraphQL
- Add retention policy (cap events count or TTL via DynamoDB TTL)
- Migration:
- First run: if localStorage has inbox/updates, import once then clear local.
- Add models (one of these approaches):
- Notes:
- Prefer capped events to avoid unbounded growth.
- Consider idempotent seeds / deterministic ids for update events.
-
TODO(stretch) Admin: delete Cognito user accounts from the app
- Would require a backend function (Lambda) with Cognito admin permissions (e.g.
AdminDeleteUser). - UI alone can’t securely delete Cognito identities.
- Consider coupling with an app-data cleanup flow (delete UserProfile + tasks/lists) for a full deprovision.
- Would require a backend function (Lambda) with Cognito admin permissions (e.g.
-
TODO(stretch) Post-MVP: Add AWS WAF rate limiting for
POST /auth/demo- Create a Web ACL in
us-east-2and attach it to the API Gateway stage fortaskmasterAuth - Add a rate-based rule keyed by source IP (start with 5 requests / 5 minutes)
- Validate behavior (normal demo works; burst requests get blocked)
- Remove the in-Lambda
ipBucketslimiter (or keep as a fallback with a higher threshold, but don’t double-punish users)
- Create a Web ACL in
-
TODO(stretch) Consider adding an "Overdue" View (conditional)
- Option: keep overdue sections inside Today/Week for visibility (MVP), but also add a dedicated
/overdueview. - UX: only show the Overdue view link when there are overdue tasks; style it as high-salience (red + icon).
- Option: keep overdue sections inside Today/Week for visibility (MVP), but also add a dedicated
-
TODO(postmvp) Switch from Pattern B → Pattern A
- During iteration: local-first + optional sync
- Final MVP: server-authoritative in
UserProfile - See definition: docs/ARCHITECTURE.md → “Pattern A / Pattern B”.
-
TODO(postmvp) Demo entry: unify flow via
/login?intent=demo- Goal: make demo entry a single, shareable, route-driven flow instead of duplicating logic across pages.
- UX:
- Home “Try Demo” navigates to
/login?intent=demo&redirect=... - LoginPage uses
intent=demoto auto-open (or prominently highlight) the demo confirmation dialog
- Home “Try Demo” navigates to
- Code health:
- Centralize demo-start logic in one place (a hook/service), so Home/Login don’t maintain parallel versions
- Keep
redirectbehavior consistent with normal auth flows
-
TODO(postmvp) Demo tools: make “Clear demo data” safe for real users
- Problem: current behavior moves non-demo tasks that are inside demo lists into the system
__Inbox. - Risk: if a user has real work living under demo lists (incl. subtasks), this becomes a cleanup burden.
- Desired behavior (safer defaults):
- Never mass-move non-demo tasks into
__Inboxby default (even when they’re inside demo lists). - Prefer: convert the containing demo list(s) to non-demo (set
isDemo=false, rename with a “Recovered” prefix), OR prompt for a destination list, OR create a dedicated “Recovered” list and preserve hierarchy. - Add stronger UX guardrails: explicit confirmation copy + counts (lists/tasks/subtasks), and a clear “this is destructive” warning.
- Never mass-move non-demo tasks into
- MVP note: intentionally deferred, but tracked for post-MVP hardening.
- Problem: current behavior moves non-demo tasks that are inside demo lists into the system
-
TODO(postmvp) Demo tools: allow converting demo tasks to “real” tasks
- Goal: let users keep a demo task without recreating/copying it.
- UX: add a “Keep this task” / “Convert to real” action (best surface: TaskDetailsPane; optional in Edit Task).
- Behavior:
- Convert the selected Task (and optionally its subtask subtree) to
isDemo=false. - If the task currently lives in a demo list:
- Prompt for destination list (default: Inbox), OR
- Auto-create a non-demo “Recovered” list and move it there, OR
- Offer “Convert this list to real” (set list
isDemo=false) as an alternative.
- Convert the selected Task (and optionally its subtask subtree) to
- Guardrails:
- Clear confirmation copy (explain how “Clear demo data” interacts with items living inside demo lists).
- Preserve hierarchy + ordering where possible.
- Ensure the conversion is resilient if some legacy rows are missing required fields.
-
TODO(postmvp) Audit & remove legacy non-null violations (starting with
isDemo)- See: docs/LEGACY_DATA_CLEANUP.md
- Why: current UI reads prefer selecting
isDemo, but fall back to omitting it when the backend throwsCannot return null for non-nullable type ... isDemo. - How to detect:
- In-app: use Admin diagnostics (lists/tasks “isDemo mode” indicators) and look for the safe-fallback warning states.
- Backend: run an admin-only scan/query for any
Task/TaskListrecords missingisDemo(or returning null) and count them. - Logs: watch for GraphQL errors containing
Cannot return null for non-nullable type+ the field name.
- What to do once confirmed:
- Backfill: write a one-off admin script/mutation pass to set
isDemo=falseon any legacy records that are missing it. - Then: remove the safe-fallback query paths and always select
isDemoin normal UI queries.
- Backfill: write a one-off admin script/mutation pass to set
- Also check: any other known legacy required fields (e.g. admin service already has safe fallbacks for
UserProfile.email), and remove those fallbacks once fully backfilled.
-
TODO(postmvp) Decide “reset/purge” strategy to eliminate legacy data before launch
- See: docs/LEGACY_DATA_CLEANUP.md
- Option A (clean): backfill legacy records (preferred if you want continuity for existing testers).
- Option B (nuke): purge all existing accounts + GraphQL records, then recreate Admin + testers.
- Pros: guarantees no legacy records remain; simplest way to ensure required fields are present everywhere.
- Cons: deletes all historical testing data; requires careful coordination and confirming you’re in the right environment.
- If choosing Option B:
- Document exact steps (Cognito user deletion + DynamoDB/AppSync model table purge) and verify they’re run against the correct Amplify env.
- After reset: disable safe-fallbacks and keep
isDemoselected everywhere.
-
TODO(postmvp) Hardening: align minimal GraphQL selection sets with TypeScript types
- Decision (Feb 2026): defer. Current MVP flow maps GraphQL → UI types via
toTaskUI/toListUIand does not rely on relation payloads. - Current state:
src/api/operationsMinimal.tsuses minimal selection sets but is typed with full Amplify-generated operation output types. - Risk: TypeScript can become misleading (it may appear that nested relations / extra fields exist when the selection set didn’t request them).
- Revisit when:
- you add features that read nested relation data from mutation results, or
- you adopt normalized caching / entity identity patterns (Apollo/urql-style), or
- you start seeing bugs caused by assumed-but-unselected fields.
- Options:
- Define custom minimal output types for the minimal ops (preferred for correctness), or
- Expand minimal selection sets to match the generated types you’re using.
- Decision (Feb 2026): defer. Current MVP flow maps GraphQL → UI types via
-
TODO(postmvp) Consider adding
__typenameto minimal queries/mutations- Decision (Feb 2026): defer. Zustand indexing/memoization currently keys off explicit ids and doesn’t need
__typename. - Notes:
- Generated codegen ops already include
__typename; the minimal ops intentionally omit it. - Add
__typenamelater if you want stronger runtime discrimination (interfaces/unions) or to support normalized cache keys.
- Generated codegen ops already include
- Decision (Feb 2026): defer. Zustand indexing/memoization currently keys off explicit ids and doesn’t need
-
TODO(postmvp) Support time-of-day due times (datetime) for tasks
- Today: the UI treats
dueAtas a “floating day” (date-only) value and formats/compares by day key to avoid timezone drift. - Upgrade path:
- Add UX for optional time (e.g.
datetime-local) and/or a separatedueTimefield - Decide storage semantics (instant vs floating local time) and migration strategy
- Update triage/views bucketing + formatting helpers to respect time-of-day where enabled
- Add UX for optional time (e.g.
- Today: the UI treats
-
TODO(postmvp) Task move: allow "move under another task" (reparent)
-
Ideal UX (A): drag/drop to change nesting + order
- Best surface: ListDetailsPage + TaskDetailsPane stack (where the tree/context is visible)
- Support: drop onto a task to make it a child, and drop between siblings to reorder
-
Fallback UX (B): manual reparent in the edit flow (ship this first)
- Surface: add a "Parent" section to Edit Task (used by TasksPage edit dialog + TaskDetailsPane edit inline)
- Step 1: choose target list
- Use existing list selector UX (FormSelect)
- Important: allow reparenting even when the user keeps the same list (don’t hide the parent controls)
- Step 2: choose parent path (nested picker, but not confusing)
- Default parent = "No parent (top-level)"
- Recommended UI: a small, progressive "parent path" chooser
- Level 1: "Parent (top-level)" select → shows top-level tasks in the chosen list
- If a parent is selected and it has children, reveal Level 2: "…under" select → shows that parent’s subtasks
- Repeat until the selected parent has no children, or the user stops (leaves next level blank)
- Show a 1-line preview so users understand the result:
- "Will move under: A ▸ B ▸ C" (or "Top-level")
- UX guardrails:
- Exclude the current task from all options
- Disable (or hide) any descendant tasks as valid parents (cycle prevention)
- Provide a quick "Clear parent" action to jump back to top-level
- Optional (nice): "Move under another task" checkbox/toggle
- Off: parent controls collapsed (keeps edit form compact)
- On: reveals the parent path chooser (default still top-level until user picks)
- Save behavior (minimum viable)
- Persist:
listId,parentTaskId(nullable) - Compute
sortOrderat destination as end-of-siblings:- siblings = tasks with same
listIdAND sameparentTaskId sortOrder = max(siblings.sortOrder) + 1
- siblings = tasks with same
- If list changes, allow optionally choosing a parent in the destination list; otherwise force
parentTaskId=null
- Persist:
- Validation + error messaging
- If selected parent is invalid (self/descendant/missing), block save + show a toast
- If API rejects the update, keep the draft selections so the user can fix and retry
- URL/pane-stack behavior
- If the task is currently open in the pane stack and gets moved:
- If it remains in the same list: keep the stack open (safe)
- If it moves lists: navigate to the new stack root for that task to avoid "Task not found"
- If the task is currently open in the pane stack and gets moved:
-
Staged rollout plan (do B first, then A)
- Stage 1 (fastest): manual reparent within the same list only
- Enable selecting parent path for tasks in ListDetailsPage/TaskDetailsPane edit
- Save updates
parentTaskId+sortOrderonly
- Stage 2: manual reparent across lists
- Allow list change + parent selection in destination list
- Ensure move clears/updates parent consistently and handles pane-stack navigation
- Stage 3: ordering strategy upgrade (enables future drag/drop reorder)
- Introduce sparse sortOrder spacing or occasional sibling renumbering
- (Optional) batch update API/resolver support for reordering without N sequential mutations
- Stage 4: drag/drop UI (A) on the stack view
- Start with reparent-only (drop onto task changes parent, keep end-of-siblings ordering)
- Then add between-sibling reorder once sortOrder strategy is proven stable
- Stage 1 (fastest): manual reparent within the same list only
-
Save payload should support:
listId,parentTaskId(nullable), and a reasonablesortOrderamong new siblings -
Guardrails:
- Prevent cycles: disallow selecting self or any descendant as parent
- Ensure the selected parent is in the same target list (or auto-fix/clear parent if list changes)
- Decide: allow parenting under subtasks (deep nesting) vs only under top-level tasks
- If a task is currently open in the pane stack, decide how URL + selection should respond after reparent
-
-
TODO(postmvp) Task move: improve ordering when moving/reparenting
- Today: moving lists resets
sortOrderto0(good enough for MVP, but crude) - Better: compute next
sortOrderfor end-of-list among siblings (same list + same parentTaskId) - Drag/drop note: true “reorder within siblings” requires either (a) sparse sortOrder spacing, or (b) occasional renumbering of many siblings (batch updates)
- Optional UX polish: "Move to top" vs "Move to bottom" vs "Keep relative order"
- Today: moving lists resets
-
TODO(P3) Architecture guardrail: forbid direct imports fromsrc/api/**insidesrc/pages/**andsrc/components/**- Enforced in
eslint.config.jsviano-restricted-importsforsrc/pages/**,src/components/**, andsrc/hooks/**. - Keeps UI-layer data access going through store/hooks.
- Enforced in
-
TODO(P1) Create a disclosure/notification that "the app uses cookies/local storage" and that closes and persists acknowledgement so it does not reopen again later.
-
TODO(P2) Add date formatting helper for task due dates- Implemented in
src/services/dateTime.tsasformatDueDate. - Used by
TaskRowandTaskDetailsPaneso we no longer print raw ISO strings.
- Implemented in
-
TODO(P2) Update ProfilePage to use real auth/user data (Cognito / Amplify)- Uses
useUserUIvia the profile page data hook.
- Uses
-
TODO(P2) Ensure user metadata always updates on account switch- Implemented via auth lifecycle hooks + Hub listeners (
useAuthUser+useUserUI) and cache guards. - Validation still belongs in the manual QA checklist item (fast account switches without reload).
- Implemented via auth lifecycle hooks + Hub listeners (
-
TODO(P3) Add an app footer- Link to the showcase site
- Move the Sign Out button into the footer
- Add an email link:
mailto:nick@nickhanson.me
-
TODO(P1) Find a way to ensure refreshing of favorites sidebar section upon starring/un-starring lists for favorites. -
TODO(P1) Fix failing toasts from editing and adding tasks and closing/canceling both windows on ListDetailsPage.- Centralized a single global
Toastermount in the app shell (removed per-page mounts). - Normalized toast calls:
fireToast(...)is synchronous and no longer awaited.
- Centralized a single global
-
TODO(P1) Add Tooltip/Toast for "already in inbox"
-
TODO(P1) AddUserProfileGraphQL model (owned by sub)- Implemented in amplify/backend/api/taskmaster/schema.graphql
- Key fields:
id (sub),owner (sub),email,seedVersion,seededAt,settingsVersion/settings,onboardingVersion/onboarding
-
TODO(P1) Implement bootstrap: fetch/createUserProfileon login- Centralized in the app shell; runs once per authenticated session.
- Version gate: if missing or
seedVersion < CURRENT_SEED_VERSION, run the seed flow.
-
TODO(P1) Implement seed flow (idempotent + race-safe)- Seed creates example lists + tasks (including subtasks) and marks them
isDemo: true. - Multi-tab safety uses conditional updates on
UserProfile.seedVersionwith an in-progress lock value (-1). - Finalizes by setting
seedVersion = CURRENT_SEED_VERSIONandseededAt = now.
- Seed creates example lists + tasks (including subtasks) and marks them
-
TODO(P1) Add “Demo seed” UX (minimal)- MVP behavior: always seed for all accounts by default.
- Temporary opt-out supported (e.g.
?demo=0or per-userlocalStorage.taskmaster:u:<scope>:seedDemo = "0").
-
TODO(P2) Demo onboarding polish (confirmation + quick tour)- Home/Login: show a confirmation + explanation step before creating a demo user + signing in.
- Demo sessions: show a one-time “Demo quick tour” checklist (dismissed via user-scoped key
demoTourSeen:v1).
-
TODO(P1) Account-switch cleanup for user-scoped caches + bootstrap- On sign out: clear
taskStorepersisted cache + user UI cache (and other user-scoped caches) - On sign in: bootstrap runs after auth restore and cache guards prevent cross-user flashes
- On sign out: clear
-
TODO(P1) Add a Demo data section within SettingsPage that contains these features:- A button to clear all demo data.
- A button to re-seed all demo data ('reset demo data').
- Note: Demo users already have a footer "Reset demo data" action. This Settings version is for non-demo accounts and must NOT remove non-demo data (implement later with stricter filtering/guardrails).
- A section for adding more demo data with:
- A button to add more tasks with multiplier. "Add [ x ] tasks."
- A button to add more lists with multiplier. "Add [ x ] lists."
- A button to add both with separate multipliers. "Add [ x ] tasks and [ x ] lists."."
-
TODO(P1) Add user-configurable default post-login landing route- Implemented (local-only): Settings selector persists per-user in
localSettingsStore. - Behavior:
- If
?redirect=is present (fromRequireAuth), respect it (after sanitization) - Otherwise navigate to the configured landing route, defaulting to
/today
- If
- Implemented (local-only): Settings selector persists per-user in
-
TODO(P1) Zustand state (MVP shipped)- tasks + taskLists (GraphQL-backed, cached client-side)
- updates feed (persisted event feed)
- updates read-state (lastReadAt / clearedBeforeAt)
- inbox UX state (dismissals, lastViewedAt, dueSoonWindowDays)
-
TODO(P1) Migrate page reads/writes → store hooks/actions- InboxPage
- ListDetailsPage
- UpdatesPage
- TasksPage
-
TODO(P2) Persist client-side state (localStorage)- Implemented:
- tasks/lists cache (versioned + TTL)
- inbox local state (dismissed ids, lastViewedAt, dueSoonWindowDays)
- updates feed + read markers
- userUI cache (username/email/role with TTL)
- Note: persisted keys are now user-scoped (to prevent cross-user mixing on shared browsers) with safe legacy migrations.
- Future: consider moving persistence to IndexedDB for offline mode.
- Implemented:
-
TODO(P3) Scope inbox/updates localStorage keys by user sub (optional polish)- Implemented via
taskmaster:u:<scope>:...key prefixing (seetaskmaster:authScope) - Inbox/Updates/TaskStore/UserUI/LocalSettings are stored under per-user
zustand:keys - Legacy unscoped keys (
taskmaster:inbox,taskmaster:updates, etc.) are migrated only when a signed-in scope is known
- Implemented via
-
TODO(P3) Add a tiny “dev reset local state” helper- Implemented via DevPage: "Clear all user caches".
- Clears current user's persisted caches (user-scoped) and resets in-memory session state:
taskmaster:u:<scope>:zustand:taskmaster:taskStoretaskmaster:u:<scope>:zustand:taskmaster:inboxtaskmaster:u:<scope>:zustand:taskmaster:updates
- Notes:
<scope>is the value oftaskmaster:authScope- Legacy unscoped keys are handled separately and should not be relied on going forward
-
TODO(stretch) Improve navigation for system Inbox tasks Prefer navigation to InboxPage for task in __Inbox instead of disabled navigation. -
TODO(P1) Improve navigation for system Inbox list- system Inbox is treated as special (hidden from normal list visibility/favorites; edit/favorite/delete disabled when detected).
- Decision (Feb 2026): keep it exclusively behind
/inboxtriage.- The “system Inbox list” is an implementation detail (a staging bucket), not a user-facing List.
- Do not add a dedicated page for it and do not special-case
ListDetailsPageto display it.