10 KiB
Changelog, Roadmap & Feedback Design Spec
Date: 2026-03-30 Status: Approved
Overview
Add a public /changelog page with three tabs (What's New, What's Next, Give Feedback), auth-gated upvoting on roadmap items, a shared feedback form available as a standalone page and a modal triggered from the nav/footer, and an admin view for reviewing submissions.
Goals
- Publicly share release notes (changelog) and upcoming work (roadmap)
- Let authenticated users upvote roadmap items to signal priority
- Collect structured user feedback (bug / feature / general) with optional email
- Store feedback in Spring Boot DB, reviewable in the admin panel
- Add "Send Feedback" entry point to TopNav and homepage footer
Routes
| Route | Description |
|---|---|
/changelog |
Three-tab page: What's New / What's Next / Give Feedback |
/feedback |
Standalone feedback page — full-width FeedbackForm |
/admin/feedback |
Admin table of feedback submissions |
Tab state on /changelog is driven by URL hash (#whats-new, #whats-next, #give-feedback) so tabs are deep-linkable. When no hash is present, the default active tab is What's New. The "Don't see what you need? Give us feedback →" nudge on the roadmap tab links to #give-feedback.
Middleware
/changelog and /feedback are public routes — no authentication required. Both must be added to isPublicPath() in middleware.ts so they remain accessible when LAUNCH_ONLY_ROOT=true. The Next.js proxy routes /api/feedback (POST) and /api/roadmap/votes (GET) require no auth and will pass through middleware as-is since they are not prefixed with /api/admin or matched by isProtectedLaunchPath(). This is intentional and requires no middleware change for those API routes.
Sanity Schema (two new document types)
changelogEntry
| Field | Type | Notes |
|---|---|---|
title |
string | Display title of the release |
version |
string | e.g. v0.4 |
publishedAt |
datetime | Controls ordering and display date |
tags |
array of string | Values: feature, fix, improvement |
body |
Portable Text | Rich text body |
roadmapItem
| Field | Type | Notes |
|---|---|---|
title |
string | Short item title |
description |
text | Plain-text description |
status |
string enum | planned | in-progress | shipped |
order |
number | Manual sort order within each status group |
Both use the existing Sanity project (zal102rv, dataset production). GROQ query strings are added to lib/sanity.ts alongside the existing postsQuery. The RSC page must call sanityFetch() from lib/sanityFetch.ts to execute those queries — not client.fetch() from the Sanity client directly. This is the same pattern used by the guides page and is required for correct behaviour in Docker/server environments where CDN DNS resolution fails.
Pages use export const revalidate = 60 ISR, consistent with guides.
The Sanity _id of each roadmapItem serves as the foreign key in Spring Boot's votes table — no additional ID field required. Sanity _id values are stable UUIDs.
GROQ Query Filters
changelogEntriesQuery: filter_type == "changelogEntry" && publishedAt <= now(), order bypublishedAt descroadmapItemsQuery: filter_type == "roadmapItem" && status != "shipped", order byorder ascwithin each status group. Shipped items are excluded from the roadmap query — when an item ships, the author changes its status toshippedin Sanity and creates a correspondingchangelogEntry. The roadmap tab never displays shipped items.
Spring Boot Backend
New Tables
feedback
| Column | Type | Notes |
|---|---|---|
id |
UUID, PK | |
category |
enum | BUG, FEATURE_REQUEST, GENERAL |
message |
text | Max 1000 chars, enforced at API level |
email |
varchar, nullable | Optional follow-up email |
ip_address |
varchar | Captured server-side for spam visibility |
user_agent |
varchar | Captured server-side for spam visibility |
reviewed |
boolean, default false | Admin marks reviewed |
created_at |
timestamp |
Rate limiting on POST /api/v1/feedback is deferred — IP and user agent capture provides enough visibility for manual spam review at this scale.
roadmap_votes
| Column | Type | Notes |
|---|---|---|
user_id |
UUID, FK → users | Part of composite PK |
sanity_item_id |
varchar | Part of composite PK; Sanity _id of the roadmap item |
created_at |
timestamp | |
| PRIMARY KEY(user_id, sanity_item_id) | Composite PK — enforces one vote per user per item |
New API Endpoints (Spring Boot)
| Method | Path | Auth | Notes |
|---|---|---|---|
POST |
/api/v1/feedback |
None | Creates feedback record; captures IP + user agent server-side |
GET |
/api/v1/roadmap/votes?ids=... |
Optional | Returns vote counts + current user's voted state for a list of Sanity IDs. For unauthenticated callers, returns vote counts with voted: false for all items — never 401. |
POST |
/api/v1/roadmap/{sanityId}/vote |
Required | Toggles vote on/off; returns new count and voted state |
Next.js Proxy Routes
All three endpoints are proxied via app/api/ — client code never calls Spring Boot directly, consistent with the existing proxy pattern.
| Next.js route | Proxies to |
|---|---|
POST /api/feedback |
POST /api/v1/feedback |
GET /api/roadmap/votes |
GET /api/v1/roadmap/votes |
POST /api/roadmap/[sanityId]/vote |
POST /api/v1/roadmap/:id/vote |
The Next.js proxy for POST /api/feedback captures x-forwarded-for and user-agent from the incoming request headers and forwards them to Spring Boot — the client form never sends these values.
Frontend Components
New Files
| File | Type | Responsibility |
|---|---|---|
app/(app)/changelog/page.tsx |
RSC | Fetches Sanity changelog + roadmap data via sanityFetch, renders tab shell + passes data as props |
components/ChangelogTabs.tsx |
"use client" |
Hash-based tab switcher (default: #whats-new), renders all three tab panels |
components/RoadmapItem.tsx |
"use client" |
Single roadmap item with optimistic upvote toggle |
components/FeedbackForm.tsx |
"use client" |
Shared form (category + message + email), posts to /api/feedback |
components/FeedbackModal.tsx |
"use client" |
Backdrop + dialog wrapper around FeedbackForm |
app/(app)/feedback/page.tsx |
RSC | Standalone page rendering FeedbackForm full-width |
app/admin/feedback/page.tsx |
RSC | Admin table of submissions, sortable + filterable |
app/api/feedback/route.ts |
API route | Proxy to Spring Boot; forwards IP + user-agent headers |
app/api/roadmap/votes/route.ts |
API route | Proxy to Spring Boot |
app/api/roadmap/[sanityId]/vote/route.ts |
API route | Proxy to Spring Boot |
Modified Files
| File | Change |
|---|---|
lib/sanity.ts |
Add changelogEntriesQuery and roadmapItemsQuery |
middleware.ts |
Add /changelog and /feedback to isPublicPath() |
components/TopNav.tsx |
Add "Send Feedback" ghost button; modal open state is local to TopNav via useState |
app/page.tsx |
Add "Send Feedback" link to the homepage's inline footer (homepage-only; this is intentional — TopNav covers all other pages) |
FeedbackModal State
FeedbackModal open/close state is managed locally in TopNav with useState. No global context or portal is required — the modal renders inside TopNav's JSX and is positioned fixed via CSS. This is sufficient given "Send Feedback" is the only trigger point in the nav.
UI Design
/changelog Page Header
BATTL BUILDERS (small caps label)
Updates (h1)
What we've shipped and what's coming next. (subhead)
[ What's New ] [ What's Next ] [ Give Feedback ] (tab bar, amber underline on active)
What's New Tab
Two-column entry layout: left column is version + date (120px fixed), right column is tags + title + body. Entries separated by bottom border, ordered newest first.
Tag values rendered as small pill badges:
feature→ amberfix→ greenimprovement→ blue
What's Next Tab
Items grouped under "In Progress" and "Planned" headings. Shipped items are never shown here (filtered out in the GROQ query). Each item is a card with:
- Left: upvote button (chevron up + count) — bordered pill, amber when voted
- Center: title + description
- Right: status badge (In Progress = amber, Planned = muted)
Unauthenticated users clicking upvote are prompted to sign in (toast). Voted state is toggled optimistically — rolls back on error.
Footer nudge: "Don't see what you need? Give us feedback →" links to #give-feedback.
Give Feedback Tab & /feedback Page
Full-width form:
- Category + email in a two-column row
- Message textarea (full width, 1000 char limit with counter)
- Submit button bottom-right, privacy note bottom-left
Feedback Modal
Triggered by "Send Feedback" in TopNav and homepage inline footer. Same fields as the tab form. Full-width submit button inside the modal.
Admin View (/admin/feedback)
Table with columns: Date, Category, Message (truncated to 120 characters), Email, Reviewed. Sortable by date. Filterable by category (Bug / Feature / General) and reviewed status. Follows existing admin page patterns.
Feedback Form Fields
| Field | Required | Validation |
|---|---|---|
| Category | Yes | One of: Bug report, Feature request, General feedback |
| Message | Yes | 1–1000 characters |
| No | Valid email format if provided |
ip_address and user_agent are captured server-side in the Next.js proxy route — never sent from the client form.
SEO Metadata
Both /changelog and /feedback export a metadata object following the existing page pattern:
/changelog:title: "What's New — Battl Builders", relevant description/feedback:title: "Send Feedback — Battl Builders", relevant description
Out of Scope
- Email notifications when feedback is submitted (separate task)
- Sorting roadmap items by vote count (manual
orderfield only for now) - Public display of vote counts on the changelog tab
- Sanity Studio schema deployment (handled separately via Sanity CLI)
- Spring Boot entity/migration code (lives in
ballistic-builder-springrepo, tracked separately) - Rate limiting on the feedback endpoint (deferred; IP capture provides visibility at current scale)