react-bootstrap migration, review & parser fixes

UI / react-bootstrap:
Migrate client components to react-bootstrap (Card, Table, Form,
Alert, Badge, Nav, Button, Spinner, Row, Col): the ZXDB explorers
and detail pages (Labels, Genres, Languages, MachineTypes, Releases,
Entries), TapeIdentifier, home page, Navbar and ThemeDropdown.
Server components (home, zxdb hub, magazines, issues) keep raw
HTML+className — react-bootstrap barrel imports resolve to undefined
under Turbopack in server components. Replace bi bi-* CSS icons with
react-bootstrap-icons. Add aria-labels to search inputs and
visually-hidden captions to data tables.

Code-review remediation (docs/todo.md):
- FileViewer: replace useState-as-effect with a proper useEffect.
- register.service: restore request-level caching of parsed registers.
- middleware: convert .js to .ts, dev-only request logging.
- Extract shared types to src/types/zxdb.ts; add src/server/repo
  barrel for incremental per-domain splitting.
- Extract helpers: parseIdList (params.ts), serialize (serialize.ts),
  buildRegisterSummary/isInfoLine (register_helpers.ts).
- Add loading.tsx skeletons for dynamic ZXDB detail routes.
- generateMetadata + notFound() on entry/release/label detail pages.
- opengraph-image: stable keys; ThemeDropdown: drop hardcoded cookie
  domain; remove unused page.module.css.

Register parser & data:
- Update data/nextreg.txt from upstream tbblue (SpectrumNext FPGA):
  0x04/0x0A/0x0F/0x80/0x81 bit changes, new Issue 5 board id, 0x43
  renamed "Palette Control", 0xF0/0xF8/0xF9/0xFA now "Issues 4 and 5
  Only".
- Add reg_44 custom parser for 0x44 (Palette Value 9-bit): the two
  consecutive writes render as separate "1st write" / "2nd write"
  modes.
- Skip commented-out register headers so the disabled 0xA3 block no
  longer leaks a phantom register.
- Add detailHasContent guard so body-less registers (0xC7/0xCB/0xCF/
  0xFF) and 0xF0's leading blank no longer emit empty tab strips.
- Capture 0xF0's leading "Issues 4 and 5 Only" line as register text.
- Add isIssueRestricted (case-sensitive) to detect the issue badge
  across rewording without flagging per-bit "(issue 5 only)" notes;
  update badge label to "Issues 4 & 5 Only".

claude-opus-4-8@lucy
This commit is contained in:
2026-06-08 22:47:37 +01:00
parent 590b07147a
commit e803274af4
79 changed files with 1346 additions and 1011 deletions

View File

@@ -1,4 +1,4 @@
import { listAvailabletypes } from "@/server/repo/zxdb";
import { listAvailabletypes } from "@/server/repo";
export async function GET() {
const items = await listAvailabletypes();

View File

@@ -1,4 +1,4 @@
import { listCasetypes } from "@/server/repo/zxdb";
import { listCasetypes } from "@/server/repo";
export async function GET() {
const items = await listCasetypes();

View File

@@ -1,4 +1,4 @@
import { listCurrencies } from "@/server/repo/zxdb";
import { listCurrencies } from "@/server/repo";
export async function GET() {
const items = await listCurrencies();

View File

@@ -23,6 +23,11 @@ export async function GET(req: NextRequest) {
return new NextResponse("Invalid source or mirroring not enabled", { status: 400 });
}
// Defense-in-depth: reject obvious traversal before joining
if (filePath.includes("..")) {
return new NextResponse("Forbidden", { status: 403 });
}
// Security: Ensure path doesn't escape baseDir
const absolutePath = path.normalize(path.join(baseDir, filePath));
if (!absolutePath.startsWith(path.normalize(baseDir))) {

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { getEntryById } from "@/server/repo/zxdb";
import { getEntryById } from "@/server/repo";
const paramsSchema = z.object({ id: z.coerce.number().int().positive() });

View File

@@ -1,4 +1,4 @@
import { listFiletypes } from "@/server/repo/zxdb";
import { listFiletypes } from "@/server/repo";
export async function GET() {
const items = await listFiletypes();

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { entriesByGenre } from "@/server/repo/zxdb";
import { entriesByGenre } from "@/server/repo";
const paramsSchema = z.object({ id: z.coerce.number().int().positive() });
const querySchema = z.object({

View File

@@ -1,4 +1,4 @@
import { listGenres } from "@/server/repo/zxdb";
import { listGenres } from "@/server/repo";
export async function GET() {
const data = await listGenres();

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { getLabelById, getLabelAuthoredEntries, getLabelPublishedEntries } from "@/server/repo/zxdb";
import { getLabelById, getLabelAuthoredEntries, getLabelPublishedEntries } from "@/server/repo";
const paramsSchema = z.object({ id: z.coerce.number().int().positive() });
const querySchema = z.object({

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { searchLabels } from "@/server/repo/zxdb";
import { searchLabels } from "@/server/repo";
const querySchema = z.object({
q: z.string().optional(),

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { entriesByLanguage } from "@/server/repo/zxdb";
import { entriesByLanguage } from "@/server/repo";
const paramsSchema = z.object({ id: z.string().trim().length(2) });
const querySchema = z.object({

View File

@@ -1,4 +1,4 @@
import { listLanguages } from "@/server/repo/zxdb";
import { listLanguages } from "@/server/repo";
export async function GET() {
const data = await listLanguages();

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { entriesByMachinetype } from "@/server/repo/zxdb";
import { entriesByMachinetype } from "@/server/repo";
const paramsSchema = z.object({ id: z.coerce.number().int().positive() });
const querySchema = z.object({

View File

@@ -1,4 +1,4 @@
import { listMachinetypes } from "@/server/repo/zxdb";
import { listMachinetypes } from "@/server/repo";
export async function GET() {
const data = await listMachinetypes();

View File

@@ -1,6 +1,7 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { searchReleases } from "@/server/repo/zxdb";
import { searchReleases } from "@/server/repo";
import { parseIdList } from "@/utils/params";
const querySchema = z.object({
q: z.string().optional(),
@@ -17,15 +18,6 @@ const querySchema = z.object({
isDemo: z.coerce.boolean().optional(),
});
function parseIdList(value: string | undefined) {
if (!value) return undefined;
const ids = value
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : undefined;
}
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const parsed = querySchema.safeParse({

View File

@@ -1,4 +1,4 @@
import { listRoletypes } from "@/server/repo/zxdb";
import { listRoletypes } from "@/server/repo";
export async function GET() {
const items = await listRoletypes();

View File

@@ -1,4 +1,4 @@
import { listSchemetypes } from "@/server/repo/zxdb";
import { listSchemetypes } from "@/server/repo";
export async function GET() {
const items = await listSchemetypes();

View File

@@ -1,6 +1,7 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { searchEntries, getEntryFacets } from "@/server/repo/zxdb";
import { searchEntries, getEntryFacets } from "@/server/repo";
import { parseIdList } from "@/utils/params";
const querySchema = z.object({
q: z.string().optional(),
@@ -19,15 +20,6 @@ const querySchema = z.object({
facets: z.coerce.boolean().optional(),
});
function parseIdList(value: string | undefined) {
if (!value) return undefined;
const ids = value
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : undefined;
}
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const parsed = querySchema.safeParse({

View File

@@ -1,4 +1,4 @@
import { listSourcetypes } from "@/server/repo/zxdb";
import { listSourcetypes } from "@/server/repo";
export async function GET() {
const items = await listSourcetypes();