# 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 by `publishedAt desc` - `roadmapItemsQuery`: filter `_type == "roadmapItem" && status != "shipped"`, order by `order asc` within each status group. **Shipped items are excluded from the roadmap query** — when an item ships, the author changes its status to `shipped` in Sanity and creates a corresponding `changelogEntry`. 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` → amber - `fix` → green - `improvement` → 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 | | Email | 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 `order` field 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-spring` repo, tracked separately) - Rate limiting on the feedback endpoint (deferred; IP capture provides visibility at current scale)