docs: rewrite README for current feature set
Lead with a user-facing summary, group features by domain (inventory, field workflow, operations dashboard, integrations), add the docker-compose deployment path, and drop the stale 2.0 phase roadmap and deferred-followups lists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,50 @@
|
|||||||
# Vector
|
# Vector
|
||||||
|
|
||||||
Hardware parts inventory system. Tracks serialized parts across sites → rooms → bins and hosts (with externally-driven state/stack lifecycle), with a full audit trail, repair/RMA workflow, per-tech custody for broken-part holds and pre-staged spares, tag-based organization, category-per-model taxonomy, manufacturer EOL tracking, and signed webhook delivery for external integrations.
|
Serialized hardware inventory for engineering fleets. Track every part from
|
||||||
|
receiving → bin → host → repair → destruction, with a complete per-part audit
|
||||||
|
trail, field-maintenance ticketing, per-technician custody, and manufacturer
|
||||||
|
EOL visibility.
|
||||||
|
|
||||||
Vector 2.0 is a ground-up TypeScript rewrite of the original JavaScript codebase, delivered as a pnpm + Turbo monorepo with shadcn/ui on the frontend and a service-layered Express API on the backend.
|
Vector is a pnpm + Turborepo monorepo written in strict TypeScript end to end:
|
||||||
|
an Express/Prisma API, a React/Vite web client, shared zod contracts, and a
|
||||||
|
shadcn/ui design system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
**Inventory**
|
||||||
|
|
||||||
|
- Serialized `Part`s live under `Site → Room → Bin` locations or on a `Host`.
|
||||||
|
- Every state change writes a `PartEvent` in the same transaction — nothing
|
||||||
|
mutates silently.
|
||||||
|
- `PartModel`s are grouped by `Manufacturer` and `Category`, with optional EOL
|
||||||
|
dates that feed dashboard risk widgets.
|
||||||
|
- `Tag`s and `SavedView`s for cross-cutting organization and reusable filters.
|
||||||
|
|
||||||
|
**Field workflow**
|
||||||
|
|
||||||
|
- `FM` tickets track active issues per host, with attached repairs and close
|
||||||
|
times.
|
||||||
|
- `Repair` logs pair a broken serial with its replacement and auto-ingest parts
|
||||||
|
from a tech's input when the broken serial isn't yet in Vector.
|
||||||
|
- Per-technician **custody** tracks parts held for repair, pending drop-off, or
|
||||||
|
pending destruction — so broken or pre-staged parts never go missing between
|
||||||
|
events.
|
||||||
|
|
||||||
|
**Operations dashboard**
|
||||||
|
|
||||||
|
- Universal KPIs: total parts, total spent, deployed value, open FMs,
|
||||||
|
past-EOL deployments, upcoming-EOL deployments (≤180 d).
|
||||||
|
- Admin-only: 7/30-day repair tempo, FMs opened, average FM close time, repairs
|
||||||
|
trend chart, open-FMs-by-host, and a custody backlog by user.
|
||||||
|
|
||||||
|
**Integrations**
|
||||||
|
|
||||||
|
- Outbound webhooks signed with `HMAC-SHA256(timestamp + "." + body)`, headers
|
||||||
|
`x-vector-signature`, `x-vector-timestamp`, `x-vector-event`, plus a
|
||||||
|
`x-vector-webhook: v1` recursion guard.
|
||||||
|
- Streaming CSV export of the audit log for admins.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,20 +53,24 @@ Vector 2.0 is a ground-up TypeScript rewrite of the original JavaScript codebase
|
|||||||
```
|
```
|
||||||
vector/
|
vector/
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── api/ Express 5 + Prisma + zod. Controllers → services → tx.
|
│ ├── api/ Express 5 + Prisma + zod. controllers → services → tx.
|
||||||
│ ├── web/ React 19 + Vite + TanStack Query/Table + shadcn/ui.
|
│ ├── web/ React 19 + Vite + TanStack Query/Table + shadcn/ui.
|
||||||
│ └── e2e/ Playwright smoke tests (login, parts, repairs, admin).
|
│ └── e2e/ Playwright smoke tests (login, parts, repairs, admin).
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── db/ Prisma schema, migrations, seed, singleton client.
|
│ ├── db/ Prisma schema, migrations, seed, singleton client.
|
||||||
│ ├── shared/ zod schemas + DTOs — single source of truth for the API contract.
|
│ ├── shared/ zod schemas + DTOs — the source of truth for the API.
|
||||||
│ ├── ui/ shadcn primitives + Vector design tokens.
|
│ ├── ui/ shadcn primitives + Vector design tokens.
|
||||||
│ └── config/ Shared tsconfig presets + Tailwind preset.
|
│ └── config/ Shared tsconfig + Tailwind tokens.
|
||||||
├── .gitea/workflows/ CI (lint · typecheck · test · build) and gated E2E job.
|
├── .gitea/workflows/ci.yaml lint · typecheck · test · build, gated E2E job.
|
||||||
├── docker-compose.yml Postgres + Redis for local development.
|
├── docker-compose.yml api + web + redis (production-style stack).
|
||||||
└── turbo.json Pipeline: `build`, `test`, `typecheck`, `lint`.
|
└── turbo.json dev · build · test · typecheck · lint.
|
||||||
```
|
```
|
||||||
|
|
||||||
The API is split along a strict controller → service → transaction boundary. Every service function takes `(tx, input, actor)` so controllers can compose multiple services inside a single `prisma.$transaction`, guaranteeing that part mutations and their `PartEvent` audit rows are atomic.
|
The API is strict about its boundaries. Every service takes `(tx, input, actor)`
|
||||||
|
so controllers can compose multiple services inside a single
|
||||||
|
`prisma.$transaction` — part mutations and their audit rows are always atomic
|
||||||
|
or not at all. Controllers do no validation and no business logic; they parse
|
||||||
|
through a zod schema and dispatch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,100 +78,100 @@ The API is split along a strict controller → service → transaction boundary.
|
|||||||
|
|
||||||
| Layer | Choice |
|
| Layer | Choice |
|
||||||
| -------------- | ---------------------------------------------------------------------- |
|
| -------------- | ---------------------------------------------------------------------- |
|
||||||
| Language | TypeScript strict mode across all workspaces |
|
| Language | TypeScript strict mode across every workspace |
|
||||||
| API | Express 5, Prisma 5, zod, JWT + refresh-token rotation, CSRF, helmet |
|
| API | Express 5, Prisma 5, zod, JWT + refresh-token rotation, CSRF, helmet |
|
||||||
| Web | React 19, Vite, TanStack Query, TanStack Table, react-hook-form, nuqs |
|
| Web | React 19, Vite, TanStack Query, TanStack Table, react-hook-form, nuqs |
|
||||||
| UI | shadcn/ui on Radix primitives, Tailwind v4, Sonner toasts, Recharts |
|
| UI | shadcn/ui on Radix primitives, Tailwind v4, Sonner toasts, Recharts |
|
||||||
| Database | SQLite for dev (Postgres cutover prepared — schema is portable) |
|
| Database | SQLite (single-file, shipped in prod via Docker volume) |
|
||||||
| Observability | pino (JSON logs in prod, pretty in dev) + request-scoped `requestId` |
|
| Observability | pino (JSON in prod, pretty in dev) with per-request `requestId` |
|
||||||
| Testing | Vitest (unit, coverage gate on services/lib), Playwright (E2E) |
|
| Testing | Vitest (unit), Playwright (E2E) |
|
||||||
| Monorepo tools | pnpm workspaces + Turborepo |
|
| Monorepo | pnpm workspaces + Turborepo |
|
||||||
| CI | Gitea Actions + Renovate (self-hosted) |
|
| CI / deps | Gitea Actions + self-hosted Renovate |
|
||||||
|
|
||||||
|
The schema is intentionally Postgres-portable; SQLite is the default because
|
||||||
|
Vector is deployed as a single-container stack per site.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Getting started
|
## Quick start
|
||||||
|
|
||||||
### Prerequisites
|
Prerequisites: **Node 20+**, **pnpm 10+** (`corepack enable`).
|
||||||
|
|
||||||
- Node.js ≥ 20
|
|
||||||
- pnpm ≥ 10 (`corepack enable` or `npm i -g pnpm`)
|
|
||||||
- Docker (optional; required for Postgres + Redis)
|
|
||||||
|
|
||||||
### Install
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm -C packages/db exec prisma generate
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment
|
|
||||||
|
|
||||||
Copy the API env template and generate a JWT secret:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp apps/api/.env.example apps/api/.env
|
cp apps/api/.env.example apps/api/.env
|
||||||
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" # paste into JWT_SECRET
|
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" # paste as JWT_SECRET
|
||||||
```
|
|
||||||
|
|
||||||
The default `DATABASE_URL` points at a local SQLite file. To use the containerized Postgres instead, start Docker and update the URL:
|
pnpm -C packages/db exec prisma migrate dev
|
||||||
|
pnpm -C packages/db run db:seed
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d postgres redis
|
|
||||||
# then edit apps/api/.env:
|
|
||||||
# DATABASE_URL=postgresql://vector:vector@localhost:5432/vector
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm -C packages/db exec prisma migrate dev # apply migrations
|
|
||||||
pnpm -C packages/db run db:seed # create default admin:admin user
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- API: <http://localhost:3001>
|
- API: <http://localhost:3001>
|
||||||
- Web: <http://localhost:5173> (proxies `/api` → API)
|
- Web: <http://localhost:5173> (proxies `/api` → API)
|
||||||
- Default login: `admin` / `admin` — **change immediately.**
|
- Default credentials: **`admin` / `admin`** — change them immediately.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Common tasks
|
## Deployment
|
||||||
|
|
||||||
All tasks run at the workspace root; Turbo fans them out to the right packages with caching.
|
`docker-compose.yml` runs the full stack from prebuilt images
|
||||||
|
(`vector-api`, `vector-web`, `redis`). The SQLite database lives in the
|
||||||
|
`vector-data` volume.
|
||||||
|
|
||||||
| Command | What it does |
|
```bash
|
||||||
| --------------------------------- | --------------------------------------------------------------- |
|
# 1. authenticate to the registry
|
||||||
| `pnpm dev` | Run `apps/api` and `apps/web` concurrently |
|
docker login gitea.thewrightserver.net
|
||||||
|
|
||||||
|
# 2. create a .env next to docker-compose.yml with at minimum:
|
||||||
|
# JWT_SECRET=<64-char hex>
|
||||||
|
# CLIENT_ORIGIN=https://vector.example.com
|
||||||
|
# WEB_PORT=8080
|
||||||
|
# COOKIE_SECURE=true # required once you're behind TLS
|
||||||
|
# TAG=latest # or a pinned commit SHA
|
||||||
|
|
||||||
|
docker compose pull && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Redis is included in anticipation of the BullMQ-backed webhook worker; the
|
||||||
|
in-process emitter currently used by the API doesn't depend on it yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development commands
|
||||||
|
|
||||||
|
All commands run at the workspace root; Turbo fans them out with caching.
|
||||||
|
|
||||||
|
| Command | Does |
|
||||||
|
| -------------------------------------- | ------------------------------------------------------ |
|
||||||
|
| `pnpm dev` | API + web concurrently |
|
||||||
| `pnpm build` | Build every package and app |
|
| `pnpm build` | Build every package and app |
|
||||||
| `pnpm typecheck` | `tsc --noEmit` across every workspace |
|
| `pnpm typecheck` | `tsc --noEmit` across the graph |
|
||||||
| `pnpm lint` | ESLint across every workspace |
|
| `pnpm lint` | ESLint across the graph |
|
||||||
| `pnpm test` | Run all Vitest unit test suites |
|
| `pnpm test` | Vitest across packages that define it |
|
||||||
| `pnpm -C apps/api test:coverage` | Unit tests + v8 coverage report on services/lib (report only) |
|
| `pnpm -C apps/api test:coverage` | Services/lib coverage report (no threshold gate yet) |
|
||||||
| `pnpm -C apps/e2e test` | Run Playwright smoke tests (requires stack running + creds) |
|
| `pnpm -C apps/e2e test` | Playwright, against a live stack (see below) |
|
||||||
| `pnpm -C packages/db run db:studio` | Open Prisma Studio against the current database |
|
| `pnpm -C packages/db run db:studio` | Prisma Studio |
|
||||||
| `pnpm -C packages/db run db:reset` | Drop schema, re-migrate, re-seed |
|
| `pnpm -C packages/db run db:reset` | Drop schema, migrate, seed |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
**Unit tests** live next to the code they cover (`*.test.ts`). Coverage is reported on `apps/api/src/{services,lib}/**` and uploaded by CI as an artifact — no threshold gate today; add one once service-level coverage catches up.
|
**Unit tests** live next to the code they cover (`*.test.ts`). The API covers
|
||||||
|
services, `lib/` helpers, and the analytics aggregator (tested with an
|
||||||
|
in-memory `Tx` double). Shared covers every zod contract.
|
||||||
|
|
||||||
- `packages/shared` — zod schema contracts (parts, webhooks, auth).
|
**End-to-end tests** in `apps/e2e/` run Playwright against a live stack and
|
||||||
- `apps/api` — pure helpers (`signBody`, `csvCell`, `http-error`), plus the analytics aggregator tested with an in-memory `Tx` double.
|
skip themselves unless `TEST_USERNAME` and `TEST_PASSWORD` are set:
|
||||||
|
|
||||||
**End-to-end tests** in `apps/e2e/` run against a live stack. Each spec skips itself unless `TEST_USERNAME` and `TEST_PASSWORD` are present in the environment:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # in one terminal
|
pnpm dev # terminal 1
|
||||||
|
|
||||||
TEST_USERNAME=admin TEST_PASSWORD=admin \
|
TEST_USERNAME=admin TEST_PASSWORD=admin \
|
||||||
pnpm -C apps/e2e exec playwright install --with-deps chromium
|
pnpm -C apps/e2e exec playwright install --with-deps chromium
|
||||||
|
|
||||||
TEST_USERNAME=admin TEST_PASSWORD=admin \
|
TEST_USERNAME=admin TEST_PASSWORD=admin \
|
||||||
pnpm -C apps/e2e test
|
pnpm -C apps/e2e test
|
||||||
```
|
```
|
||||||
@@ -136,7 +182,7 @@ Reports land in `apps/e2e/playwright-report/`.
|
|||||||
|
|
||||||
## Continuous integration
|
## Continuous integration
|
||||||
|
|
||||||
CI runs on Gitea Actions — [.gitea/workflows/ci.yaml](.gitea/workflows/ci.yaml). Every push and pull request executes:
|
`.gitea/workflows/ci.yaml` runs on every push and PR:
|
||||||
|
|
||||||
1. `pnpm install --frozen-lockfile`
|
1. `pnpm install --frozen-lockfile`
|
||||||
2. `prisma generate`
|
2. `prisma generate`
|
||||||
@@ -144,49 +190,26 @@ CI runs on Gitea Actions — [.gitea/workflows/ci.yaml](.gitea/workflows/ci.yaml
|
|||||||
4. `pnpm typecheck`
|
4. `pnpm typecheck`
|
||||||
5. `pnpm -C packages/shared test` + `pnpm -C apps/api test:coverage`
|
5. `pnpm -C packages/shared test` + `pnpm -C apps/api test:coverage`
|
||||||
6. `pnpm build`
|
6. `pnpm build`
|
||||||
7. API coverage uploaded as an artifact.
|
7. API coverage uploaded as a workflow artifact.
|
||||||
|
|
||||||
A second Playwright job is gated behind the repository variable `ENABLE_E2E=true` and requires these secrets:
|
A second Playwright job is gated by repo variable `ENABLE_E2E=true` and needs
|
||||||
|
secrets `E2E_BASE_URL`, `E2E_USERNAME`, `E2E_PASSWORD`.
|
||||||
|
|
||||||
| Secret | Purpose |
|
Dependency updates come from self-hosted Renovate — grouped minor/patch PRs
|
||||||
| ----------------- | --------------------------------------- |
|
weekly, auto-merge for dev-dep patches, `prisma` + `@prisma/client` grouped
|
||||||
| `E2E_BASE_URL` | URL of a running Vector stack |
|
together, Radix/shadcn held for manual review.
|
||||||
| `E2E_USERNAME` | Test admin username |
|
|
||||||
| `E2E_PASSWORD` | Test admin password |
|
|
||||||
|
|
||||||
Dependency updates come from a self-hosted Renovate instance configured via [renovate.json](renovate.json) — grouped minor/patch PRs weekly, auto-merge for dev-dep patches, `prisma` + `@prisma/client` grouped together, Radix/shadcn held for manual review.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- **API shape**: all responses follow `{ code, message, requestId, details? }` for errors. Paginated lists use `{ data, page, pageSize, total }`.
|
- **API shape** — errors are `{ code, message, requestId, details? }`;
|
||||||
- **Validation**: every request body and query string is parsed through a zod schema from `@vector/shared` before it reaches a controller. No ad-hoc validation inside controllers.
|
paginated lists are `{ data, page, pageSize, total }`.
|
||||||
- **Query keys**: the web app uses a hierarchical factory at [apps/web/src/lib/queryKeys.ts](apps/web/src/lib/queryKeys.ts). Invalidate by domain (`queryKeys.parts.all`) or by filter (`queryKeys.parts.list(filters)`).
|
- **Validation** — every request body and query is parsed through a zod schema
|
||||||
- **Commits**: [Conventional Commits](https://www.conventionalcommits.org/) — Renovate already expects them.
|
from `@vector/shared` before reaching a controller. No ad-hoc validation
|
||||||
- **Webhooks**: every delivery is signed with HMAC-SHA256 over `${timestamp}.${body}` and sent with headers `x-vector-signature`, `x-vector-timestamp`, `x-vector-event`, and the recursion-guard `x-vector-webhook: v1`.
|
inside route handlers.
|
||||||
|
- **Query keys** — hierarchical factory at `apps/web/src/lib/queryKeys.ts`.
|
||||||
---
|
Invalidate by domain (`queryKeys.parts.all`) or by filter set
|
||||||
|
(`queryKeys.parts.list(filters)`).
|
||||||
## Roadmap
|
- **Commits** — [Conventional Commits](https://www.conventionalcommits.org/).
|
||||||
|
Renovate already expects them.
|
||||||
All nine phases of the 2.0 rewrite are in-tree:
|
|
||||||
|
|
||||||
| Phase | Scope |
|
|
||||||
| ----- | ------------------------------------------------------------------------ |
|
|
||||||
| 0 | Monorepo foundation (pnpm + Turbo, apps scaffolded) |
|
|
||||||
| 1 | Strict TypeScript, Prisma schema, env validation, singleton client |
|
|
||||||
| 2 | Service-layered API, transactional mutations, auth hardening, pagination |
|
|
||||||
| 3 | Schema extensions (hosts, repairs, tags, categories, webhooks, FTS-ready) |
|
|
||||||
| 4 | shadcn/ui design system + `DataTable<T>`, Form pattern, query-keys factory|
|
|
||||||
| 5 | Page-by-page rewrite (Parts, Locations split, Manufacturers, Users) |
|
|
||||||
| 6 | Feature slice — Repair/RMA, Tags, Bulk ops, Saved views |
|
|
||||||
| 7 | Analytics dashboard (Recharts) + EOL + Webhooks + streaming CSV export |
|
|
||||||
| 8 | Vitest + Playwright + Gitea Actions CI + Renovate |
|
|
||||||
|
|
||||||
### Deferred follow-ups
|
|
||||||
|
|
||||||
- **Postgres cutover.** Schema is already portable; the data-migration script lives at `packages/db/POSTGRES_FTS.md`.
|
|
||||||
- **BullMQ worker.** Replace the in-process webhook emitter in [apps/api/src/lib/webhook-emitter.ts](apps/api/src/lib/webhook-emitter.ts) with a Redis-backed worker. Signature is stable — one-line swap.
|
|
||||||
- **PDF audit export** via `@react-pdf/renderer` in the worker.
|
|
||||||
- **CSV import wizard UI** to pair with the existing `CsvImportJob` staging table.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user