尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Personal VHS collection manager backed by PostgreSQL/Neon. Scan tapes with barcode or AI vision, track condition and value, and manage your collection with optional serverless deployment.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗A personal tool to catalog a VHS collection — capturing what each tape is, what it might be worth, and building a record you can actually use (sell, store, share). Live at vhs.nitsuah.io. Backed by PostgreSQL, served by Express, containerized with Docker.
public/) served via Express (src/server.js)VHS-XXXX IDs; REST CRUD at /api/tapesin_collection, for_sale, sold, donated, missing, wanted; great/good/fair/poor with notesfor_sale tapes; copy-ready title, description, and suggested price per tapefor_sale tapesGOOGLE_CLIENT_ID blank to run as single-user/c/<uuid> URL per usernpm run test:journeys (Docker: see tests/journeys/README.md); visual baselines per step, failures filed as bot:journey issues.github/workflows/visual-docs.yml (shown below)src/server.js + src/modules/**)node --check syntax, dep-install check, Jest unit tests, Docker build smoke testsite/ at nitsuah.github.io/vhsnetlify/functions/server.js, serverless-http)Architecture — docs/diagrams/architecture.svg · source docs/diagrams/architecture.mmd
Capture flow — docs/diagrams/capture-flow.svg · source docs/diagrams/capture-flow.mmd
Job lifecycle — docs/diagrams/job-lifecycle.svg · source docs/diagrams/job-lifecycle.mmd
Ai photo scanning — docs/screenshots/ai-photo-scanning.png
Collection first default view — docs/screenshots/collection-first-default-view.png
Condition tracking — docs/screenshots/condition-tracking.png
Csv export — docs/screenshots/csv-export.png
Full text search — docs/screenshots/full-text-search.png
Hamburger drawer — docs/screenshots/hamburger-drawer.png
Mobile ui — docs/screenshots/mobile-ui.png
Sort — docs/screenshots/sort.png
Sortable resizable wall table columns — docs/screenshots/sortable-resizable-wall-table-columns.png
Tabbed edit form — docs/screenshots/tabbed-edit-form.png
Wall view — docs/screenshots/wall-view.png
cp .env.example .env # fill in DATABASE_URL and HOST_IP at minimum
docker compose -f config/docker-compose.yml up -d --build
App at http://localhost:8080 (or whatever APP_PORT you set in .env).
HTTPS at https://localhost:8443.
The Express app can also run as a Netlify Function. The serverless-http package wraps it as a Lambda-compatible handler. Static files are served from public/; all /api/* and /auth/* routes are proxied to the function via netlify.toml.
# Deploy via Netlify CLI
netlify deploy --prod
Migrations run automatically on each cold start (idempotent). Set the same .env variables (especially DATABASE_URL) as Netlify environment variables.
Note: The Ollama proxy and local HTTPS cert generation are Docker-only features. The Netlify path assumes Claude Vision API (
ANTHROPIC_API_KEY) for AI scanning.
Auth is disabled by default — the app runs as a single-user tool with no login required.
To enable Google OAuth:
http://localhost:8080/auth/google/callback (or your APP_BASE_URL).env:
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
JWT_SECRET=<run: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))">
APP_BASE_URL=http://localhost:8080
To migrate existing un-owned tapes to your Google account on first login:
[email protected]
Mobile browsers block camera access on plain HTTP. The app auto-generates a self-signed TLS cert on first boot and serves it for easy installation.
One-time setup per device:
HOST_IP=<your LAN IP> in .env (e.g. HOST_IP=192.168.1.171)docker compose -f config/docker-compose.yml up -d --builddocker compose -f config/docker-compose.yml exec web openssl x509 -in /app/certs/ca.crt -noout -fingerprint -sha256
http://192.168.1.171:8080/api/ca-certhttps://192.168.1.171:8443 on your phone — camera will workIf your IP changes:
docker volume rm vhs_certs # forces cert regeneration with new IP on next start
Note: The cert is self-signed by a local CA that only your devices trust. Traffic never leaves your LAN. The cert lasts 10 years.
vhs/
├── config/
│ └── docker-compose.yml
├── docs/ ← CHANGELOG, FEATURES, ROADMAP, TASKS
├── migrations/ ← SQL schema migrations (run on startup)
├── public/ ← browser app static files
│ └── js/ ← ES-module frontend
│ └── modules/ ← sub-modules (list-view, wall-view, etc.)
├── src/
│ ├── server.js ← Express entry point
│ └── modules/ ← auth, config, routes
├── tests/ ← Jest unit tests
├── tests_playwright/ ← Playwright E2E tests
├── .env.example ← copy to .env and fill in
└── Dockerfile
Each tape is one row in the PostgreSQL tapes table. The row schema is:
| SQL Column | Type | Notes |
|---|---|---|
id | TEXT | Immutable identifier (primary key); VHS-XXXX format when assigned at creation, or the scanned UPC barcode if a barcode is captured before the VHS-XXXX is assigned |
data | JSONB | Full tape object — all metadata fields live here |
scanned_at | TEXT | ISO timestamp; default sort key |
owner_id | TEXT | Google account sub; NULL in single-user mode |
All tape metadata is stored inside the data JSONB column as a single document. Key fields within that document:
| Field | Notes |
|---|---|
id | Mirrors the SQL id; VHS-XXXX or barcode UPC when valid |
title | Required; search, sort, display, lookup key for Fill |
year | OMDb authoritative source; fill target |
label | VHS distributor/studio; e.g. Paramount Home Video |
format | VHS, DVD, Blu-ray, etc. |
condition | great, good, fair, poor |
condition_notes | Free text; detail modal only |
status | in_collection, for_sale, sold, donated, missing, wanted |
tags | Array of genre/custom tag strings |
value_low / value_high | USD resale estimate range |
sold_price | Actual sale price (shown when status = sold) |
barcode | UPC/EAN scan identifier; dedup guard |
imdb_id | OMDb confidence gate for Fill |
photos | Array of base64-encoded compressed images (max 1200px, JPEG 0.75) |
photo_thumbnail | Auto-set on confirm; universal thumbnail fallback |
photo_face | User-pinned in detail modal; cover-wall primary |
photo_spine | User-pinned in detail modal; spine-wall primary |
notes | Searchable free-text field |
Immutable IDs: once a tape gets a VHS-XXXX ID, it keeps it forever. If a barcode is scanned first and no VHS-XXXX exists yet, the UPC becomes the id if it is not already in use.
tapes.json when multi-user auth arrived. PostgreSQL via Neon handles concurrent writes, upserts, and user-scoped queries cleanly. The ID scheme (VHS-XXXX) is still immutable. (data/tapes.json remains in the repo as an empty legacy file from the initial version.)config/docker-compose.yml) and Netlify Functions (see netlify/functions/server.js and netlify.toml). The serverless-http package wraps the Express app as a Lambda-compatible handler.GOOGLE_CLIENT_ID is not set, the app runs with no login wall and all tapes are global."great". Notes field for anything specific.status field handles everything: in_collection, for_sale, sold, donated. No archive table needed.Every doc at the repo root (other than this README) and under docs/ (the files mirrored into the Obsidian vault), so none of them is orphaned.
docs/CHANGELOG.mddocs/EASTEREGG.mddocs/FEATURES.mddocs/METRICS.mddocs/ROADMAP.mddocs/TASKS.mddocs/system-flow.md