2026-07-26 17:45:58 -05:00
2026-07-26 17:45:58 -05:00
2026-07-23 17:48:44 -05:00
2026-07-26 17:45:58 -05:00
2026-07-23 17:48:44 -05:00
2026-07-26 17:45:58 -05:00
2026-07-26 17:45:58 -05:00
2026-07-23 17:48:44 -05:00
2026-07-26 17:45:58 -05:00
2026-07-23 17:48:44 -05:00
2026-07-23 17:48:44 -05:00
2026-07-23 17:48:44 -05:00
2026-07-23 17:48:44 -05:00
2026-07-26 17:45:58 -05:00
2026-07-23 17:48:44 -05:00

BizMatch Phase 2 — Broker Workflow App

Module 1: foundation (Docker Compose, PostgreSQL, schema, migrations, login). Module 2: business scan (NAS -> DB) and the first UI. Module 3: recursive file listing, PDF streaming from the NAS and the ported pdf.js viewer.

The UI and all domain constants are English.

Setup on 192.168.100.99 (Ubuntu 24.04)

Prerequisite: Docker + Compose plugin (sudo apt install docker.io docker-compose-v2).

# Unpack the project, then:
cd bizmatch-app
cp .env.example .env          # optionally adjust DB_PASSWORD
docker compose up -d --build

The app applies all migrations on start and then listens on http://192.168.100.99:8090.

Upgrading from Module 1: 001_init.sql was rewritten in place (German constants -> English). Applying it needs a fresh database: docker compose down -v && docker compose up -d --build. The DB held no production data yet, so there is nothing to migrate.

First smoke test:

curl http://localhost:8090/api/health
# -> {"ok":true}

# Create the three staff members (adjust the names):
curl -X POST localhost:8090/api/staff -H 'content-type: application/json' -d '{"name":"Chris"}'
curl -X POST localhost:8090/api/staff -H 'content-type: application/json' -d '{"name":"..."}'

Dev mode (without the app container)

docker compose up -d db       # database only
npm install
set -a; source .env; set +a
npm run dev                   # tsx watch, migrations run on start

# second terminal — frontend with hot reload, /api is proxied to :8090
cd web && npm install && npm run dev

NAS mount

Mount it on the host via NFS, e.g. in /etc/fstab:

<truenas-ip>:/mnt/<pool>/bizmatch  /mnt/bizmatch-nas  nfs  ro,soft,timeo=100  0  0

The compose file already passes NAS_ROOT (default /mnt/bizmatch-nas) into the app container. Write access (NDA filing) comes in module 6 — then replace ro with rw and limit the permissions to the two write paths.

Business directories

Directly below NAS_ROOT there are three status directories; every immediate subdirectory of those is one business. The directory names are configurable (they contain spaces and are treated as opaque strings):

Env var Default Business status
NAS_DIR_ACTIVE AAA = ACTIVE ACTIVE
NAS_DIR_SOLD AAA = SOLD SOLD
NAS_DIR_INACTIVE AAA = INACTIVE INACTIVE

The scan is idempotent: existing rows are matched by name and only updated when nas_path or status changed. Businesses that exist in the DB but no longer on disk are kept and only reported as a warning in the log. A missing configured directory aborts the scan with an error naming the path.

Moving to the AI machine (later)

  1. docker compose down on .99
  2. Take a dump: docker compose exec db pg_dump -U bizmatch bizmatch > backup.sql
  3. Copy the project folder to the AI machine, run docker compose up -d --build
  4. Restore the dump: docker compose exec -T db psql -U bizmatch bizmatch < backup.sql

API (as of module 3)

Method Path Purpose Session
GET /api/health liveness incl. DB check no
GET /api/staff staff list no
POST /api/staff create staff member {name} no
POST /api/login login via {staff_id} → session cookie no
GET /api/me signed-in staff member yes
POST /api/logout sign out yes
POST /api/businesses/scan scan the NAS → {scanned, inserted, updated, missing} yes
GET /api/businesses list ?status=&search= + counts per status yes
GET /api/businesses/:id single business incl. nas_path yes
GET /api/businesses/:id/files recursive listing, max depth 3 (PDFs first) yes
GET /api/businesses/:id/file stream one file, ?path=<relative> yes

