Every line of a hand-built Next.js Kanban planner, read and judged against 2026 standards by a fleet of 93 AI review agents. The good, the bad, and the Dispatch<any>.
z-planner is a Kanban-style task planner — boards, columns, drag-and-drop cards, subtasks, color-coded categories, an archive, search and category filters — built entirely by hand by one developer between September 2023 and July 2024.
You sign in with Clerk, land on a board, and drag cards between columns like Trello. Cards open into a dialog with notes and a Notion-style subtask editor (Enter inserts, Backspace-on-empty deletes, arrow keys move focus). Completing a card moves it to the bottom of its column; the next visit archives it. A settings page manages boards, columns, and categories, each guarded by confirmation dialogs. Under the hood it is a single-page app: one initial fetch pulls the user's entire planner into a normalized client-side store, and every interaction updates the UI optimistically before persisting through a REST API to a single MongoDB document per user.
The codebase contains a (currently disabled) news banner that reads: “Do not expect anything to work or your data to be saved.” As you are about to see, this is the most accurate line of documentation in the repository — and also far too modest, because most things do work, smoothly, in the happy path. This dossier is largely the story of the unhappy paths.
Architecturally, z-planner is a classic 2023-era client-side SPA wearing an App Router folder structure. The root layout is the only Server Component in the entire route tree — everything else, both layouts and all five pages, is 'use client'.
The entire planner lives in one MongoDB document per user: five Map fields of flat entities, plus ID arrays for ordering. It's a Redux normalized store, persisted. No joins, no populate(), no cross-collection transactions — and dragging a card across two columns is one atomic $set. The client store and the database have the same shape, so the GET endpoint is effectively a store-hydration call.
The render path from the page to a single subtask input crosses eleven components. Component decomposition is genuinely disciplined — each file does one thing — but every level subscribes to the same global context, so they all re-render together.
| Layer | Shape |
|---|---|
| Pages | 5 pages, 2 layouts, 0 loading.tsx / error.tsx / not-found.tsx. 6 of 7 app-route files are 'use client'. |
| API | 19 route files, 20 handlers (1 GET, 5 POST, 9 PATCH, 5 DELETE), 530 lines, all wrapped in the same withMiddleware. |
| State | 2 reducer stores (planner + filters), 32 + 3 actions, 5 normalized entity maps, ~37 consuming components. |
| Mutations | 23 near-identical util files (940 lines): optimistic dispatch → axios → identical 4-line catch. 3 are debounced at 500ms. |
| Components | 98 files: 55 planner, 8 global, 34 shadcn/Aceternity primitives (12 of which have zero importers), 1 orphaned theme provider. |
| Code split | 8,239 lines total: 2,889 vendored UI boilerplate, 5,350 hand-written. |
Nine review dimensions, scored against modern (2026) Next.js standards — React Server Components, Server Actions, typed boundaries, accessible-by-default UI. Built in 2023–24, judged by 2026 rules: the scores are harsh by construction.
Overall: 3.9 / 10 against 2026 standards — with the repeated caveat from nearly every reviewer: “for a solo 2023–24 build, the instincts are better than average.”
The reviewers logged 64 genuine strengths. These are the ones that show real engineering judgment — decisions many production codebases get wrong.
One MongoDB document per user makes the hottest multi-entity write — dragging a card across columns — atomic without transactions, and makes cross-tenant IDOR structurally impossible: every query is scoped to { clerkUserId }, so possessing someone else's card ID gets you nothing.
Five ID-keyed entity maps plus order arrays — textbook Redux normalization, hand-rolled with useReducer + immer. No entity duplication, O(1) lookups, reordering is an array swap. The server returns data already in store shape.
Drag mutations send the complete final ID array, not from/to deltas. The server $sets it wholesale — replaying a retried request converges to the same state. This is the right consistency model, chosen apparently by instinct.
Separate PlannerContext and PlannerDispatchContext — the canonical optimization that lets dispatch-only consumers skip re-renders. Done in both stores. (Undone by the lack of memoization elsewhere, but the idiom is correct.)
Progress bars, filtered lists, and category counts are computed in render from canonical state, never duplicated into useState. Stable entity IDs are used as keys in every drag-critical list. Error boundary actually wired, with imperative showBoundary on fetch failure.
dbConnect caches the connection promise on global with bufferCommands: false and promise-reset on failure — the officially recommended pattern, implemented correctly.
withMiddleware = withAuth(withDbConnect(handler)) applied to all 19 routes with total consistency. Identity comes exclusively from server-side Clerk auth() — never from the request body. No route can forget to authenticate.
Exactly one PATCH per drag. Text edits debounced at 500ms. Lodash imported modularly. framer-motion confined to the landing page — the board itself runs zero animation libraries. Fonts self-hosted via next/font.
Every destructive action gates behind an AlertDialog with explanatory copy. Disabled delete buttons get tooltips explaining why ("A board with columns cannot be deleted."). New users get a real welcome flow, not an empty screen.
Enter inserts a subtask below, Backspace-on-empty deletes, ArrowUp/Down move focus — a Notion-style keyboard editing flow most hobby Kanban apps never attempt.
"strict": true with almost no @ts-ignore, ~5 narrow casts in the whole app, string-literal unions instead of enums, and textbook zod + react-hook-form schema-first typing in all 8 forms.
All five domains (board/card/category/column/subTask) follow identical addNew*/change*/delete* naming. Path aliases everywhere, zero TODO/FIXME debt, and comments that record reasoning — why drag state lives where it does — not restatements of code.
138 issues survived adversarial verification (every major finding was independently re-checked against the code by a second agent — none were refuted). Six are critical. The pattern across all of them: the happy path was built with care; the failure path was never built at all.
A real MongoDB Atlas username + password was committed in backend/.env and later "deleted" — but deleting a file does not remove it from history, and the repo is public. Resolved same-day: the credential was tested and found dead (the Atlas cluster it pointed at no longer exists), and the history has since been scrubbed with git filter-repo and force-pushed.
NextResponse.json({ status: 401 }) puts the status code in the response body; the real HTTP status stays 200. Universal across all 19 routes. Axios only rejects on non-2xx, so the client's error handling is unreachable for auth failures and server errors.
All 23 mutation utils catch errors by setting backendErrorOccurred — a flag no component ever reads. No rollback, no retry, no error toast; meanwhile success toasts fire before the request. A failed write shows a green checkmark and loses your data on next reload.
Title/content edits share a single module-level lodash debounce. Edit card A, then click into card B within 500ms: A's pending PATCH is cancelled — replaced by B's. A's title silently never persists. Three files carry the same copy-pasted bug.
Opening a task card cannot be done with a keyboard: the DialogTrigger sits on a plain div, and the only focusable element lifts the card for dragging instead. Adding a card is an onClick on an SVG. There is not a single aria-label in the hand-written codebase.
withDbConnect does return handler(...) inside a try — without await, the promise's rejection escapes the catch. Malformed JSON, Mongoose errors, and the empty-body TypeError in PATCH all bypass the only error handler the API has.
getToken() before each send. Drag 1's request can arrive after drag 2's and resurrect stale order.moveCardAcrossColumns.ts:32{ status }. The order exists exclusively in client memory — guaranteed divergence on reload.plannerReducer.tsx:108 · changeCardCheckedStatus.tsObject.entries(body)[0] silently drops every field after the first, throws a TypeError on {} (unhandled, see the missing await), and accepts arbitrary field names into the update path.cards/[cardId]/route.ts:18scheduledTaskCards state · no undo for permanent destructive actions.await req.json() raw; whole client objects are persisted verbatim; TypeScript request interfaces are compile-time fiction with [key: string]: any escape hatches.all 19 routes$set path with no allowlist. Scoped to the user's own document (so not cross-tenant), but a dotted key like "a.b" writes unmodeled nested state the app never expects.cards/[cardId]/route.ts:18 · subtasks/[subTaskId]/route.ts$set/$unset/$pull update would be atomic for free. A failure between steps leaves dangling references.boards/[boardId]/categories/[categoryId]/route.ts:21NEXT_PUBLIC_MONGO_URI — a prefix that tells Next.js to inline the value into the client bundle. One import away from publishing the database credential.config/mongo-client.ts:3updateMany used on per-user singletons · schema lacks timestamps, enums, and any optimistic-concurrency story · response bodies carry no server echo to reconcile against."next": "latest" is a build time bombSeven dependencies float on "latest" while the lockfile freezes mid-2024 (Next 14.2.4, React 18.2, Tailwind 3.3). Any lockless install in 2026 jumps multiple majors simultaneously: async params, React 18/19 peer conflicts, and the Tailwind v4 PostCSS break — all at once.package.json:47'use client'; the 13-line root layout is the only Server Component. No RSC data fetching, no Server Actions, no streaming — the entire dataset arrives via client-side axios after hydration. This is the exact pattern RSC was designed to replace.app/** · hooks/Planner/Planner.tsx:40auth(). Boilerplate ceremony on every call site for zero benefit.utils/plannerUtils/**export const metadata anywhere. Browser tabs show the bare URL; link previews are empty. This has been the App Router standard since Next 13.2.app/layout.tsx:6hasLoaded flags; errors use a third-party boundary inside a client layout; unknown boards router.push('/not-found') — during render, to a route that doesn't exist.app/boards/[boardId]/page.tsx:16reactStrictMode: false · CalSans font instantiated twice in different modules · client-side useEffect redirects where server redirect() belongs · dead next-themes setup with suppressHydrationWarning stranded on body · JSX.Element typings that break under React 19 · middleware matcher excludes any route containing a dot · tsconfig targets ES5.idOfCardBeingDragged globally — re-rendering the entire board at the two most latency-sensitive moments of a drag, the exact anti-pattern @hello-pangea/dnd's performance docs warn about. The library's own snapshot.isDragging was available the whole time.components/planner/utils.ts:74 · TaskCard.tsx:60selectedBoard is seeded once from context and never reconciled; delete that board in the card above and boards[selectedBoard].columns dereferences undefined.ManageColumnsCard.tsx:13auth(), never the request. Uniform tenant scoping makes classic IDOR structurally impossible — a traced attack with another user's cardId modifies nothing. Zero XSS sinks: no dangerouslySetInnerHTML, no innerHTML, no eval.Dispatch<any> — 31 times32 reducer action types, zero compile-time payload checking, no exhaustiveness. The compiler verifies nothing about the app's central data flow, and at least one missing-payload bug already slipped through.types.tsx:96 · every mutation utilany. A real null-dereference in the category-delete route hides behind it.models/Planner.ts:1response.data hydrates the entire store untyped and unvalidated; axios.get<T> appears zero times. The three layers where TypeScript would earn its keep — dispatch, database, wire — are all any.hooks/Planner/Planner.tsx:50any where the real type is one import awaysource: any, destination: any for dnd's DraggableLocation — the same package whose types are already imported one file over.moveCardAcrossColumns.ts:8getToken() in useEffect → GET /api/planner. Nothing renders until all of it lands — and the GET performs a full-map database write on every read.hooks/Planner/Planner.tsx:40getToken() awaited per keystroke outside the debounce · search dispatches per keystroke into unmemoized per-column filtering · permanent full-viewport blurred-gradient animation on the landing page · stack frozen in mid-2024, forfeiting two years of framework performance work.'error occured lol' · commented-out code across nine files · CategoryBadge duplicated then diverged · mixed default/named export conventions · stale pages/api comments in 7 of 19 App Router route files.visibility: hidden except on mouse hover, which removes it from the tab order entirely.EditableSubTask.tsx:40id='terms' checkboxes from pasted shadcn docs.ModifyBoardDialogContent.tsx:49 · CategoryColorPicker.tsx:18document.body.style.pointerEvents on a 100ms timer after close — with comments admitting the page would otherwise become permanently unclickable after the nested Dialog → AlertDialog flow.ManageItemCardDialogWrapper.tsx:30<button onClick={router.push}> — no middle-click, no link semantics, no prefetch, no aria-current. Focus rings are also deliberately stripped from every editing input, so keyboard users navigate blind.SidebarButton.tsx:19 · TaskFilterSearchBar.tsx:9z-planner is a well-organized 2023-era SPA wearing an App Router costume, built by someone whose instincts are consistently better than their follow-through. The high-level decisions are genuinely good — the single-document data model that makes tenancy watertight and the hot-path write atomic, the normalized Redux-style store, idempotent full-array reorders, optimistic UI on all 23 mutations, a uniformly applied auth wrapper. These are choices many professional teams get wrong.
But the codebase optimizes relentlessly for the happy path. Every error path either doesn't exist (backendErrorOccurred: written 23 times, read zero times), can't fire (HTTP 200 for every failure; an un-awaited handler escaping its try/catch), or actively destroys data (the shared debounce; the destructive GET). The consistency that makes the code pleasant to read was achieved by copy-paste, so its one data-loss bug exists in three files. And the gap between installed and used — SWR never imported, zod never on the server, cypress with zero tests, dark mode built but never mounted — tells the story of a project that kept buying tools for the person it wanted to become.
Scored 3.9/10 against 2026 standards; closer to a 6.5 against the standards of its own era. The architecture underneath is worth keeping. The plumbing needs a deliberate week of repair.
In strict priority order. Items 1–3 are a single afternoon and remove the live hazards; items 6–8 are the actual migration to modern Next.js.
Resolved: the leaked credential was tested and found dead (its Atlas cluster no longer exists), and backend/.env was purged from all 117 commits with git filter-repo + force-push the same day as the review. Still to do as part of this item: delete config/mongo-client.ts and the NEXT_PUBLIC_MONGO_URI variable.
Move status codes into ResponseInit (NextResponse.json(body, { status })) and add the missing await in withDbConnect/withAuth. Two tiny diffs that make every error in the system visible for the first time.
Kill all seven "latest" specifiers, move cypress out (or just delete it), declare nanoid, drop the ~7 unused packages. Add unique: true, index: true to clerkUserId and switch create-on-GET to an upsert.
Capture a pre-mutation snapshot and restore it in catch (or adopt SWR/TanStack mutate with rollbackOnError — SWR is already installed). Key the debounce per entity ID. Move success toasts after the response. This closes every silent-data-loss path.
One PlannerAction discriminated union (kills all 31 Dispatch<any>), ESM-import the Mongoose model with InferSchemaType, and one shared zod schema module parsed on both client and server. Roughly doubles real type safety without touching architecture.
Fetch the planner in a Server Component with await auth() + a direct Mongo query (no HTTP hop, no Bearer ceremony), pass it to a client Board island. Replace the 23 mutation files + 19 routes with Server Actions using useOptimistic — which gives rollback for free. Add loading.tsx / error.tsx / not-found.tsx and a metadata export.
Real buttons for card-open and card-add, focus-visible drag handles, restore focus rings, dark-text badges on light hues, links instead of router.push buttons, labels that match their inputs, and mount the dark mode that's been sitting there finished since 2024.
The 204-line reducer and the drag-end handler are pure functions — Vitest tests need no mocks. A GitHub Action running lint + typecheck + test would have caught a third of this dossier automatically.
This dossier was produced by Claude (Fable 5) running a structured multi-agent review on June 10, 2026.
| Phase | What happened |
|---|---|
| Map | 6 agents charted one subsystem each — routing, API surface, state management, client mutations, component tree, data model — producing file-level traces of every read and write path. |
| Review | 9 agents each critiqued one dimension (Next.js standards, React & state, API/backend, security, TypeScript, performance, correctness, code quality, UX/a11y), instructed to find genuine strengths as well as flaws, citing file and line for every claim. |
| Verify | Every major or critical finding went to an adversarial verification agent instructed to refute it by re-reading the code. 77 findings were confirmed, 0 refuted; a handful had severity adjusted. |
| Critic | A completeness agent walked the repo tree hunting for anything the dimensional reviews missed — which is how the phantom nanoid dependency, the ES5 target, and the .gitignore archaeology made it in. |
93 agents · 1,421 tool calls · ~3.2M tokens · 27 minutes wall-clock · repo at commit f07d5e0 (July 14, 2024)