Commit Graph
125 Commits
Author SHA1 Message Date
toanhoang 8ac95c974b Add ExpectedAsset CSV import + bot search/pick for pre-known inventory
CI / test (push) Failing after 8s
Lets the operator upload a CSV of already-known assets (asset tag, model,
serial, old area/assignee) before an office-move walk, via a new
session-authed /inventory-scans/import page. The Telegram bot gains a
"tim trong list" search step at the asset-tag prompt: picking a match
pre-fills tag/serial and offers one-tap "giu nguyen" buttons for the
old area/assignee, then marks that row matched once the scan is created.

Follows the same-day decision to turn off local OCR (qwen2.5vl via Ollama)
for this feature -- see CLAUDE.md and the ITAM/Ollama vault notes for the
accuracy/speed numbers behind that call.

Deviates from repo convention same as the InventoryScan work above: no
Vitest coverage (manual smoke test only, via simulated webhook calls),
hand-written migration, not through the ba->pm->feature-implementer->
test-runner->code-reviewer pipeline. tsc/lint are clean.
2026-08-21 08:59:18 -04:00
toanhoangandClaude Sonnet 5 b4e2f6c83e docs: note decision to merge inventory-capture bot into this app
CI / test (push) Failing after 9s
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-20 09:47:33 -04:00
toanhoangandClaude Sonnet 5 b52106dfa6 fix(ci): use db service hostname instead of localhost for Postgres
CI / test (push) Successful in 4m20s
self-hosted runner is pinned to homelab_proxy network (needed to resolve the gitea host for checkout), so job+service containers are separate network members, not sharing a netns like GitHub-hosted runners service containers do -- localhost inside the job container never reached the db service. Also drops the now-unneeded host port publish (avoids repeating the port-already-allocated failure an earlier orphaned run left behind).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:48:19 -04:00
toanhoangandClaude Sonnet 5 d7c0da2328 ci: retrigger #3 (cleared orphaned container holding port 5432)
CI / test (push) Failing after 12m29s
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:31:51 -04:00
toanhoangandClaude Sonnet 5 555fbf64fd ci: retrigger #2
CI / test (push) Failing after 8s
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:30:27 -04:00
toanhoangandClaude Sonnet 5 5a8dea2ea3 ci: retrigger after runner network + resource-limit fix
CI / test (push) Failing after 7s
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:25:05 -04:00
toanhoangandClaude Sonnet 5 acc79a1d3c chore: add Gitea Actions CI workflow
CI / test (push) Failing after 16m22s
npm ci -> prisma generate -> tsc --noEmit -> lint -> test (Postgres service) -> build, on every push/PR. Requires a self-hosted act_runner registered to git.toanhoang.work (set up 2026-08-20, see /opt/homelab/gitea-runner/ on homelab01 -- outside this repo). No auto-deploy yet, still manual on appsrv01.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:42 -04:00
toanhoangandClaude Sonnet 5 9773dd8a39 docs: document InventoryScan feature + note deviation from agent pipeline
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:39 -04:00
toanhoangandClaude Sonnet 5 84e6db7406 test: add Area, InventoryScan and bot API tests
369/369 tests pass (35 new), tsc --noEmit clean, next lint clean. Not yet run through the repos code-reviewer subagent gate (this session operated via SSH, cwd was not the project dir so .claude/agents/ subagents were not recognized) -- see CLAUDE.md note.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:32 -04:00
toanhoangandClaude Sonnet 5 77dbd15450 feat: add Inventory Scans review page
List + inline edit (model within category, area, assignee, specs, asset tag, serial number) + delete. No create form -- creation only happens via the bot.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:25 -04:00
toanhoangandClaude Sonnet 5 ae84737620 feat: add session-authed Area + InventoryScan API routes
GET /api/areas, GET /api/inventory-scans, PATCH+DELETE /api/inventory-scans/[id], GET /api/inventory-scans/[id]/photo (proxies Telegram file_id on demand, derives Content-Type from file_path extension since Telegram serves application/octet-stream regardless of actual type).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:18 -04:00
toanhoangandClaude Sonnet 5 17c5882e86 feat: add bot-facing API routes (catalog, areas, employees, asset-models, inventory-scans)
Auth via BOT_INGEST_SECRET bearer header (src/lib/bot-auth.ts), not Auth.js session -- caller is the inventory-capture Telegram bot service, not a browser.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:10 -04:00
toanhoangandClaude Sonnet 5 d5a0f1cb74 feat: add bot-secret auth helper for /api/bot/* routes
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:07 -04:00
toanhoangandClaude Sonnet 5 8a02a7c7ef feat: add InventoryScan validation + CRUD (src/lib/inventory-scans.ts)
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:00 -04:00
toanhoangandClaude Sonnet 5 1e6dec9cf2 feat: add Area validation + CRUD (src/lib/areas.ts)
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:06:58 -04:00
toanhoangandClaude Sonnet 5 3078d9e8c5 feat: add Area and InventoryScan Prisma models + migrations
Office-move inventory kiem ke feature. See docs/asset-license-management.md InventoryScan section for full business rationale (Area vs Seat, why not AssetItem, asset tag/serial nullable).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:06:52 -04:00
toanhoang e30489297c docs: note Assignment transfer UI landing in CLAUDE.md 2026-08-04 10:44:05 -04:00
toanhoang d700eaa6a9 feat: add transfer UI for Assignment
Per-row employee <Select> + Transfer button alongside the existing Return
button, shown for active (returned_at null) rows. PATCHes
/api/assignments/[id] with {employeeId}, which the route already dispatches
to transferAssignment() vs returnAssignment() based on body shape.
2026-08-04 10:43:39 -04:00
toanhoang 713dfad364 docs: note MaintenanceRecord UI landing in CLAUDE.md 2026-08-04 10:24:27 -04:00
toanhoang 74e78322b4 feat: add list+create+close UI for MaintenanceRecord
Mirrors assignments/page.tsx's per-row close mini-form pattern; targetStatus
select reuses the closed AssetItemStatus set client-side, same precedent as
licenses page's licenseType select.
2026-08-04 10:23:50 -04:00
toanhoang 651ff2d626 fix: read password from stdin prompt instead of CLI arg in create-user script
Avoids leaking the plaintext password via shell history/`ps`/process
listings. --email/--name stay as CLI flags (not sensitive); password is now
read via a masked interactive prompt using only Node built-ins (readline +
raw-mode stdin), no new dependency.
2026-08-04 06:42:35 -04:00
toanhoangandClaude Sonnet 5 b702528e19 fix: change Assignment.assetItemId/licenseId FK onDelete to RESTRICT
Was implicitly SET NULL (Prisma default for nullable FK columns),
inconsistent with employeeId's RESTRICT. Once a delete endpoint for
AssetItem/License exists, SET NULL would silently null out historical
Assignment rows on delete, erasing what was assigned and re-violating
the assetItemId/licenseId XOR retroactively. No delete endpoint exists
yet, so no real data was ever affected by the old behavior.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-04 06:41:13 -04:00
toanhoang be47eb69d3 test: add malformed/non-object body tests for assets, employees, requests
Closes the coverage gap noted in CLAUDE.md's "Việc còn dở" — mirrors the
existing pattern in tests/licenses.test.ts and tests/assignments.test.ts for
POST /api/assets, POST /api/employees, POST /api/requests (the only body-
accepting methods on these three routes; none have a PATCH route yet).
2026-08-04 06:40:13 -04:00
toanhoang 2a0acd11a9 fix: run dummy bcrypt.compare on unknown user to prevent timing side-channel 2026-08-04 06:39:08 -04:00
toanhoang 44dd33dd3b docs: note MaintenanceRecord landing + code-review gate PASS in CLAUDE.md
Records the entity build (4 commits), the HIGH race-condition finding
and MEDIUM targetStatus finding from code-reviewer, and their fixes.
2026-08-04 06:36:05 -04:00
toanhoang 7e1351bde2 fix: isolate Prisma Client per test file, re-enable fileParallelism
Root-cause fix for cross-file DB pollution in the vitest suite, which
surfaced as intermittent FK-violation failures once the suite hit 12
test files. tests/setup.ts previously mutated the global
process.env.DATABASE_URL and relied on src/lib/prisma.ts's singleton
picking it up, which is racy across files sharing a worker process
(process.env isn't reset between files even though Vitest resets each
file's module registry). fileParallelism: false only masked this.

Now each test file's own tests/setup.ts creates its own throwaway
Postgres database and vi.mock()s "@/lib/prisma" with a dedicated
PrismaClient constructed via the explicit datasourceUrl option, so no
mutable state is shared between files and fileParallelism can go back
to its default (true).

Verified with 4 consecutive full `npm test` runs (327/327 passing each
time) and no leftover throwaway databases after each run.
2026-08-04 06:04:23 -04:00
toanhoang 91624d5f07 fix: close MaintenanceRecord race condition + restrict targetStatus
createMaintenanceRecord()'s AssetItem status guard pinned status:{not:"retired"}
instead of status:"in_stock", so two concurrent opens (or an open racing a
concurrent createAssignment()) could both pass the guard, producing two open
MaintenanceRecords or an orphaned active Assignment. Guard now pins "in_stock"
exactly, mirroring createAssignment()'s own atomic guard.

closeMaintenanceRecord()'s targetStatus accepted any of the 4 AssetItemStatus
values, including "assigned" — which sets AssetItem.status without creating a
corresponding Assignment row, desyncing Assignment (source of truth for who
holds what) from AssetItem.status with no way back. Restricted to
in_stock|retired, the only two outcomes a closed repair can actually resolve
to.
2026-08-04 06:01:34 -04:00
toanhoang dfbee1a00d test: add MaintenanceRecord API tests 2026-08-04 05:35:37 -04:00
toanhoang 7f4a727669 feat: add MaintenanceRecord API routes
POST/GET /api/maintenance-records and PATCH /api/maintenance-records/[id]
(close action), mirroring the Assignment route pattern exactly.
2026-08-04 05:32:21 -04:00
toanhoang 67fe335e76 feat: add MaintenanceRecord validation + CRUD (src/lib/maintenance-records.ts)
createMaintenanceRecord() opens a record and flips AssetItem.status to
"repair" inside one transaction, rejecting on missing/retired AssetItem,
an active Assignment, or an already-open record for the same item.
closeMaintenanceRecord() sets endDate/resolutionNotes and applies a
caller-supplied targetStatus (never defaulted) to the AssetItem, guarding
double-close and endDate>=startDate. Mirrors src/lib/assignments.ts's
Zod-validate + $transaction + WHERE-guarded updateMany race-closing pattern.
2026-08-04 05:29:26 -04:00
toanhoang 5089bd6dfc feat: add MaintenanceRecord Prisma model + migration
New entity tracking repair/warranty/preventive maintenance jobs for an
AssetItem. Reuses Vendor (nullable) for the repair location, no cost
field (mirrors PurchaseOrder). "At most 1 open record per AssetItem"
invariant left to the application layer, mirroring Assignment.
2026-08-04 05:25:22 -04:00
toanhoang d557694f77 docs: chốt spec MaintenanceRecord (BA + stakeholder 2026-08-04)
Entity mới track sửa chữa/bảo hành cho AssetItem, hiện thực hoá case
"in repair" đã note từ trước. Field, type enum, đồng bộ AssetItem.status,
và quyết định không track cost đều đã chốt với stakeholder — sẵn sàng
giao pm chia task.
2026-08-04 04:18:47 -04:00
toanhoang e1ad9da72d docs: consolidate CLAUDE.md status/backlog, add Assignment review findings
Condensed the "Trạng thái hiện tại"/"Việc còn dở" sections — most of the
detailed per-entity narrative was either resolved, superseded by a later
change, or derivable from git log, so it's trimmed to what isn't:
decisions, gotchas, and still-open risks. Added the UI (worktree-ui-basic
merge), the Assignment transfer fix, and the two dormant findings
(XOR defense-in-depth, Assignment FK onDelete) from the code-reviewer gate.
2026-08-03 20:27:52 -04:00
toanhoang d6cea577c9 fix: scope vitest to tests/ dir to avoid picking up stray worktree checkouts
Vitest's default glob isn't excluded for .claude/, so a sibling worktree
under .claude/worktrees/ (e.g. worktree-ui-basic) got silently collected
and run against this repo's current src/ via the "@" alias, doubling the
test count and running a stale test file against current code under a
shared identity. Found by an independent test-runner pass while verifying
the Assignment transfer fix.
2026-08-03 19:54:49 -04:00
toanhoang a9dafcc140 fix: add atomic Assignment transfer (close old + create new in one transaction)
The only way to move an Assignment to a new employee was two separate calls
(PATCH return + POST create), which is not atomic and left a race window
where a third party could grab the now-in_stock AssetItem in between, plus
no rollback if the second call failed. Adds transferAssignment() which
closes the old Assignment and creates the new one inside a single
prisma.$transaction(), per docs/asset-license-management.md's "Assignment
(chốt 2026-08-02)" spec. Does not touch AssetItem.status or
License.seatsUsed — the resource never becomes free mid-transfer. Wired
into PATCH /api/assignments/[id]: an employeeId key in the body dispatches
to transfer, a returnedAt key to return, both together is rejected as
mutually exclusive.
2026-08-03 19:46:43 -04:00
toanhoang e951a14cc5 feat: add basic list+create+return UI for Assignment 2026-08-03 07:31:32 -04:00
toanhoang 2981ea6202 feat: add basic list+create UI for AssetItem, License, Request 2026-08-03 07:23:25 -04:00
toanhoang e471ae9c90 feat: add basic list+create UI for AssetCategoryAttributeDef, AssetModel, PurchaseOrder 2026-08-03 07:09:42 -04:00
toanhoang 4bcd4da11b feat: add basic list+create UI for Employee, Vendor, AssetCategory 2026-08-03 06:54:41 -04:00
toanhoang 0576d6e61c feat: add shared UI nav + fetch hook + table/select/label/textarea primitives
Basic infra for a functional (no-polish) UI pass over existing entity APIs.
2026-08-03 06:36:16 -04:00
toanhoang 6b39c01f4e test: add Assignment API tests
48 tests covering the XOR validation, the AssetItem.status/active-assignment
preconditions, seat overbooking, seat/status sync across assign+return,
FK-not-found for all three FKs, double-return, 404s, auth-gating, malformed
bodies, and two explicit concurrency tests (Promise.all against a
1-remaining-seat License and against a single in_stock AssetItem) exercising
the transaction race-closing logic in src/lib/assignments.ts.
2026-08-02 21:19:40 -04:00
toanhoang 5145c6af31 feat: add Assignment API routes
POST/GET /api/assignments mirror the existing entity route pattern
(auth-gated, thin handler delegating to src/lib/assignments.ts). PATCH
/api/assignments/[id] is the first dynamic-segment route in the codebase,
per the stakeholder-confirmed convention (docs/asset-license-management.md)
of update routes being PATCH /api/<entity>/[id] rather than an
action-based sub-route.
2026-08-02 21:19:34 -04:00
toanhoang a72b8dbbbe feat: add Assignment validation + CRUD (src/lib/assignments.ts)
createAssignment() enforces the XOR (assetItemId/licenseId) at the Zod
layer and the AssetItem.status/License.seatsUsed business rules inside a
single prisma.$transaction(), using atomic WHERE-guarded updateMany() calls
(not plain read-then-write) to close the seat-overbooking and
double-assignment race conditions flagged for License by code-reviewer.
returnAssignment() reverses those side-effects the same way and closes the
double-return race with the same pattern.
2026-08-02 21:19:28 -04:00
toanhoang 07b01cd707 feat: add Assignment Prisma model + migration
Links Employee to exactly one of AssetItem/License (XOR enforced at the
Zod layer, not built yet) via nullable FKs, plus assignedAt/returnedAt/
signedAt timestamps. See docs/asset-license-management.md's "Assignment"
section for the full spec.
2026-08-02 21:13:46 -04:00
toanhoangandClaude Sonnet 5 2037e6939e Merge branch 'feat/asset-item-model-id'
Retrofit AssetItem.model (string) -> modelId (FK to AssetModel) —
closes out Lane B (AssetCategory -> AssetCategoryAttributeDef +
AssetModel -> dynamic specs validation -> AssetItem.modelId).
Verified green by test-runner (225/225 tests, tsc/lint/build clean)
before merge.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-02 21:11:13 -04:00
toanhoang 2a5792df67 fix: retrofit AssetItem.model to modelId FK
AssetItem.model was a free-text string with no referential integrity.
Now a required FK to AssetModel, mirroring the Request.employeeId and
License.vendorId retrofit precedent (no backfill needed, no real
AssetItem data existed).
2026-08-02 21:07:12 -04:00
toanhoangandClaude Sonnet 5 d280279322 Merge branch 'feat/asset-model-dynamic-specs'
Wire dynamic specs validation into AssetModel creation: builds a
runtime Zod schema from the category's AssetCategoryAttributeDef
rows, rejects unknown keys, enforces required keys. Verified green
by test-runner (223/223 tests, tsc/lint/build clean) before merge.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-02 21:02:21 -04:00
toanhoang dc4c438ca9 feat: wire dynamic specs validation into AssetModel creation 2026-08-02 20:58:19 -04:00
toanhoangandClaude Sonnet 5 bcd41e4ba3 Merge branch 'feat/asset-model'
AssetModel entity: Prisma model+migration, Zod+CRUD (categoryId FK
+ business rule requiring valueGroup=high_value, specs JSONB loosely
validated for now), API routes, tests. Verified green by test-runner
(217/217 tests, tsc/lint/build clean) before merge.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-02 20:54:06 -04:00
toanhoang 0fdf82be61 test: add AssetModel API tests 2026-08-02 20:49:24 -04:00