From d7190a56db992a3538622cf2ab7ad6a6f8d62111 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 10 Feb 2026 00:53:30 -0800 Subject: [PATCH] feat: wire roadmap page to persistent data --- data/roadmap.json | 110 +++---- src/app/api/roadmap/[id]/route.ts | 47 ++- src/app/api/roadmap/route.ts | 25 +- src/app/api/roadmap/vote/route.ts | 8 +- src/app/roadmap/[id]/page.tsx | 474 ++++++++---------------------- src/app/roadmap/page.tsx | 318 +++++++------------- src/lib/server/roadmapStore.ts | 79 ++--- 7 files changed, 373 insertions(+), 688 deletions(-) diff --git a/data/roadmap.json b/data/roadmap.json index 28ec9247..4635ab79 100644 --- a/data/roadmap.json +++ b/data/roadmap.json @@ -4,119 +4,87 @@ "title": "Context-Aware Agent Memory", "description": "Persist project-specific memory across sessions with automatic context loading.", "status": "planned", + "quarter": "Q2 2026", "category": "platform", - "voteCount": 84, - "voters": ["kai", "buildbot", "sable"], - "targetDate": "2026-04-15" + "votes": 84, + "updatedAt": "2026-02-01T10:12:00.000Z", + "voters": ["kai", "buildbot", "sable"] }, { "id": "skill-composition-workflows", "title": "Skill Composition Workflows", "description": "Chain multiple skills into reusable workflows with parameter templates.", "status": "planned", + "quarter": "Q2 2026", "category": "tools", - "voteCount": 73, - "voters": ["autoops", "agent_qa"], - "targetDate": "2026-05-01" + "votes": 73, + "updatedAt": "2026-02-02T08:40:00.000Z", + "voters": ["autoops", "agent_qa"] }, { "id": "community-reputation-graph", "title": "Community Reputation Graph", "description": "Visualize trusted contributors and verified maintainer relationships.", - "status": "planned", + "status": "in-progress", + "quarter": "Q1 2026", "category": "community", - "voteCount": 61, - "voters": ["signalfox", "alice-agent"], - "targetDate": "2026-05-20" + "votes": 61, + "updatedAt": "2026-02-06T15:30:00.000Z", + "voters": ["signalfox", "alice-agent"] }, { "id": "enterprise-audit-export", "title": "Enterprise Audit Export", "description": "Scheduled exports for SOC2/ISO compliance including tool-call traces.", - "status": "planned", - "category": "enterprise", - "voteCount": 49, - "voters": ["secbot", "compliance-ai"], - "targetDate": "2026-06-10" - }, - { - "id": "live-roadmap-sync", - "title": "Live Roadmap Sync", - "description": "Realtime update stream for roadmap votes and status transitions.", - "status": "in-progress", - "category": "platform", - "voteCount": 112, - "voters": ["kai", "stream-agent", "ux-helper"], - "targetDate": "2026-03-18" - }, - { - "id": "test-replay-runner", - "title": "Test Replay Runner", - "description": "Replay recorded agent sessions to catch regressions before deploy.", - "status": "in-progress", - "category": "tools", - "voteCount": 98, - "voters": ["ci-bot", "release-bot"], - "targetDate": "2026-03-29" - }, - { - "id": "mentor-matchmaking", - "title": "Mentor Matchmaking", - "description": "Pair new contributors with experienced maintainers by interest tags.", - "status": "in-progress", - "category": "community", - "voteCount": 77, - "voters": ["helper-one", "onboarder", "mentorbot"], - "targetDate": "2026-04-04" - }, - { - "id": "sso-provisioning-bridge", - "title": "SSO Provisioning Bridge", - "description": "Automate enterprise account provisioning from IdP group changes.", "status": "in-progress", + "quarter": "Q1 2026", "category": "enterprise", - "voteCount": 65, - "voters": ["it-admin", "infra-agent"], - "targetDate": "2026-04-12" + "votes": 49, + "updatedAt": "2026-02-05T19:10:00.000Z", + "voters": ["secbot", "compliance-ai"] }, { "id": "agent-profile-directory", "title": "Agent Profile Directory", "description": "Searchable profiles for public agents with capabilities and trust signals.", "status": "completed", + "quarter": "Q1 2026", "category": "platform", - "voteCount": 145, - "voters": ["kai", "reviewer", "atlas"], - "completedDate": "2026-01-22" + "votes": 145, + "updatedAt": "2026-01-22T18:00:00.000Z", + "voters": ["kai", "reviewer", "atlas"] }, { "id": "cli-command-recipes", "title": "CLI Command Recipes", "description": "Copy-ready command sets for common build, deploy, and debugging tasks.", "status": "completed", + "quarter": "Q1 2026", "category": "tools", - "voteCount": 131, - "voters": ["ops-agent", "shellsmith"], - "completedDate": "2026-01-30" + "votes": 131, + "updatedAt": "2026-01-30T12:45:00.000Z", + "voters": ["ops-agent", "shellsmith"] }, { - "id": "monthly-builder-roundup", - "title": "Monthly Builder Roundup", - "description": "Community-curated highlight posts for launches, lessons, and experiments.", - "status": "considering", - "category": "community", - "voteCount": 38, - "voters": ["scribe", "story-agent"], - "targetDate": "2026-06-30" + "id": "live-roadmap-sync", + "title": "Live Roadmap Sync", + "description": "Realtime update stream for roadmap votes and status transitions.", + "status": "shipped", + "quarter": "Q1 2026", + "category": "platform", + "votes": 112, + "updatedAt": "2026-02-08T09:00:00.000Z", + "voters": ["kai", "stream-agent", "ux-helper"] }, { "id": "tenant-level-budget-guards", "title": "Tenant-Level Budget Guards", "description": "Set hard and soft usage limits by team with alerting and auto-throttles.", - "status": "considering", + "status": "shipped", + "quarter": "Q4 2025", "category": "enterprise", - "voteCount": 56, - "voters": ["finops", "guardian"], - "targetDate": "2026-07-15" + "votes": 56, + "updatedAt": "2026-01-14T17:25:00.000Z", + "voters": ["finops", "guardian"] } ] diff --git a/src/app/api/roadmap/[id]/route.ts b/src/app/api/roadmap/[id]/route.ts index eb7aa980..35fef5e6 100644 --- a/src/app/api/roadmap/[id]/route.ts +++ b/src/app/api/roadmap/[id]/route.ts @@ -5,6 +5,26 @@ type VoteRequest = { agentHandle?: unknown; }; +export async function GET( + _request: NextRequest, + context: { params: Promise<{ id: string }> } +) { + try { + const { id } = await context.params; + const items = await readRoadmapItems(); + const item = items.find((entry) => entry.id === id); + + if (!item) { + return NextResponse.json({ error: "Roadmap item not found" }, { status: 404 }); + } + + return NextResponse.json({ item }, { headers: { "Cache-Control": "no-store" } }); + } catch (error) { + console.error("Failed to load roadmap item", error); + return NextResponse.json({ error: "Failed to load roadmap item" }, { status: 500 }); + } +} + export async function POST( request: NextRequest, context: { params: Promise<{ id: string }> } @@ -14,10 +34,6 @@ export async function POST( const body = (await request.json()) as VoteRequest; const agentHandle = typeof body.agentHandle === "string" ? body.agentHandle.trim() : ""; - if (!agentHandle) { - return NextResponse.json({ error: "agentHandle is required" }, { status: 400 }); - } - const items = await readRoadmapItems(); const itemIndex = items.findIndex((item) => item.id === id); @@ -25,21 +41,26 @@ export async function POST( return NextResponse.json({ error: "Roadmap item not found" }, { status: 404 }); } - const normalizedHandle = agentHandle.toLowerCase(); const item = items[itemIndex]; - const hasVoted = item.voters.some((voter) => voter.toLowerCase() === normalizedHandle); + const voters = item.voters ?? []; + + if (agentHandle) { + const normalizedHandle = agentHandle.toLowerCase(); + const hasVoted = voters.some((voter) => voter.toLowerCase() === normalizedHandle); - if (hasVoted) { - return NextResponse.json( - { error: "You have already voted for this feature", item }, - { status: 409 } - ); + if (hasVoted) { + return NextResponse.json( + { error: "You have already voted for this feature", item }, + { status: 409 } + ); + } } const updatedItem = { ...item, - voteCount: item.voteCount + 1, - voters: [...item.voters, agentHandle], + votes: item.votes + 1, + voters: agentHandle ? [...voters, agentHandle] : voters, + updatedAt: new Date().toISOString(), }; items[itemIndex] = updatedItem; diff --git a/src/app/api/roadmap/route.ts b/src/app/api/roadmap/route.ts index 3134ce11..c25d364d 100644 --- a/src/app/api/roadmap/route.ts +++ b/src/app/api/roadmap/route.ts @@ -15,6 +15,7 @@ type CreateRoadmapRequest = { title?: unknown; description?: unknown; category?: unknown; + quarter?: unknown; }; function asTrimmedString(value: unknown): string { @@ -24,14 +25,16 @@ function asTrimmedString(value: unknown): string { export async function GET(request: NextRequest) { try { const statusParam = request.nextUrl.searchParams.get("status"); + const categoryParam = request.nextUrl.searchParams.get("category"); const search = request.nextUrl.searchParams.get("search") ?? undefined; let status: RoadmapStatus | undefined; + let category: RoadmapCategory | undefined; if (statusParam) { if (!isRoadmapStatus(statusParam)) { return NextResponse.json( - { error: "Invalid status. Use planned, in-progress, completed, or considering." }, + { error: "Invalid status. Use planned, in-progress, completed, or shipped." }, { status: 400 } ); } @@ -39,9 +42,21 @@ export async function GET(request: NextRequest) { status = statusParam; } + if (categoryParam) { + if (!isRoadmapCategory(categoryParam)) { + return NextResponse.json( + { error: "Invalid category. Use platform, tools, community, or enterprise." }, + { status: 400 } + ); + } + + category = categoryParam; + } + const items = await readRoadmapItems(); const filteredItems = filterRoadmapItems(items, { status, + category, search, }); @@ -62,6 +77,7 @@ export async function POST(request: NextRequest) { const title = asTrimmedString(body.title); const description = asTrimmedString(body.description); const category = asTrimmedString(body.category).toLowerCase(); + const quarter = asTrimmedString(body.quarter) || "Backlog"; if (!title || !description || !category) { return NextResponse.json( @@ -83,10 +99,11 @@ export async function POST(request: NextRequest) { title, description, category: category as RoadmapCategory, - status: "considering", - voteCount: 0, + status: "planned", + quarter, + votes: 0, + updatedAt: new Date().toISOString(), voters: [], - createdAt: new Date().toISOString(), }; items.unshift(newItem); diff --git a/src/app/api/roadmap/vote/route.ts b/src/app/api/roadmap/vote/route.ts index 6394eee9..28863d54 100644 --- a/src/app/api/roadmap/vote/route.ts +++ b/src/app/api/roadmap/vote/route.ts @@ -44,7 +44,8 @@ export async function POST(request: NextRequest) { const normalizedHandle = agentHandle.toLowerCase(); const item = items[itemIndex]; - const hasVoted = item.voters.some((voter) => voter.toLowerCase() === normalizedHandle); + const voters = item.voters ?? []; + const hasVoted = voters.some((voter) => voter.toLowerCase() === normalizedHandle); if (hasVoted) { return NextResponse.json( @@ -55,8 +56,9 @@ export async function POST(request: NextRequest) { const updatedItem = { ...item, - voteCount: item.voteCount + 1, - voters: [...item.voters, agentHandle], + votes: item.votes + 1, + voters: [...voters, agentHandle], + updatedAt: new Date().toISOString(), }; items[itemIndex] = updatedItem; diff --git a/src/app/roadmap/[id]/page.tsx b/src/app/roadmap/[id]/page.tsx index cd800fcd..056b3df3 100644 --- a/src/app/roadmap/[id]/page.tsx +++ b/src/app/roadmap/[id]/page.tsx @@ -1,51 +1,53 @@ +/* eslint-disable react/no-unescaped-entities */ "use client"; -import { use, useState } from "react"; import Link from "next/link"; -import { notFound } from "next/navigation"; +import { use, useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import roadmapData from "@/data/roadmap.json"; -type RoadmapStatus = "Planned" | "In Progress" | "Shipped" | "Considering"; -type RoadmapCategory = "Platform" | "Skills" | "Community" | "API" | "Enterprise"; +type RoadmapStatus = "planned" | "in-progress" | "completed" | "shipped"; +type RoadmapCategory = "platform" | "tools" | "community" | "enterprise"; type RoadmapItem = { id: string; title: string; description: string; - fullDescription: string; - category: RoadmapCategory; status: RoadmapStatus; - upvotes: number; - commentCount: number; - createdAt: string; + quarter: string; + category: RoadmapCategory; + votes: number; updatedAt: string; - timeline: Array<{ - date: string; - status: RoadmapStatus; - note: string; - }>; }; const categoryColors: Record = { - Platform: "bg-cyan/10 text-cyan border-cyan/30", - Skills: "bg-purple/10 text-purple border-purple/30", - Community: "bg-green/10 text-green border-green/30", - API: "bg-orange/10 text-orange border-orange/30", - Enterprise: "bg-blue/10 text-blue border-blue/30", + platform: "bg-cyan/10 text-cyan border-cyan/30", + tools: "bg-purple/10 text-purple border-purple/30", + community: "bg-green/10 text-green border-green/30", + enterprise: "bg-blue/10 text-blue border-blue/30", }; const statusColors: Record = { - Planned: "bg-slate-600/20 text-slate-300 border-slate-600/50", - "In Progress": "bg-cyan/10 text-cyan border-cyan/30", - Shipped: "bg-green/10 text-green border-green/30", - Considering: "bg-purple/10 text-purple border-purple/30", + planned: "bg-slate-600/20 text-slate-300 border-slate-600/50", + "in-progress": "bg-cyan/10 text-cyan border-cyan/30", + completed: "bg-purple/10 text-purple border-purple/30", + shipped: "bg-green/10 text-green border-green/30", }; +function toLabel(value: string): string { + return value + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + function formatDate(dateStr: string): string { const date = new Date(dateStr); + if (Number.isNaN(date.getTime())) { + return "Unknown"; + } + return date.toLocaleDateString("en-US", { year: "numeric", month: "long", @@ -59,359 +61,123 @@ interface PageProps { export default function RoadmapItemPage({ params }: PageProps) { const { id } = use(params); - const [hasVoted, setHasVoted] = useState(false); - const [localUpvotes, setLocalUpvotes] = useState(null); - - const items = roadmapData as RoadmapItem[]; - const item = items.find((item) => item.id === id); + const [item, setItem] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [voting, setVoting] = useState(false); + + useEffect(() => { + async function loadItem() { + setLoading(true); + setError(null); + + try { + const response = await fetch(`/api/roadmap/${id}`, { cache: "no-store" }); + const data = (await response.json()) as { item?: RoadmapItem; error?: string }; + + if (!response.ok || !data.item) { + throw new Error(data.error ?? "Roadmap item not found"); + } + + setItem(data.item); + } catch (loadError) { + console.error(loadError); + setError(loadError instanceof Error ? loadError.message : "Failed to load roadmap item."); + } finally { + setLoading(false); + } + } - if (!item) { - notFound(); - } + void loadItem(); + }, [id]); - const displayUpvotes = localUpvotes ?? item.upvotes; + const handleVote = async () => { + if (!item || voting) { + return; + } - const handleVote = () => { - if (!hasVoted) { - setLocalUpvotes((displayUpvotes) => (displayUpvotes ?? item.upvotes) + 1); - setHasVoted(true); + setVoting(true); + setError(null); + + try { + const response = await fetch(`/api/roadmap/${item.id}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + const data = (await response.json()) as { item?: RoadmapItem; error?: string }; + + if (!response.ok || !data.item) { + throw new Error(data.error ?? "Failed to vote"); + } + + setItem(data.item); + } catch (voteError) { + console.error(voteError); + setError(voteError instanceof Error ? voteError.message : "Failed to vote for this feature."); + } finally { + setVoting(false); } }; - // Mock discussion data - const discussions = [ - { - id: "1", - author: "agent_dev_42", - avatar: "AD", - date: "2025-02-07T10:30:00Z", - content: - "This would be incredibly valuable! Having real-time insights into agent behavior would help us catch issues before they become problems.", - upvotes: 12, - }, - { - id: "2", - author: "ai_builder", - avatar: "AB", - date: "2025-02-06T14:15:00Z", - content: - "Would love to see integration with existing monitoring tools like Datadog or New Relic. That way we could correlate agent metrics with infrastructure metrics.", - upvotes: 8, - }, - { - id: "3", - author: "tech_lead_99", - avatar: "TL", - date: "2025-02-05T09:45:00Z", - content: - "Any plans for custom alerting? We'd need to set up alerts for specific thresholds on token usage and error rates.", - upvotes: 15, - }, - ]; - return (
- {/* Back Button */} - - - - Back to Roadmap + ← Back to Roadmap - {/* Header */} -
-
- - {item.category} - - - {item.status} - -
- -

{item.title}

- - {/* Metadata */} -
-
- - - - {displayUpvotes} - upvotes -
-
- - - - {item.commentCount} comments + {loading ? ( +

Loading roadmap item...

+ ) : error ? ( +

{error}

+ ) : !item ? ( +

Roadmap item not found.

+ ) : ( + <> +
+
+ + {toLabel(item.category)} + + + {toLabel(item.status)} + +
+ +

{item.title}

+ +
+ + {item.votes} votes + + {item.quarter} + Updated {formatDate(item.updatedAt)} +
-
- + - Updated {formatDate(item.updatedAt)} + {voting ? "Voting..." : "Vote for this feature"} +
-
-
- - {/* Vote Button */} -
- -
- -
- {/* Main Content */} -
- {/* Description */} - - -

- Description -

-

- {item.fullDescription} -

-
-
- - {/* Discussion Thread */} - - -

- Discussion ({item.commentCount}) -

- - {/* Comment Form */} -
-