Everything except health, staff (GET+POST) and login requires the session cookie; without it the API answers 401.

File listing and streaming

/files walks the business directory recursively (max depth 3), skipping dotfiles, dot-directories and symlinks, and returns { path, size, mtime } with path relative to the business directory and always posix-separated. PDFs come first, then everything else, each group alphabetical.

/file?path=… streams one of those files straight from disk (createReadStream, never buffered):

  • the path is resolved against nas_path and then realpath-validated to be inside realpath(business dir). Absolute paths, .., leading dots, empty paths and symlinks pointing out of the tree get 400; a missing file 404.
  • single-range HTTP Range requests answer 206 with Content-Range, unsatisfiable ones 416.
  • ETag is derived from mtime + size, If-None-Match answers 304.
  • .pdf is served as application/pdf (inline), anything else as application/octet-stream with Content-Disposition: attachment.

Frontend

web/ is a Vite + React + TypeScript app with Tailwind v4 (no router, no state library). Views: login ("Who is working?"), business list (tabs with counts, search, "Scan NAS now") and business detail — a master-detail split filling the viewport: file table left, PDF viewer right.

In dev, Vite proxies /api to http://localhost:8090. In production the Fastify app serves web/dist via @fastify/static with an SPA fallback to index.html for all non-/api routes; the Dockerfile builds the frontend in its own stage and copies web/dist into the runtime image.

PDF viewer

The viewer is the proven one from the phase-1 Deno desktop app (see viewer-phase1/), ported nearly byte-identical. It lives in web/public/viewer/ as plain, unbundled ES modules — Vite serves public/ as-is, so the same files work in dev and prod. The React app embeds it in an <iframe>:

/viewer/index.html?file=<urlencoded /api/businesses/:id/file?path=...>

The page refuses any file value that is not a root-relative /api/ path, and the iframe is same-origin, so the normal session cookie authenticates it.

web/public/pdfjs/ holds the pdf.js runtime, copied out of node_modules/pdfjs-dist (pinned to exactly 6.1.200) by web/scripts/copy-pdfjs.mjs, which runs on predev and prebuild — also inside the Docker web stage. The directory is generated and git-ignored:

web/public/pdfjs/legacy/         pdf.min.mjs + pdf.worker.min.mjs
web/public/pdfjs/wasm/           CCITT-G4/JBIG2, JPEG2000 and ICC decoders
web/public/pdfjs/standard_fonts/ standardFontDataUrl
web/public/pdfjs/iccs/           iccUrl

The wasm/ directory is what makes scanned B/W pages render at all; without it pdf.js fails the decoders silently and shows blank white canvases.

Structure

migrations/   numbered SQL migrations (001_init.sql = full schema)
src/
  config.ts           env configuration
  db.ts               pg pool + query helpers
  migrate.ts          migration runner (transactional, advisory lock)
  business-scan.ts    NAS scan, recursive listing, safe file path resolution
  server.ts           Fastify app (health, staff, login, businesses, file, static)
web/
  scripts/copy-pdfjs.mjs  pdfjs-dist -> public/pdfjs/ (predev + prebuild)
  public/viewer/      standalone, unbundled pdf.js viewer page
  public/pdfjs/       generated, git-ignored pdf.js runtime
  src/api.ts          typed API client
  src/App.tsx         session gate + view switch
  src/views/          Login, Businesses, BusinessDetail
viewer-phase1/        reference copy of the phase-1 desktop viewer
Description
No description provided
Readme 322 KiB
Languages
TypeScript 78.2%
JavaScript 16.4%
PLpgSQL 2.4%
CSS 1.9%
HTML 0.9%
Other 0.2%