Merge branch 'dev'
This commit is contained in:
28
docs/ZXDB.md
28
docs/ZXDB.md
@@ -39,6 +39,34 @@ Notes:
|
||||
- The URL must start with `mysql://`. Env is validated at boot by `src/env.ts` (Zod), failing fast if misconfigured.
|
||||
- The app uses a singleton `mysql2` pool (`src/server/db.ts`) and Drizzle ORM for typed queries.
|
||||
|
||||
### Local File Mirrors
|
||||
|
||||
The explorer can optionally show "Local Mirror" links for downloads if you have local copies of the ZXDB and World of Spectrum (WoS) file archives.
|
||||
|
||||
#### Configuration
|
||||
|
||||
To enable local mirrors, set the following variables in your `.env`:
|
||||
|
||||
```bash
|
||||
# Absolute paths to your local mirrors
|
||||
ZXDB_LOCAL_FILEPATH=/path/to/your/zxdb/mirror
|
||||
WOS_LOCAL_FILEPATH=/path/to/your/wos/mirror
|
||||
|
||||
# Optional: Remote path prefixes to strip from database links before prepending local paths
|
||||
ZXDB_FILE_PREFIX=/zxdb/sinclair/
|
||||
WOS_FILE_PREFIX=/pub/sinclair/
|
||||
```
|
||||
|
||||
#### How it works
|
||||
|
||||
1. The app identifies if a download link matches `ZXDB_FILE_PREFIX` or `WOS_FILE_PREFIX`.
|
||||
2. It strips the prefix from the database link.
|
||||
3. It joins the remaining relative path to the corresponding `*_LOCAL_FILEPATH`.
|
||||
4. It checks if the file exists on the local disk.
|
||||
5. If the file exists and the environment variable is set, a "Local Mirror" link is displayed in the UI, pointing to a proxy download API (`/api/zxdb/download`).
|
||||
|
||||
Note: Obtaining these mirrors is left as an exercise to the host. The paths do not need to share a common parent directory. Both mirrors are optional and independent; you can configure one, both, or neither.
|
||||
|
||||
## Running
|
||||
|
||||
```
|
||||
|
||||
89
docs/changelog.md
Normal file
89
docs/changelog.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Changelog 📜
|
||||
|
||||
All notable changes to Next Explorer, in roughly chronological order.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 v0.3.0 — Tapes, Hashes & Downloads *(unreleased, Feb 2026)*
|
||||
|
||||
The great "what IS this .tap file?" release. ZXDB downloads become first-class citizens with local mirroring, grouping, inline previews, and now the ability to identify a tape by dropping it on the page.
|
||||
|
||||
### ✨ New
|
||||
- **Tape Identifier** — drag-and-drop a `.tap`/`.tzx`/`.p`/`.o` file onto `/zxdb` and get instant ZXDB entry matches based on SHA-256 hash lookup 🎯
|
||||
- **Software hashes database** — 32,960-row snapshot of SHA-256 hashes for known ZXDB downloads, with a pipeline script (`update-software-hashes.mjs`) to rebuild/extend it
|
||||
- **Local ZXDB / WoS mirror support** — proxy downloads through the app's own API so self-hosted mirrors work seamlessly; inline previews rendered without leaving the page
|
||||
- **Magazine reviews** — reviews now shown on magazine issue pages
|
||||
- **Label detail pages** — full label view with releases, genre breakdown, and year filtering
|
||||
- **Year filter** on releases/entries
|
||||
|
||||
### 🔧 Improved
|
||||
- Download viewer reworked: grouped by format, inline previews, human-readable sizes
|
||||
- Local file path resolution corrected for edge-cases
|
||||
- Silently skip `/denied/` and other non-hosted prefixes during hash imports
|
||||
|
||||
---
|
||||
|
||||
## ✅ v0.2.0 — ZXDB Explorer *(December 2025 – January 2026)*
|
||||
|
||||
The big one. A full cross-linked browser for the ZXDB software database, server-rendered for fast first paint, with deep links everywhere.
|
||||
|
||||
### ✨ New
|
||||
- **ZXDB integration** — MySQL via `mysql2` + Drizzle ORM; Zod-validated env (`ZXDB_URL`)
|
||||
- **Entries browser** — search, paginate, filter; deep-links to individual entry pages
|
||||
- **Entry detail pages** — aliases, web references, relations, tags, ports, scores, origins, facets 🗂️
|
||||
- **Releases browser** — filterable by machine type, genre, label, year; download links
|
||||
- **Labels browser + detail** — label pages with linked releases
|
||||
- **Genres, Languages, Machine Types** — category hubs with entry counts
|
||||
- **Magazines + Issues** — stub magazine browser with issue listing
|
||||
- **Cross-linked UI** — `EntryLink` component used everywhere; Next `Link` for prefetching
|
||||
- **SSR + ISR** — index pages server-render initial data; `revalidate = 3600` on non-search pages for fast repeat visits
|
||||
- **Multi-select machine type filter** with chip toggles; favouring Next hardware by default
|
||||
- **Shared explorer components** — `ExplorerLayout`, `FilterSidebar`, `FilterSection`, `MultiSelectChips`, `Pagination` 🧩
|
||||
- **Breadcrumbs** decoupled from search input
|
||||
- **Landing page** for `/zxdb` with hub links and hero
|
||||
- **Deploy helper** script (`bin/deploy.sh`)
|
||||
- **OG images** for register pages (happy new year from Codex 🎉)
|
||||
|
||||
### 🔧 Improved
|
||||
- ZXDB pagination counters fixed
|
||||
- Facets filter aliasing corrected
|
||||
- Case-insensitive search improvements
|
||||
- Graceful handling of missing `releases` / `downloads` schema tables
|
||||
- Homepage hero updated
|
||||
- Registers sidebar refactored to share explorer components
|
||||
|
||||
### 🏗️ Infrastructure
|
||||
- Zod env validation for all ZXDB config
|
||||
- `information_schema.tables` check before querying optional tables
|
||||
- API routes under `/api/zxdb/*` with Zod input validation, Node runtime
|
||||
- ZXDB setup guide at `docs/ZXDB.md`
|
||||
|
||||
---
|
||||
|
||||
## ✅ v0.1.0 — Registers Explorer *(October – November 2025)*
|
||||
|
||||
The origin story. Started from a Create Next App scaffold with a GPT-5 assist, then heavily hand-crafted into something actually useful for exploring Spectrum Next hardware registers.
|
||||
|
||||
### ✨ New
|
||||
- **Register browser** — loads and parses `data/nextreg.txt`; real-time search/filter with results at a glance
|
||||
- **Register detail pages** — per-mode bitfield views (read/write/common), notes, and source modal
|
||||
- **Source viewer** — inline modal showing the raw `nextreg.txt` source for any register
|
||||
- **Wikilink support** — links parsed from register definitions and rendered as external refs
|
||||
- **Multi-parser architecture** — `reg_default.ts` for standard registers; `reg_f0.ts` for the exotic `0xF0` register; easy to extend 🔌
|
||||
- **Multi-line footnote support** — parser handles footnotes that span multiple lines
|
||||
- **Deep-linkable search** — `?q=` query param synced to the search box so searches can be bookmarked/shared 🔗
|
||||
- **Dark mode** — cookie-based theme set server-side to eliminate flash-of-wrong-theme ☀️🌙
|
||||
- **Bootswatch Pulse theme** — purple primary; react-bootstrap throughout
|
||||
|
||||
### 🔧 Improved
|
||||
- iOS bitfield fix — prevent Safari turning hex values into tappable phone numbers 📱
|
||||
- Parser on-demand (lazy load, not at startup)
|
||||
- Robust case-insensitive search
|
||||
- Linting and dead code removed; hallucination CSS cleaned up
|
||||
- Dokku build pipeline stabilised (pnpm store-dir pinned)
|
||||
- Next.js security bump (Dec 2025)
|
||||
|
||||
### 🏗️ Infrastructure
|
||||
- Project self-documents via `CLAUDE.md` / `AGENT.md`
|
||||
- Live `nextreg.txt` used (not bundled snapshot)
|
||||
- Dev runner fixed for local development
|
||||
75
docs/plans/plan_feature-software-hashes_implimentation.md
Normal file
75
docs/plans/plan_feature-software-hashes_implimentation.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# WIP: Software Hashes
|
||||
|
||||
**Branch:** `feature/software-hashes`
|
||||
**Started:** 2026-02-17
|
||||
**Status:** Complete
|
||||
|
||||
## Plan
|
||||
|
||||
Implements [docs/plans/software-hashes.md](software-hashes.md) — a derived `software_hashes` table storing MD5, CRC32 and size for tape-image contents extracted from download zips.
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] Create `data/zxdb/` directory (for JSON snapshot)
|
||||
- [x] Add `software_hashes` Drizzle schema model
|
||||
- [x] Create `bin/update-software-hashes.mjs` — main pipeline script
|
||||
- [x] DB query for tape-image downloads (filetype_id IN 8, 22)
|
||||
- [x] Resolve local zip path via CDN mapping (uses CDN_CACHE env var)
|
||||
- [x] Extract `_CONTENTS` (skip if exists)
|
||||
- [x] Find tape file (.tap/.tzx/.pzx/.csw) with priority order
|
||||
- [x] Compute MD5, CRC32, size_bytes
|
||||
- [x] Upsert into software_hashes
|
||||
- [x] State file for resume support
|
||||
- [x] JSON export after bulk update (atomic write)
|
||||
- [x] Update `bin/import_mysql.sh` to reimport snapshot on DB wipe
|
||||
- [x] Add pnpm script entries
|
||||
|
||||
## Progress Log
|
||||
|
||||
### 2026-02-17T16:00Z
|
||||
- Started work. Branch created from `main` at `b361201`.
|
||||
- Explored codebase: understood DB schema, CDN mapping, import pipeline.
|
||||
- Key findings:
|
||||
- filetype_id 8 = "Tape image" (33,427 rows), 22 = "BUGFIX tape image" (98 rows)
|
||||
- CDN_CACHE = /Volumes/McFiver/CDN, paths: SC/ (zxdb) and WoS/ (pub)
|
||||
- `_CONTENTS` dirs exist in WoS but not yet in SC
|
||||
- data/zxdb/ directory needs creation
|
||||
- import_mysql.sh needs software_hashes reimport step
|
||||
|
||||
### 2026-02-17T16:04Z
|
||||
- Implemented Drizzle schema model for `software_hashes`.
|
||||
- Created `bin/update-software-hashes.mjs` pipeline script.
|
||||
- Updated `bin/import_mysql.sh` with JSON snapshot reimport.
|
||||
- Added `update:hashes` and `export:hashes` pnpm scripts.
|
||||
|
||||
### 2026-02-17T16:09Z
|
||||
- First full run completed successfully:
|
||||
- 33,525 total tape-image downloads in DB
|
||||
- 32,305 rows hashed and inserted into software_hashes
|
||||
- ~1,220 skipped (missing local zips, `/denied/` prefix, `.p` ZX81 files with no tape content)
|
||||
- JSON snapshot exported: 7.2MB, 32,305 rows at `data/zxdb/software_hashes.json`
|
||||
- All plan steps verified working.
|
||||
|
||||
## Decisions & Notes
|
||||
|
||||
- Target filetype IDs: 8 and 22 (tape image + bugfix tape image).
|
||||
- Tape file priority: .tap > .tzx > .pzx > .csw (most common first).
|
||||
- CDN_CACHE comes from env var (not hard-coded, unlike sync-downloads.mjs).
|
||||
- JSON snapshot at data/zxdb/software_hashes.json (7.2MB, committed to repo).
|
||||
- Node.js built-in `crypto` for MD5; custom CRC32 lookup table (no external deps).
|
||||
- `inner_path` column added (not in original plan) to record which file inside the zip was hashed.
|
||||
- `/denied/` and `/nvg/` prefix downloads (~443) are logged and skipped (no local mirror).
|
||||
- `.p` files (ZX81 programs) categorized as tape images but contain no .tap/.tzx/.pzx/.csw — logged as "no tape file".
|
||||
- Uses system `unzip` for extraction (handles bracket-heavy filenames via `execFile` not shell).
|
||||
|
||||
## Blockers
|
||||
|
||||
None.
|
||||
|
||||
## Commits
|
||||
|
||||
b361201 - Ready to start adding hashes
|
||||
944a2dc - wip: start feature/software-hashes — init progress tracker
|
||||
f5ae89e - feat: add software_hashes table schema and reimport pipeline
|
||||
edc937a - feat: add update-software-hashes.mjs pipeline script
|
||||
9bfebc1 - feat: add initial software_hashes JSON snapshot (32,305 rows)
|
||||
@@ -0,0 +1,44 @@
|
||||
# WIP: Tape Identifier Dropzone
|
||||
|
||||
**Branch:** `feature/software-hashes`
|
||||
**Started:** 2026-02-17
|
||||
**Status:** Complete
|
||||
|
||||
## Plan
|
||||
|
||||
Implements the tape identifier feature from [docs/plans/tape-identifier.md](tape-identifier.md).
|
||||
|
||||
Drop a tape file on the /zxdb page → client computes MD5 + size → server action looks up `software_hashes` → returns identified ZXDB entry.
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] Add `lookupByMd5()` to `src/server/repo/zxdb.ts`
|
||||
- [x] Create `src/utils/md5.ts` — pure-JS MD5 for browser
|
||||
- [x] Create `src/app/zxdb/actions.ts` — server action `identifyTape`
|
||||
- [x] Create `src/app/zxdb/TapeIdentifier.tsx` — client component with dropzone
|
||||
- [x] Insert `<TapeIdentifier />` into `src/app/zxdb/page.tsx`
|
||||
- [ ] Verify on http://localhost:4000/zxdb
|
||||
|
||||
## Progress Log
|
||||
|
||||
### 2026-02-17T00:00
|
||||
- Started work. Continuing on `feature/software-hashes` at `e27a16e`.
|
||||
|
||||
### 2026-02-17T00:01
|
||||
- All implementation complete. Type check passes. Ready for visual verification.
|
||||
|
||||
## Decisions & Notes
|
||||
|
||||
- Uses RSC server actions (not API routes) to discourage bulk scripting.
|
||||
- MD5 computed client-side; file never leaves the browser.
|
||||
- No new npm dependencies — pure-JS MD5 implementation (~130 lines).
|
||||
- TapeIdentifier placed between hero and "Start exploring" grid in a row layout with explanatory text alongside.
|
||||
|
||||
## Blockers
|
||||
|
||||
None.
|
||||
|
||||
## Commits
|
||||
|
||||
fc513c5 - wip: start tape identifier — init progress tracker
|
||||
8624050 - feat: add tape identifier dropzone on /zxdb
|
||||
155
docs/plans/software-hashes.md
Normal file
155
docs/plans/software-hashes.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Software Hashes Plan
|
||||
|
||||
Plan for adding a derived `software_hashes` table, its update pipeline, and JSON snapshot lifecycle to survive DB wipes.
|
||||
|
||||
---
|
||||
|
||||
## 1) Goals and Scope (Plan Step 1)
|
||||
|
||||
- Create and maintain `software_hashes` for (at this stage) tape-image downloads.
|
||||
- Preserve existing `_CONTENTS` folders; only create missing ones.
|
||||
- Export `software_hashes` to JSON after each bulk update.
|
||||
- Reimport `software_hashes` JSON during DB wipe in `bin/import_mysql.sh` (or a helper script it invokes).
|
||||
- Ensure all scripts are idempotent and resume-safe.
|
||||
|
||||
---
|
||||
|
||||
## 2) Confirm Pipeline Touchpoints (Plan Step 2)
|
||||
|
||||
- Verify `bin/import_mysql.sh` is the authoritative DB wipe/import entry point.
|
||||
- Confirm `bin/sync-downloads.mjs` remains responsible only for CDN cache sync.
|
||||
- Confirm `src/server/schema/zxdb.ts` uses `downloads.id` as the natural FK target.
|
||||
|
||||
---
|
||||
|
||||
## 3) Define Data Model: `software_hashes` (Plan Step 3)
|
||||
|
||||
### Table naming and FK alignment
|
||||
|
||||
- Table: `software_hashes`.
|
||||
- FK: `download_id` → `downloads.id`.
|
||||
- Column names follow existing DB `snake_case` conventions.
|
||||
|
||||
### Planned columns
|
||||
|
||||
- `download_id` (PK or unique index; FK to `downloads.id`)
|
||||
- `md5`
|
||||
- `crc32`
|
||||
- `size_bytes`
|
||||
- `updated_at`
|
||||
|
||||
### Planned indexes / constraints
|
||||
|
||||
- Unique index on `download_id`.
|
||||
- Index on `md5` for reverse lookup.
|
||||
- Index on `crc32` for reverse lookup.
|
||||
|
||||
---
|
||||
|
||||
## 4) Define JSON Snapshot Format (Plan Step 4)
|
||||
|
||||
### Location
|
||||
|
||||
- Default: `data/zxdb/software_hashes.json` (or another agreed path).
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"exportedAt": "2026-02-17T15:18:00.000Z",
|
||||
"rows": [
|
||||
{
|
||||
"download_id": 123,
|
||||
"md5": "...",
|
||||
"crc32": "...",
|
||||
"size_bytes": 12345,
|
||||
"updated_at": "2026-02-17T15:18:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Planned import policy
|
||||
|
||||
- If snapshot exists: truncate `software_hashes` and bulk insert.
|
||||
- If snapshot missing: log and continue without error.
|
||||
|
||||
---
|
||||
|
||||
## 5) Implement Tape Image Update Workflow (Plan Step 5)
|
||||
|
||||
### Planned script
|
||||
|
||||
- `bin/update-software-hashes.mjs` (name can be adjusted).
|
||||
|
||||
### Planned input dataset
|
||||
|
||||
- Query `downloads` for tape-image rows (filter by `filetype_id` or joined `filetypes` table).
|
||||
|
||||
### Planned per-item process
|
||||
|
||||
1. Resolve local zip path using the same CDN mapping used by `sync-downloads`.
|
||||
2. Compute `_CONTENTS` folder name: `<zip filename>_CONTENTS` (exact match).
|
||||
3. If `_CONTENTS` exists, keep it untouched.
|
||||
4. If missing, extract zip into `_CONTENTS` using a library that avoids shell expansion issues with brackets.
|
||||
5. Locate tape file inside (`.tap`, `.tzx`, `.pzx`, `.csw`):
|
||||
- Apply a deterministic priority order.
|
||||
- If multiple candidates remain, log and skip (or record ambiguity).
|
||||
6. Compute `md5`, `crc32`, and `size_bytes` for the selected file.
|
||||
7. Upsert into `software_hashes` keyed by `download_id`.
|
||||
|
||||
### Planned error handling
|
||||
|
||||
- Log missing zips or missing tape files.
|
||||
- Continue after recoverable errors; fail only on critical DB errors.
|
||||
|
||||
---
|
||||
|
||||
## 6) Implement JSON Export Lifecycle (Plan Step 6)
|
||||
|
||||
- After each bulk update, export `software_hashes` to JSON.
|
||||
- Write atomically (temp file + rename).
|
||||
- Include `exportedAt` timestamp in snapshot.
|
||||
|
||||
---
|
||||
|
||||
## 7) Reimport During Wipe (`bin/import_mysql.sh`) (Plan Step 7)
|
||||
|
||||
### Planned placement
|
||||
|
||||
- Immediately after database creation and ZXDB SQL import completes.
|
||||
|
||||
### Planned behavior
|
||||
|
||||
- Attempt to read JSON snapshot.
|
||||
- If present, truncate and reinsert `software_hashes`.
|
||||
- Log imported row count.
|
||||
|
||||
---
|
||||
|
||||
## 8) Add Idempotency and Resume Support (Plan Step 8)
|
||||
|
||||
- State file similar to `.sync-downloads.state.json` to track last `download_id` processed.
|
||||
- CLI flags:
|
||||
- `--resume` (default)
|
||||
- `--start-from-id`
|
||||
- `--rebuild-all`
|
||||
- Reprocess when zip file size or mtime changes.
|
||||
|
||||
---
|
||||
|
||||
## 9) Validation Checklist (Plan Step 9)
|
||||
|
||||
- `_CONTENTS` folders are never deleted.
|
||||
- Hashes match expected MD5/CRC32 for known samples.
|
||||
- JSON snapshot is created and reimported correctly.
|
||||
- Reverse lookup by `md5`/`crc32`/`size_bytes` identifies misnamed files.
|
||||
- Script can resume safely after interruption.
|
||||
|
||||
---
|
||||
|
||||
## 10) Open Questions / Confirmations (Plan Step 10)
|
||||
|
||||
- Final `software_hashes` column list and types.
|
||||
- Exact JSON snapshot path.
|
||||
- Filetype IDs that map to “Tape Image” in `downloads`.
|
||||
67
docs/plans/tape-identifier.md
Normal file
67
docs/plans/tape-identifier.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Plan: Tape Identifier Dropzone on /zxdb
|
||||
|
||||
## Context
|
||||
|
||||
We have 32,960 rows in `software_hashes` with MD5, CRC32, size, and inner_path for tape-image contents. This feature exposes that data to users: drop a tape file, get it identified against the ZXDB database.
|
||||
|
||||
Uses RSC (server actions) rather than an API endpoint to make bulk scripted identification harder.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Client-side:** Compute MD5 + file size in the browser, then call a server action with just those two values (file never leaves the client).
|
||||
|
||||
**Server-side:** A Next.js Server Action looks up `software_hashes` by MD5 (and optionally size_bytes for disambiguation), joins to `downloads` and `entries` to return the entry title, download details, and a link.
|
||||
|
||||
**Client-side MD5:** Web Crypto doesn't support MD5. Include a small pure-JS MD5 utility (~80 lines, well-known algorithm). No new npm dependencies.
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### 1. `src/utils/md5.ts` — Pure-JS MD5 for browser use
|
||||
- Exports `async function computeMd5(file: File): Promise<string>`
|
||||
- Reads file as ArrayBuffer, computes MD5, returns hex string
|
||||
- Standard MD5 algorithm implementation, typed for TypeScript
|
||||
|
||||
### 2. `src/app/zxdb/actions.ts` — Server Action
|
||||
- `'use server'` directive
|
||||
- `identifyTape(md5: string, sizeBytes: number)`
|
||||
- Queries `software_hashes` JOIN `downloads` JOIN `entries` by MD5
|
||||
- If multiple matches and size_bytes narrows it, filter further
|
||||
- Returns array of `{ downloadId, entryId, entryTitle, innerPath, md5, crc32, sizeBytes }`
|
||||
|
||||
### 3. `src/app/zxdb/TapeIdentifier.tsx` — Client Component
|
||||
- `'use client'`
|
||||
- States: `idle` → `hashing` → `identifying` → `results` / `not-found`
|
||||
- Dropzone UI:
|
||||
- Dashed border card, large tape icon, "Drop a tape file to identify it"
|
||||
- Lists supported formats: `.tap .tzx .pzx .csw .p .o`
|
||||
- Also has a hidden `<input type="file">` with a "or choose file" link
|
||||
- Drag-over highlight state
|
||||
- On file drop/select:
|
||||
- Validate extension against supported list
|
||||
- Show spinner + "Computing hash..."
|
||||
- Compute MD5 + size client-side
|
||||
- Call server action `identifyTape(md5, size)`
|
||||
- Show spinner + "Searching ZXDB..."
|
||||
- Results view (replaces dropzone):
|
||||
- Match found: entry title as link to `/zxdb/entries/{id}`, inner filename, MD5, file size
|
||||
- Multiple matches: list all
|
||||
- No match: "No matching tape found in ZXDB"
|
||||
- "Identify another tape" button to reset
|
||||
|
||||
### 4. `src/app/zxdb/page.tsx` — Add TapeIdentifier section
|
||||
- Insert `<TapeIdentifier />` as a new section between the hero and "Start exploring" grid
|
||||
- Wrap in a card with distinct styling to make it visually prominent
|
||||
|
||||
### 5. `src/server/repo/zxdb.ts` — Add lookup function
|
||||
- `lookupByMd5(md5: string)` — joins `software_hashes` → `downloads` → `entries`
|
||||
- Returns download_id, entry_id, entry title, inner_path, hash details
|
||||
|
||||
## Verification
|
||||
|
||||
- Visit http://localhost:4000/zxdb
|
||||
- Dropzone should be visible and prominent between hero and navigation grid
|
||||
- Drop a known .tap/.tzx file → should show the identified entry with a link
|
||||
- Drop an unknown file → should show "No matching tape found"
|
||||
- Click "Identify another tape" → resets to dropzone
|
||||
- Check file never leaves browser (Network tab: only the server action call with md5 + size)
|
||||
- Verify non-supported extensions are rejected with helpful message
|
||||
137
docs/plans/zxdb-missing-features.md
Normal file
137
docs/plans/zxdb-missing-features.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# ZXDB Explorer — Missing Features & Gaps
|
||||
|
||||
Audit of the `/zxdb` pages against the ZXDB schema and existing data. Everything listed below is backed by tables already present in the Drizzle schema (`src/server/schema/zxdb.ts`) but not yet surfaced in the UI.
|
||||
|
||||
---
|
||||
|
||||
## Current Coverage
|
||||
|
||||
| Section | List page | Detail page | Facets/Filters |
|
||||
|----------------|-----------|-------------|---------------------------------|
|
||||
| Entries | Search | Full detail | genre, language, machinetype |
|
||||
| Releases | Search | Downloads, scraps, files, magazine refs | — |
|
||||
| Labels | Search | Authored/published entries, permissions, licenses | — |
|
||||
| Magazines | Search | Issues list | — |
|
||||
| Issues | via magazine | Magazine refs (reviews/references) | — |
|
||||
| Genres | List | Entries by genre | — |
|
||||
| Languages | List | Entries by language | — |
|
||||
| Machine Types | List | Entries by type | — |
|
||||
|
||||
---
|
||||
|
||||
## Missing Top-Level Browse Pages
|
||||
|
||||
### 1. Countries
|
||||
- **Tables:** `countries`, `labels.country_id`
|
||||
- **Value:** Browse by country ("all software from Spain", "UK publishers").
|
||||
|
||||
### 2. Tools
|
||||
- **Tables:** `tools`, `tooltypes`
|
||||
- **Value:** Utilities, emulators, and development tools catalogued in ZXDB.
|
||||
|
||||
### 3. Features
|
||||
- **Tables:** `features`
|
||||
- **Value:** Hardware/software features (Multiface, Kempston joystick, etc.).
|
||||
|
||||
### 4. Topics
|
||||
- **Tables:** `topics`, `topictypes`
|
||||
- **Value:** Editorial/thematic groupings used by magazines.
|
||||
|
||||
### 5. Tags / Collections
|
||||
- **Tables:** `tags`, `tagtypes`, `members`
|
||||
- **Value:** Tags are shown per-entry but there is no top-level "browse by tag" page (e.g. all CSSCGC entries, compilations).
|
||||
|
||||
### 6. Licenses
|
||||
- **Tables:** `licenses`, `licensetypes`, `relatedlicenses`, `licensors`
|
||||
- **Value:** Shown per-entry detail but no "browse all licenses" hub (e.g. all games based on a Marvel license).
|
||||
|
||||
---
|
||||
|
||||
## Missing Cross-Links & Facets on Existing Pages
|
||||
|
||||
### 7. Magazine reviews on Entry detail
|
||||
- Release detail shows magazine refs, but entry detail does **not** aggregate them.
|
||||
- A user viewing an entry cannot see "reviewed in Crash #42, p.34" without drilling into each release.
|
||||
|
||||
### 8. Year / date filter on Entries
|
||||
- ZXDB has `release_year` on releases. No year facet on the entries explorer.
|
||||
- Users cannot browse "all games from 1985".
|
||||
|
||||
### 9. Availability type filter on Entries
|
||||
- `availabletypes` API route exists but is not a facet on the entries explorer.
|
||||
- Would allow filtering by "Never released", "MIA", etc.
|
||||
|
||||
### 10. Max players filter on Entries
|
||||
- `entries.max_players` exists but is not filterable.
|
||||
- Would enable "all multiplayer games".
|
||||
|
||||
### 11. Label type filter on Labels page
|
||||
- `labeltypes` table exists and `roletypes` API is served.
|
||||
- Cannot filter the labels list by type (person / company / team / magazine).
|
||||
|
||||
### 12. Country filter on Labels page
|
||||
- Labels have `country_id` but no filter on the list page.
|
||||
|
||||
### 13. Country / language filter on Magazines page
|
||||
- Magazine list has search but no country or language filter chips.
|
||||
|
||||
---
|
||||
|
||||
## Missing Data on Detail Pages
|
||||
|
||||
### 14. Entry detail: magazine reviews section
|
||||
- `search_by_magrefs` is used in release detail but entry detail does not aggregate magazine references across all releases.
|
||||
- Same issue as #7 — the entry page should show a combined reviews/references panel.
|
||||
|
||||
### 15. Label detail: country display
|
||||
- Labels have `country_id` / `country2_id` but the detail page does not show them.
|
||||
|
||||
### 16. Label detail: Wikipedia / website links
|
||||
- `labels.link_wikipedia` and `labels.link_site` exist but are not displayed on the label detail page.
|
||||
|
||||
### 17. Entry detail: related entries via same license
|
||||
- Licenses are shown per-entry but there is no click-through to "other games with this license".
|
||||
|
||||
---
|
||||
|
||||
## Entirely Unsurfaced Datasets
|
||||
|
||||
### 18. NVGs
|
||||
- **Table:** `nvgs`
|
||||
- Historical download archive metadata. Not exposed anywhere.
|
||||
|
||||
### 19. SPEX entries / authors
|
||||
- **Tables:** `spex_entries`, `spex_authors`
|
||||
- No UI.
|
||||
|
||||
### 20. Awards
|
||||
- **Table:** `zxsr_awards`, referenced by `magrefs.award_id`
|
||||
- No awards browsing or display.
|
||||
|
||||
### 21. Review text
|
||||
- **Table:** `zxsr_reviews` (`intro_text`, `review_text`)
|
||||
- Magazine refs link to reviews by ID but the actual review text is never rendered.
|
||||
|
||||
### 22. Articles
|
||||
- **Tables:** `articles`, `articletypes`
|
||||
- No articles browsing.
|
||||
|
||||
---
|
||||
|
||||
## Navigation / UX Gaps
|
||||
|
||||
### 23. No discovery mechanism
|
||||
- No "random entry", "on this day", or "featured" section. Common for large historic databases.
|
||||
|
||||
### 24. No stats / dashboard
|
||||
- No summary counts ("ZXDB has X entries, Y labels, Z magazines"). Would anchor the landing page.
|
||||
|
||||
---
|
||||
|
||||
## Suggested Priority
|
||||
|
||||
| Priority | Items | Rationale |
|
||||
|----------|-------|-----------|
|
||||
| High | 7/14 (magazine refs on entry detail), 8 (year filter), 15-16 (label country + links) | Data exists, just not wired up. High user value. |
|
||||
| Medium | 1 (countries), 5 (tags browse), 6 (licenses browse), 9 (availability filter), 24 (stats) | New pages but straightforward queries. |
|
||||
| Low | 2-4 (tools/features/topics), 10-13 (additional filters), 17-22 (unsurfaced datasets), 23 (discovery) | Useful but niche or requires more design work. |
|
||||
286
docs/todo.md
Normal file
286
docs/todo.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# 📋 Next Explorer — Code Review & TODO
|
||||
|
||||
> Full codebase review performed 2026-03-04. Findings grouped by priority and area.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Critical
|
||||
|
||||
### 🐛 Bug: `FileViewer` uses `useState` as `useEffect`
|
||||
|
||||
**File:** `src/components/FileViewer.tsx:23`
|
||||
|
||||
`useState(() => { ... })` is being abused as a side-effect initializer — it calls `fetch()` inside `useState`'s initializer function. This works by accident on first render but violates React rules:
|
||||
- The initializer can run multiple times in Strict Mode (double-invocation).
|
||||
- It never re-runs if `url` or `title` props change.
|
||||
- Side effects in `useState` initializers are explicitly discouraged by React.
|
||||
|
||||
**Fix:** Replace with `useEffect` with proper dependency array on `[url, isText]`.
|
||||
|
||||
### 🐛 Bug: Register service caching is commented out
|
||||
|
||||
**File:** `src/services/register.service.ts:14-18`
|
||||
|
||||
The `if (registers.length === 0)` guard is commented out, so the file is re-read and re-parsed on every call to `getRegisters()`. This means every register page load (including the OG image generator and `generateMetadata`) re-parses the entire `nextreg.txt` file. The `[hex]/page.tsx` calls `getRegisters()` twice (once in `generateMetadata`, once in the page function), reading the file from disk twice per request.
|
||||
|
||||
**Fix:** Uncomment the caching guard, or better yet, wrap with React `cache()` for request-level deduplication.
|
||||
|
||||
### 🔒 Security: `middleware.js` is untyped JavaScript
|
||||
|
||||
**File:** `src/middleware.js`
|
||||
|
||||
The only `.js` file in the project. It logs every request path to stdout, including potentially sensitive paths. In production this creates noise and potential log injection vectors.
|
||||
|
||||
**Fix:** Convert to TypeScript (`.ts`). Consider restricting logging to development only, or removing it entirely since Next.js has built-in request logging.
|
||||
|
||||
---
|
||||
|
||||
## 🟠 High Priority
|
||||
|
||||
### 🧱 Architecture: Duplicated type definitions across files
|
||||
|
||||
`Paged<T>`, `Item`, `SearchScope`, `EntryFacets`, and similar types are independently re-declared in:
|
||||
- `src/hooks/useSearchFetch.ts`
|
||||
- `src/app/zxdb/entries/EntriesExplorer.tsx`
|
||||
- `src/app/zxdb/releases/ReleasesExplorer.tsx`
|
||||
- `src/app/zxdb/labels/LabelsSearch.tsx`
|
||||
- `src/app/zxdb/genres/GenresSearch.tsx`
|
||||
|
||||
And the `EntryDetailData` type in `EntryDetail.tsx` is a near-duplicate of `EntryDetail` in `src/server/repo/zxdb.ts`.
|
||||
|
||||
**Fix:** Extract shared types to `src/types/zxdb.ts` and import everywhere.
|
||||
|
||||
### 🧱 Architecture: Duplicated `parseMachineIds` / `parseIdList` helpers
|
||||
|
||||
The same ID-parsing logic appears in:
|
||||
- `src/app/zxdb/entries/page.tsx`
|
||||
- `src/app/zxdb/entries/EntriesExplorer.tsx`
|
||||
- `src/app/zxdb/releases/page.tsx`
|
||||
- `src/app/zxdb/releases/ReleasesExplorer.tsx`
|
||||
- `src/app/api/zxdb/search/route.ts`
|
||||
|
||||
**Fix:** Extract to a shared utility (e.g., `src/utils/params.ts`).
|
||||
|
||||
### 🧱 Architecture: Duplicated `buildRegisterSummary` logic
|
||||
|
||||
`[hex]/page.tsx` and `[hex]/opengraph-image.tsx` each have their own version of register-summary-building logic (one returns a string, one returns lines). The `isInfoLine` filter is duplicated.
|
||||
|
||||
**Fix:** Extract to a shared utility in `src/utils/register_helpers.ts`.
|
||||
|
||||
### ⚡ Performance: Repository file is 800+ lines with no code splitting
|
||||
|
||||
**File:** `src/server/repo/zxdb.ts` (31,000+ tokens)
|
||||
|
||||
This monolithic file contains all DB queries. It's hard to navigate and cannot benefit from tree-shaking at the module level.
|
||||
|
||||
**Fix:** Split into per-domain files: `repo/entries.ts`, `repo/labels.ts`, `repo/releases.ts`, `repo/magazines.ts`, `repo/lookups.ts`, etc.
|
||||
|
||||
### ⚡ Performance: `releases/page.tsx` uses `JSON.parse(JSON.stringify(...))` for serialization
|
||||
|
||||
**File:** `src/app/zxdb/releases/page.tsx:51-63`
|
||||
|
||||
Uses `JSON.parse(JSON.stringify(initial))` to strip non-serializable values. This is a known workaround for Drizzle decimal types. However, it's applied 7 times in the same function.
|
||||
|
||||
**Fix:** Create a `serialize()` helper, or configure Drizzle's `decimal` columns to return strings/numbers natively.
|
||||
|
||||
### 🎨 UI Consistency: Mixed raw HTML and react-bootstrap components
|
||||
|
||||
Per `CLAUDE.md`, the project should always use react-bootstrap components. Several pages use raw HTML instead:
|
||||
|
||||
| File | Issue |
|
||||
|------|-------|
|
||||
| `LabelsSearch.tsx` | Raw `<table>`, `<input>`, `<button>`, `<form>` instead of `Table`, `Form.*`, `Button` |
|
||||
| `LabelDetail.tsx` | Raw `<table>`, `<input>`, `<button>`, nav tabs using raw `<ul>/<li>/<button>` |
|
||||
| `GenresSearch.tsx` | Raw `<table>`, `<input>`, `<button>` |
|
||||
| `MachineTypesSearch.tsx` | Likely same pattern |
|
||||
| `LanguagesSearch.tsx` | Likely same pattern |
|
||||
| `ReleaseDetail.tsx` | Raw `<table>` throughout instead of `<Table>` |
|
||||
| `zxdb/page.tsx` | Raw `<input>`, `<select>`, `<button>` in the search form |
|
||||
| `magazines/page.tsx` | Raw `<table>`, local `Pagination` component instead of shared one |
|
||||
| `magazines/[id]/page.tsx` | Raw `<table>` |
|
||||
| `issues/[id]/page.tsx` | Raw `<table>` |
|
||||
| `page.tsx` (home) | Raw `<div className="card">` instead of `<Card>` |
|
||||
| `TapeIdentifier.tsx` | Raw `<table>` for hash display |
|
||||
|
||||
**Fix:** Systematically replace with react-bootstrap equivalents to match `EntriesExplorer.tsx` and `RegisterBrowser.tsx` patterns.
|
||||
|
||||
### 🎨 UI: Magazines page has its own inline `Pagination` component
|
||||
|
||||
**File:** `src/app/zxdb/magazines/page.tsx:89-117`
|
||||
|
||||
Defines a local `Pagination` function instead of using the shared `src/components/explorer/Pagination.tsx`.
|
||||
|
||||
**Fix:** Use the shared `Pagination` component.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Medium Priority
|
||||
|
||||
### ⚛️ React: `useSearchFetch` `onExtra` callback may cause infinite loops
|
||||
|
||||
**File:** `src/hooks/useSearchFetch.ts:75`
|
||||
|
||||
The `fetch_` callback depends on `[endpoint, onExtra]`. If the caller doesn't memoize `onExtra`, this dependency changes every render, creating a new `fetch_` reference, which could cascade into effect re-runs.
|
||||
|
||||
The `EntriesExplorer.tsx` correctly `useCallback`-wraps `handleExtra`, but this is a fragile contract.
|
||||
|
||||
**Fix:** Store `onExtra` in a ref instead of including it in the dependency array, or document the requirement clearly.
|
||||
|
||||
### ⚛️ React: Missing `notFound()` call in entry detail page
|
||||
|
||||
**File:** `src/app/zxdb/entries/[id]/page.tsx:13-15`
|
||||
|
||||
When `getEntryById` returns null, the page still renders with status 200 — the client component shows an "alert-warning" div. This means:
|
||||
- Search engines index a 200 page with "Not found" content.
|
||||
- No proper 404 HTTP status.
|
||||
|
||||
**Fix:** Call `notFound()` in the server component when `data` is null, like `magazines/[id]/page.tsx` does.
|
||||
|
||||
### ⚛️ React: Same issue for release detail page
|
||||
|
||||
**File:** `src/app/zxdb/releases/[entryId]/[releaseSeq]/page.tsx`
|
||||
|
||||
Same pattern — no `notFound()` when `data` is null.
|
||||
|
||||
### ⚛️ React: `RegisterBrowser` disables exhaustive-deps without justification
|
||||
|
||||
**File:** `src/app/registers/RegisterBrowser.tsx:90-91`
|
||||
|
||||
The `eslint-disable-next-line react-hooks/exhaustive-deps` on the `searchParams` sync effect excludes `searchTerm` from deps, which could lead to stale closures if `searchParams` changes while `searchTerm` is mid-update.
|
||||
|
||||
### 📦 Metadata: Entry/release/label detail pages lack dynamic metadata
|
||||
|
||||
**Files:**
|
||||
- `src/app/zxdb/entries/[id]/page.tsx` — static `metadata = { title: "ZXDB Entry" }`
|
||||
- `src/app/zxdb/releases/[entryId]/[releaseSeq]/page.tsx` — static `metadata = { title: "ZXDB Release" }`
|
||||
- `src/app/zxdb/labels/[id]/page.tsx` — static `metadata = { title: "ZXDB Label" }`
|
||||
|
||||
These should use `generateMetadata` to include the entry/release/label title for SEO and social sharing, similar to how `registers/[hex]/page.tsx` does it.
|
||||
|
||||
### 🧱 Architecture: `ThemeDropdown` hardcodes cookie domain
|
||||
|
||||
**File:** `src/components/ThemeDropdown.tsx:16`
|
||||
|
||||
```typescript
|
||||
document.cookie = `${name}=...; Domain=specnext.dev`;
|
||||
```
|
||||
|
||||
This hardcoded domain means the theme cookie won't work on `localhost` or any non-specnext.dev domain during development.
|
||||
|
||||
**Fix:** Remove the `Domain=` attribute (let it default to current host), or conditionally set it based on environment.
|
||||
|
||||
### ⚡ Performance: No `loading.tsx` or `Suspense` boundaries
|
||||
|
||||
None of the route segments define `loading.tsx` files. For `force-dynamic` pages (entries, releases, labels, genres, languages, machinetypes), users see a blank page or frozen UI while the server fetches from MySQL.
|
||||
|
||||
**Fix:** Add `loading.tsx` with skeleton/spinner states for ZXDB routes.
|
||||
|
||||
### ⚡ Performance: `opengraph-image.tsx` calls `getRegisters()` for every OG image
|
||||
|
||||
**File:** `src/app/registers/[hex]/opengraph-image.tsx:132-136`
|
||||
|
||||
Loads ALL registers from disk just to find one. With the caching fix above this becomes a non-issue, but currently it's reading and parsing the full file for each image request.
|
||||
|
||||
### 🔒 Security: Download API path traversal protection should normalize before joining
|
||||
|
||||
**File:** `src/app/api/zxdb/download/route.ts:27-28`
|
||||
|
||||
The current protection `path.normalize(path.join(baseDir, filePath))` is correct, but the check should also reject paths containing `..` before `join` for defense-in-depth.
|
||||
|
||||
### 📝 DX: `parseNextReg()` is async but does no async work
|
||||
|
||||
**File:** `src/utils/register_parser.ts:49`
|
||||
|
||||
`parseNextReg()` is declared `async` and returns `Promise<Register[]>`, but the function body is entirely synchronous. This forces callers to `await` unnecessarily.
|
||||
|
||||
**Fix:** Remove `async` and return `Register[]` directly.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Low Priority
|
||||
|
||||
### 🎨 UI: `Navbar` uses `Link` with `className="nav-link"` instead of `Nav.Link`
|
||||
|
||||
**File:** `src/components/Navbar.tsx:14-16`
|
||||
|
||||
For consistency with react-bootstrap patterns, use `<Nav.Link as={Link} href="...">` instead of raw `<Link className="nav-link">`.
|
||||
|
||||
### 🎨 UI: Home page uses `bi bi-*` CSS classes instead of react-bootstrap-icons
|
||||
|
||||
**File:** `src/app/page.tsx:12,29`
|
||||
|
||||
Uses `<span className="bi bi-collection">` instead of the `react-bootstrap-icons` package that's used elsewhere.
|
||||
|
||||
### 🎨 UI: `TapeIdentifier` uses `bi bi-*` CSS classes
|
||||
|
||||
**File:** `src/app/zxdb/TapeIdentifier.tsx`
|
||||
|
||||
Same issue — uses Bootstrap icon CSS classes instead of `react-bootstrap-icons` components.
|
||||
|
||||
### 🎨 UI: Inconsistent "not found" patterns
|
||||
|
||||
Some pages use `notFound()` (magazines, issues), others render inline alerts (entries, releases, labels). This creates inconsistent UX.
|
||||
|
||||
### ⚛️ React: `buildRegisterSummaryLines` in OG image could use better key strategy
|
||||
|
||||
**File:** `src/app/registers/[hex]/opengraph-image.tsx:174,188`
|
||||
|
||||
Uses `key={line}` which will produce duplicate keys if two lines have identical text.
|
||||
|
||||
### 📝 DX: Commented-out code in register parsers
|
||||
|
||||
**Files:**
|
||||
- `src/utils/register_parsers/reg_default.ts:13,106,122-126` — commented `footnoteTarget` variable and `valueMatch` block
|
||||
- `src/services/register.service.ts:14,18` — commented caching guard
|
||||
|
||||
**Fix:** Remove dead code or convert to tracked TODOs.
|
||||
|
||||
### 📝 DX: `app/page.module.css` exists but is never imported
|
||||
|
||||
Check if this file has any content; if empty/unused, remove it.
|
||||
|
||||
### 🧪 Testing: Zero test coverage
|
||||
|
||||
No test files exist (`*.test.ts`, `*.spec.ts`, `__tests__/`). Key areas that would benefit:
|
||||
1. Register parser (`parseNextReg`, `parseDescriptionDefault`, `parseDescriptionF0`) — complex parsing logic with edge cases.
|
||||
2. API route input validation (Zod schemas).
|
||||
3. `useSearchFetch` hook behavior (cancellation, race conditions).
|
||||
4. `computeMd5` correctness.
|
||||
5. Component rendering for entry detail, release detail.
|
||||
|
||||
### ♿ Accessibility: Search inputs lack proper labeling
|
||||
|
||||
Several search inputs use `placeholder` as the only label (no associated `<label>` or `aria-label`):
|
||||
- `zxdb/page.tsx` search form
|
||||
- Various filter sidebars
|
||||
|
||||
### ♿ Accessibility: Tables lack `<caption>` elements
|
||||
|
||||
Data tables throughout the ZXDB explorer have no `<caption>`, making it harder for screen readers to understand table purpose.
|
||||
|
||||
### ⚡ Performance: `EntriesExplorer` and `ReleasesExplorer` have very similar structures
|
||||
|
||||
Both follow the same pattern: sidebar with filters, table results, pagination. They share about 60% structural similarity. Consider extracting a shared `SearchExplorer` wrapper that accepts column definitions and filter config.
|
||||
|
||||
### 📦 Bundle: `import * as Icon from 'react-bootstrap-icons'`
|
||||
|
||||
**File:** `src/app/registers/RegisterDetail.tsx:8`, `src/components/ThemeDropdown.tsx:4`
|
||||
|
||||
Importing the entire icon library. While tree-shaking should handle this, named imports are safer and make dependencies explicit.
|
||||
|
||||
**Fix:** `import { Wikipedia, Link45deg, CodeSlash } from 'react-bootstrap-icons'`
|
||||
|
||||
### 📝 DX: `CLAUDE.md` structure tree is outdated
|
||||
|
||||
The project tree in `CLAUDE.md` doesn't reflect the current file structure (missing `hooks/`, `components/explorer/`, ZXDB pages, API routes, `server/`, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Feature Ideas (non-bugs)
|
||||
|
||||
- **Search debouncing** — `RegisterBrowser` updates the URL on every keystroke. Consider debouncing the URL update (keep instant local filtering).
|
||||
- **Entry detail OG images** — Register pages have OG images; ZXDB entry pages do not.
|
||||
- **Keyboard navigation** — Add keyboard shortcuts for pagination (left/right arrows).
|
||||
- **Back-to-top button** — Long entry detail pages would benefit.
|
||||
- **Error boundaries** — No `error.tsx` files exist for graceful error recovery in route segments.
|
||||
- **Rate limiting** — API routes have no rate limiting for the search endpoints.
|
||||
Reference in New Issue
Block a user