import { readFile } from "node:fs/promises" import path from "node:path" import { hasPermission } from "@cms/content/rbac" import Link from "next/link" import { redirect } from "next/navigation" import { resolveRoleFromServerContext } from "@/lib/access-server" export const dynamic = "force-dynamic" type TodoState = "done" | "partial" | "planned" type TodoPriority = "P1" | "P2" | "P3" | "NONE" type StateFilter = "all" | TodoState type PriorityFilter = "all" | Exclude type MetaFilter = "show" | "hide" type TodoTask = { state: TodoState text: string priority: TodoPriority } type TodoBlock = { title: string tasks: TodoTask[] notes: string[] } type TodoSection = { title: string blocks: TodoBlock[] notes: string[] } type ProgressCounts = { done: number partial: number planned: number } type SearchParamsInput = Record const metaSections = new Set(["Status Legend", "Priority Legend", "How We Use This File"]) const stateOrder: Record = { partial: 1, planned: 2, done: 3, } const priorityOrder: Record = { P1: 1, P2: 2, P3: 3, NONE: 4, } function isMetaSection(section: TodoSection | null): boolean { return section ? metaSections.has(section.title) : false } function toTaskState(token: string): TodoState { const value = token.toLowerCase() if (value === "x") { return "done" } if (value === "~") { return "partial" } return "planned" } function parsePriority(rawText: string): { priority: TodoPriority; text: string } { const match = rawText.match(/^\[(P[1-3])\]\s+(.+)$/i) if (!match) { return { priority: "NONE", text: rawText.trim(), } } return { priority: match[1].toUpperCase() as Exclude, text: match[2].trim(), } } function parseTodo(content: string): TodoSection[] { const sections: TodoSection[] = [] let currentSection: TodoSection | null = null let currentBlock: TodoBlock | null = null const ensureSection = (title = "General"): TodoSection => { if (!currentSection) { currentSection = { title, blocks: [], notes: [], } sections.push(currentSection) } return currentSection } const ensureBlock = (title = "Overview"): TodoBlock => { const section = ensureSection() if (!currentBlock) { currentBlock = { title, tasks: [], notes: [], } section.blocks.push(currentBlock) } return currentBlock } for (const rawLine of content.split("\n")) { const line = rawLine.trim() if (!line) { continue } if (line.startsWith("# ")) { continue } if (line.startsWith("## ")) { currentSection = { title: line.replace("## ", "").trim(), blocks: [], notes: [], } sections.push(currentSection) currentBlock = null continue } if (line.startsWith("### ")) { const section = ensureSection() currentBlock = { title: line.replace("### ", "").trim(), tasks: [], notes: [], } section.blocks.push(currentBlock) continue } const taskMatch = line.match(/^- \[([ x~X])\] (.+)$/) if (taskMatch) { if (isMetaSection(currentSection)) { ensureSection().notes.push(taskMatch[0].replace("- ", "")) continue } const task = parsePriority(taskMatch[2].trim()) ensureBlock().tasks.push({ state: toTaskState(taskMatch[1]), text: task.text, priority: task.priority, }) continue } const bulletMatch = line.match(/^- (.+)$/) if (bulletMatch) { if (currentBlock) { currentBlock.notes.push(bulletMatch[1].trim()) } else { ensureSection().notes.push(bulletMatch[1].trim()) } } } return sections } function getProgressCounts(sections: TodoSection[]): ProgressCounts { const counts: ProgressCounts = { done: 0, partial: 0, planned: 0, } for (const section of sections) { if (isMetaSection(section)) { continue } for (const block of section.blocks) { for (const task of block.tasks) { counts[task.state] += 1 } } } return counts } function getProgressPercent(progress: ProgressCounts): number { const total = progress.done + progress.partial + progress.planned if (total === 0) { return 0 } return Math.round(((progress.done + progress.partial * 0.5) / total) * 100) } async function getTodoMarkdown(): Promise { const candidates = [ path.resolve(process.cwd(), "TODO.md"), path.resolve(process.cwd(), "../TODO.md"), path.resolve(process.cwd(), "../../TODO.md"), ] for (const filePath of candidates) { try { return await readFile(filePath, "utf8") } catch { // Try next candidate path. } } return "# TODO file not found\n\nCreate `/TODO.md` in the repository root to track progress." } function statusBadgeClass(state: TodoState): string { if (state === "done") { return "bg-emerald-50 text-emerald-700 border-emerald-200" } if (state === "partial") { return "bg-amber-50 text-amber-700 border-amber-200" } return "bg-slate-50 text-slate-700 border-slate-200" } function statusLabel(state: TodoState): string { if (state === "done") { return "Done" } if (state === "partial") { return "Partial" } return "Planned" } function priorityBadgeClass(priority: TodoPriority): string { if (priority === "P1") { return "bg-rose-50 text-rose-700 border-rose-200" } if (priority === "P2") { return "bg-indigo-50 text-indigo-700 border-indigo-200" } if (priority === "P3") { return "bg-teal-50 text-teal-700 border-teal-200" } return "bg-neutral-100 text-neutral-700 border-neutral-200" } function normalizeStateFilter(value: string | undefined): StateFilter { if (value === "done" || value === "partial" || value === "planned") { return value } return "all" } function normalizePriorityFilter(value: string | undefined): PriorityFilter { if (value === "P1" || value === "P2" || value === "P3") { return value } return "all" } function normalizeMetaFilter(value: string | undefined): MetaFilter { if (value === "show") { return "show" } return "hide" } function toSingleQueryValue(value: string | string[] | undefined): string | undefined { if (Array.isArray(value)) { return value[0] } return value } function sortTasks(tasks: TodoTask[]): TodoTask[] { return [...tasks].sort((left, right) => { const priorityDelta = priorityOrder[left.priority] - priorityOrder[right.priority] if (priorityDelta !== 0) { return priorityDelta } const stateDelta = stateOrder[left.state] - stateOrder[right.state] if (stateDelta !== 0) { return stateDelta } return left.text.localeCompare(right.text) }) } function applyFilters( sections: TodoSection[], stateFilter: StateFilter, priorityFilter: PriorityFilter, metaFilter: MetaFilter, ): TodoSection[] { return sections .filter((section) => metaFilter === "show" || !isMetaSection(section)) .map((section) => { const filteredBlocks = section.blocks .map((block) => { const filteredTasks = sortTasks( block.tasks.filter((task) => { const stateMatches = stateFilter === "all" || task.state === stateFilter const priorityMatches = priorityFilter === "all" || task.priority === priorityFilter return stateMatches && priorityMatches }), ) return { ...block, tasks: filteredTasks, } }) .filter((block) => block.tasks.length > 0 || block.notes.length > 0) return { ...section, blocks: filteredBlocks, } }) .filter((section) => section.blocks.length > 0 || section.notes.length > 0) } function buildFilterHref( current: { state: StateFilter; priority: PriorityFilter; meta: MetaFilter }, overrides: Partial<{ state: StateFilter; priority: PriorityFilter; meta: MetaFilter }>, ): string { const next = { ...current, ...overrides, } const params = new URLSearchParams() if (next.state !== "all") { params.set("state", next.state) } if (next.priority !== "all") { params.set("priority", next.priority) } if (next.meta !== "hide") { params.set("meta", next.meta) } const query = params.toString() if (!query) { return "/todo" } return `/todo?${query}` } function filterButtonClass(active: boolean): string { return active ? "bg-neutral-900 text-white border-neutral-900" : "bg-white text-neutral-700 border-neutral-300 hover:bg-neutral-100" } export default async function AdminTodoPage(props: { searchParams?: SearchParamsInput | Promise }) { const role = await resolveRoleFromServerContext() if (!role) { redirect("/login?next=/todo") } if (!hasPermission(role, "roadmap:read", "global")) { redirect("/unauthorized?required=roadmap:read&scope=global") } const content = await getTodoMarkdown() const sections = parseTodo(content) const progress = getProgressCounts(sections) const completionPercent = getProgressPercent(progress) const rawSearchParams = props.searchParams ? await props.searchParams : {} const stateFilter = normalizeStateFilter(toSingleQueryValue(rawSearchParams.state)) const priorityFilter = normalizePriorityFilter(toSingleQueryValue(rawSearchParams.priority)) const metaFilter = normalizeMetaFilter(toSingleQueryValue(rawSearchParams.meta)) const filteredSections = applyFilters(sections, stateFilter, priorityFilter, metaFilter) const currentFilters = { state: stateFilter, priority: priorityFilter, meta: metaFilter, } return (

Admin App

Roadmap and Progress

Structured view from root `TODO.md` (single source of truth).

Back to dashboard

Weighted completion

{completionPercent}%

Completed items

{progress.done}

Partially done items

{progress.partial}

Planned items

{progress.planned}

Status

{(["all", "planned", "partial", "done"] as const).map((value) => ( {value === "all" ? "All" : statusLabel(value)} ))}

Priority

{(["all", "P1", "P2", "P3"] as const).map((value) => ( {value} ))} {metaFilter === "show" ? "Hide Meta Sections" : "Show Meta Sections"}
{filteredSections.length === 0 ? (
No items match the current filter.
) : ( filteredSections.map((section) => (

{section.title}

{section.notes.length > 0 ? (
    {section.notes.map((note) => (
  • {note}
  • ))}
) : null}
{section.blocks.map((block) => (

{block.title}

    {block.tasks.map((task) => (
  • {task.priority !== "NONE" ? ( {task.priority} ) : null}

    {task.text}

    {statusLabel(task.state)}
  • ))}
{block.notes.length > 0 ? (
    {block.notes.map((note) => (
  • {note}
  • ))}
) : null}
))}
)) )}
View raw markdown source (`TODO.md`)
          {content}
        
) }