Rename Forte to LunarFront, generalize for any small business

Rebrand from Forte (music-store-specific) to LunarFront (any small business):
- Package namespace @forte/* → @lunarfront/*
- Database forte/forte_test → lunarfront/lunarfront_test
- Docker containers, volumes, connection strings
- UI branding, localStorage keys, test emails
- All documentation and planning docs

Generalize music-specific terminology:
- instrumentDescription → itemDescription
- instrumentCount → itemCount
- instrumentType → itemCategory (on service templates)
- New migration 0027_generalize_terminology for column renames
- Seed data updated with generic examples
- RBAC descriptions updated
This commit is contained in:
Ryan Moon
2026-03-30 08:51:54 -05:00
parent 535446696c
commit 9400828f62
84 changed files with 390 additions and 820 deletions

View File

@@ -1,9 +1,9 @@
# Forte — Environment Variables
# LunarFront — Environment Variables
# Copy to .env and adjust values for your setup.
# Docker Compose overrides host values (postgres, valkey) automatically.
# Database (PostgreSQL 16)
DATABASE_URL=postgresql://forte:forte@localhost:5432/forte
DATABASE_URL=postgresql://lunarfront:lunarfront@localhost:5432/lunarfront
# Valkey / Redis
REDIS_URL=redis://localhost:6379
@@ -20,7 +20,7 @@ NODE_ENV=development
# Logging (optional)
# LOG_LEVEL=info
# LOG_FILE=./logs/forte.log
# LOG_FILE=./logs/lunarfront.log
# File Storage (optional — defaults to local)
# STORAGE_PROVIDER=local

View File

@@ -1,8 +1,8 @@
# Forte — Project Conventions
# LunarFront — Project Conventions
## App
- **Name:** Forte
- **Purpose:** Music store management platform (POS, inventory, rentals, lessons, repairs, accounting)
- **Name:** LunarFront
- **Purpose:** Small business management platform (POS, inventory, rentals, scheduling, repairs, accounting)
- **Company:** Lunarfront Tech LLC
## Tech Stack
@@ -18,12 +18,12 @@
- **Linting:** ESLint 9 flat config + Prettier
## Package Namespace
- `@forte/shared` — types, Zod schemas, business logic, utils
- `@forte/backend` — Fastify API server
- `@lunarfront/shared` — types, Zod schemas, business logic, utils
- `@lunarfront/backend` — Fastify API server
## Database
- Dev: `forte` on localhost:5432
- Test: `forte_test` on localhost:5432
- Dev: `lunarfront` on localhost:5432
- Test: `lunarfront_test` on localhost:5432
- Multi-tenant: `company_id` (uuid FK) on all domain tables for tenant isolation
- `location_id` (uuid FK) on tables that need per-location scoping (inventory, transactions, drawer)
- Migrations via Drizzle Kit (`bunx drizzle-kit generate`, `bunx drizzle-kit migrate`)
@@ -46,7 +46,7 @@
- `?sort=name&order=asc` — sorting by field name, asc or desc
- List responses always return `{ data: [...], pagination: { page, limit, total, totalPages } }`
- Search and filtering is ALWAYS server-side, never client-side
- Use `PaginationSchema` from `@forte/shared/schemas` to parse query params
- Use `PaginationSchema` from `@lunarfront/shared/schemas` to parse query params
- Use pagination helpers from `packages/backend/src/utils/pagination.ts`
- **Lookup endpoints** (e.g., `/roles/all`, `/statuses/all`) are the exception — these return a flat unpaginated list for populating dropdowns/selects. Use a `/all` suffix to distinguish from the paginated list endpoint for the same resource.
@@ -60,7 +60,7 @@
## Conventions
- Shared Zod schemas are the single source of truth for validation (used on both frontend and backend)
- Business logic lives in `@forte/shared`, not in individual app packages
- Business logic lives in `@lunarfront/shared`, not in individual app packages
- API routes are thin — validate with Zod, call a service, return result
- All financial events must be auditable (append-only audit records)
- JSON structured logging with request IDs on every log line

View File

@@ -1,6 +1,6 @@
# Forte
# LunarFront
Music store management platform — POS, inventory, rentals, lessons, repairs, and accounting.
Small business management platform — POS, inventory, rentals, scheduling, repairs, and accounting.
Built by [Lunarfront Tech LLC](https://lunarfront.com).

104
bun.lock
View File

@@ -3,7 +3,7 @@
"configVersion": 1,
"workspaces": {
"": {
"name": "forte",
"name": "lunarfront",
"dependencies": {
"zod": "^4.3.6",
},
@@ -16,11 +16,11 @@
},
},
"packages/admin": {
"name": "@forte/admin",
"name": "@lunarfront/admin",
"version": "0.0.1",
"dependencies": {
"@forte/shared": "workspace:*",
"@hookform/resolvers": "^5.2.2",
"@lunarfront/shared": "workspace:*",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
@@ -55,14 +55,14 @@
},
},
"packages/backend": {
"name": "@forte/backend",
"name": "@lunarfront/backend",
"version": "0.0.1",
"dependencies": {
"@fastify/cors": "^10",
"@fastify/jwt": "^9",
"@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0",
"@forte/shared": "workspace:*",
"@lunarfront/shared": "workspace:*",
"bcrypt": "^6",
"drizzle-orm": "^0.38",
"fastify": "^5",
@@ -80,7 +80,7 @@
},
},
"packages/shared": {
"name": "@forte/shared",
"name": "@lunarfront/shared",
"version": "0.0.1",
"dependencies": {
"zod": "^4",
@@ -243,12 +243,6 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@forte/admin": ["@forte/admin@workspace:packages/admin"],
"@forte/backend": ["@forte/backend@workspace:packages/backend"],
"@forte/shared": ["@forte/shared@workspace:packages/shared"],
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -273,6 +267,12 @@
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
"@lunarfront/admin": ["@lunarfront/admin@workspace:packages/admin"],
"@lunarfront/backend": ["@lunarfront/backend@workspace:packages/backend"],
"@lunarfront/shared": ["@lunarfront/shared@workspace:packages/shared"],
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
@@ -399,55 +399,55 @@
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
@@ -487,15 +487,15 @@
"@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="],
"@tanstack/react-router": ["@tanstack/react-router@1.168.7", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.6", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-fW/HvQja4PQeu9lsoyh+pXpZ0UXezbpQkkJvCuH6tHAaW3jvPkjh14lfadrNNiY+pXT7WiMTB3afGhTCC78PDQ=="],
"@tanstack/react-router": ["@tanstack/react-router@1.168.8", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.7", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-t0S0QueXubBKmI9eLPcN/A1sLQgTu8/yHerjrvvsGeD12zMdw0uJPKwEKpStQF2OThQtw64cs34uUSYXBUTSNw=="],
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
"@tanstack/router-core": ["@tanstack/router-core@1.168.6", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-okCno3pImpLFQMJ/4zqEIGjIV5yhxLGj0JByrzQDQehORN1y1q6lJUezT0KPK5qCQiKUApeWaboLPjgBVx1kaQ=="],
"@tanstack/router-core": ["@tanstack/router-core@1.168.7", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-z4UEdlzMrFaKBsG4OIxlZEm+wsYBtEp//fnX6kW18jhQpETNcM6u2SXNdX+bcIYp6AaR7ERS3SBENzjC/xxwQQ=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.21", "", { "dependencies": { "@tanstack/router-core": "1.168.6", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-pJWsP6HaGrkIkfkcg6vzKyCBMbf1vV1BrQH+bFAVzXj3T/afmix3IPV2hiAj4zzjMxuddJD1on0Hn5+WDYA7zQ=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.22", "", { "dependencies": { "@tanstack/router-core": "1.168.7", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-wQ7H8/Q2rmSPuaxWnurJ3DATNnqWV2tajxri9TSiW4QHsG7cWPD34+goeIinKG+GajJyEdfVpz6w/gRJXfbAPw=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.8", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.6", "@tanstack/router-generator": "1.166.21", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-/X4ACYsSX4bRmomj5X2TBU75cHuIVI99Fsax6DWnP6hPb4PaSjPUHVBfHhk2NemJzEOZu1L31UQ9QDlbHU4ZTQ=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.9", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.7", "@tanstack/router-generator": "1.166.22", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.8", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-h/VV05FEHd4PVyc5Zy8B3trWLcdLt/Pmp+mfifmBKGRw+MUtvdQKbBHhmy4ouOf67s5zDJMc+n8R3xgU7bDwFA=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="],
@@ -503,17 +503,17 @@
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="],
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="],
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.21", "", { "os": "darwin", "cpu": "x64" }, "sha512-kfGoM0Iw8ZNZpbds+4IzOe0hjvHldqJwUPRAjXJi3KBxg/QOZL95N893SRoMtf2aJ+jJ3dk32yPkp8rvcIjP9g=="],
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="],
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.21", "", { "os": "darwin", "cpu": "arm64" }, "sha512-o9HEflxUEyr987x0cTUzZBhDOyL6u95JmdmlkH2VyxAw7zq2sdtM5e72y9ufv2N5SIoOBw1fVn9UES5VY5H6vQ=="],
"@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="],
"@turbo/linux-64": ["@turbo/linux-64@2.8.21", "", { "os": "linux", "cpu": "x64" }, "sha512-uTxlCcXWy5h1fSSymP8XSJ+AudzEHMDV3IDfKX7+DGB8kgJ+SLoTUAH7z4OFA7I/l2sznz0upPdbNNZs91YMag=="],
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="],
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-cdHIcxNcihHHkCHp0Y4Zb60K4Qz+CK4xw1gb6s/t/9o4SMeMj+hTBCtoW6QpPnl9xPYmxuTou8Zw6+cylTnREg=="],
"@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="],
"@turbo/windows-64": ["@turbo/windows-64@2.8.21", "", { "os": "win32", "cpu": "x64" }, "sha512-/iBj4OzbqEY8CX+eaeKbBTMZv2CLXNrt0692F7HnK7LcyYwyDecaAiSET6ZzL4opT7sbwkKvzAC/fhqT3Quu1A=="],
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="],
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-95tMA/ZbIidJFUUtkmqioQ1gf3n3I1YbRP3ZgVdWTVn2qVbkodcIdGXBKRHHrIbRsLRl99SiHi/L7IxhpZDagQ=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -597,7 +597,7 @@
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ=="],
"bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="],
@@ -615,7 +615,7 @@
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001782", "", {}, "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw=="],
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
@@ -991,7 +991,7 @@
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
"rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="],
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -1067,7 +1067,7 @@
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"turbo": ["turbo@2.8.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="],
"turbo": ["turbo@2.8.21", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.21", "@turbo/darwin-arm64": "2.8.21", "@turbo/linux-64": "2.8.21", "@turbo/linux-arm64": "2.8.21", "@turbo/windows-64": "2.8.21", "@turbo/windows-arm64": "2.8.21" }, "bin": { "turbo": "bin/turbo" } }, "sha512-FlJ8OD5Qcp0jTAM7E4a/RhUzRNds2GzKlyxHKA6N247VLy628rrxAGlMpIXSz6VB430+TiQDJ/SMl6PL1lu6wQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
@@ -1153,7 +1153,7 @@
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],

View File

@@ -3,7 +3,7 @@ services:
build:
context: .
dockerfile: Dockerfile.dev
container_name: forte-api
container_name: lunarfront-api
restart: unless-stopped
ports:
- "8000:8000"
@@ -14,7 +14,7 @@ services:
- /app/packages/backend/node_modules
- /app/packages/shared/node_modules
environment:
DATABASE_URL: postgresql://forte:forte@postgres:5432/forte
DATABASE_URL: postgresql://lunarfront:lunarfront@postgres:5432/lunarfront
REDIS_URL: redis://valkey:6379
JWT_SECRET: dev-secret-do-not-use-in-production
NODE_ENV: development
@@ -28,30 +28,30 @@ services:
postgres:
image: postgres:16
container_name: forte-postgres
container_name: lunarfront-postgres
restart: unless-stopped
environment:
POSTGRES_USER: forte
POSTGRES_PASSWORD: forte
POSTGRES_DB: forte
POSTGRES_USER: lunarfront
POSTGRES_PASSWORD: lunarfront
POSTGRES_DB: lunarfront
ports:
- "5432:5432"
volumes:
- forte-pgdata:/var/lib/postgresql/data
- lunarfront-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forte"]
test: ["CMD-SHELL", "pg_isready -U lunarfront"]
interval: 5s
timeout: 3s
retries: 5
valkey:
image: valkey/valkey:8
container_name: forte-valkey
container_name: lunarfront-valkey
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- forte-valkey:/data
- lunarfront-valkey:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
@@ -59,5 +59,5 @@ services:
retries: 5
volumes:
forte-pgdata:
forte-valkey:
lunarfront-pgdata:
lunarfront-valkey:

View File

@@ -3,16 +3,16 @@
## Monorepo Structure
```
forte/
lunarfront/
packages/
shared/ @forte/shared — Zod schemas, types, business logic, utils
backend/ @forte/backend — Fastify API server
admin/ @forte/admin — Admin UI (React + Vite)
shared/ @lunarfront/shared — Zod schemas, types, business logic, utils
backend/ @lunarfront/backend — Fastify API server
admin/ @lunarfront/admin — Admin UI (React + Vite)
planning/ Domain planning docs (01-26)
docs/ Technical documentation
```
Managed with Turborepo and Bun workspaces. `@forte/shared` is a dependency of both `backend` and `admin`.
Managed with Turborepo and Bun workspaces. `@lunarfront/shared` is a dependency of both `backend` and `admin`.
## Backend

View File

@@ -6,8 +6,8 @@ PostgreSQL 16. Two databases:
| Database | Port | Usage |
|----------|------|-------|
| `forte` | 5432 | Development |
| `forte_api_test` | 5432 | API integration tests (auto-created by test runner) |
| `lunarfront` | 5432 | Development |
| `lunarfront_api_test` | 5432 | API integration tests (auto-created by test runner) |
## Migrations
@@ -35,7 +35,7 @@ All domain tables include `company_id` (uuid FK to `company`). Every query filte
| Table | Description |
|-------|-------------|
| `company` | Tenant (music store business) |
| `company` | Tenant (tenant business) |
| `location` | Physical store location |
| `user` | Staff/admin user account |

View File

@@ -9,7 +9,7 @@
## Installation
```bash
git clone <repo-url> && cd forte
git clone <repo-url> && cd lunarfront
bun install
```
@@ -18,7 +18,7 @@ bun install
Create a `.env` file in the project root:
```env
DATABASE_URL=postgresql://forte:forte@localhost:5432/forte
DATABASE_URL=postgresql://lunarfront:lunarfront@localhost:5432/lunarfront
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-here
NODE_ENV=development
@@ -28,7 +28,7 @@ NODE_ENV=development
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | `postgresql://forte:forte@localhost:5432/forte` | PostgreSQL connection string |
| `DATABASE_URL` | `postgresql://lunarfront:lunarfront@localhost:5432/lunarfront` | PostgreSQL connection string |
| `REDIS_URL` | `redis://localhost:6379` | Valkey/Redis connection string |
| `JWT_SECRET` | (auto-generated in dev) | Secret for signing JWTs. **Required in production.** |
| `PORT` | `8000` | Backend API port |
@@ -45,7 +45,7 @@ NODE_ENV=development
```bash
# Create the database
createdb forte
createdb lunarfront
# Run migrations
cd packages/backend

View File

@@ -4,7 +4,7 @@
The primary test suite lives at `packages/backend/api-tests/`. It uses a custom runner that:
1. Creates/migrates a `forte_api_test` database
1. Creates/migrates a `lunarfront_api_test` database
2. Seeds company, lookup tables, RBAC permissions/roles
3. Starts the backend on port 8001
4. Registers a test user with admin role

View File

@@ -1,6 +1,7 @@
{
"name": "forte",
"name": "lunarfront",
"private": true,
"packageManager": "bun@1.3.11",
"workspaces": ["packages/*"],
"scripts": {
"dev": "turbo run dev",

View File

@@ -1,473 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/users'
import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile'
import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help'
import { Route as AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index'
import { Route as AuthenticatedMembersIndexRouteImport } from './routes/_authenticated/members/index'
import { Route as AuthenticatedAccountsIndexRouteImport } from './routes/_authenticated/accounts/index'
import { Route as AuthenticatedRolesNewRouteImport } from './routes/_authenticated/roles/new'
import { Route as AuthenticatedRolesRoleIdRouteImport } from './routes/_authenticated/roles/$roleId'
import { Route as AuthenticatedMembersMemberIdRouteImport } from './routes/_authenticated/members/$memberId'
import { Route as AuthenticatedAccountsNewRouteImport } from './routes/_authenticated/accounts/new'
import { Route as AuthenticatedAccountsAccountIdRouteImport } from './routes/_authenticated/accounts/$accountId'
import { Route as AuthenticatedAccountsAccountIdIndexRouteImport } from './routes/_authenticated/accounts/$accountId/index'
import { Route as AuthenticatedAccountsAccountIdTaxExemptionsRouteImport } from './routes/_authenticated/accounts/$accountId/tax-exemptions'
import { Route as AuthenticatedAccountsAccountIdProcessorLinksRouteImport } from './routes/_authenticated/accounts/$accountId/processor-links'
import { Route as AuthenticatedAccountsAccountIdPaymentMethodsRouteImport } from './routes/_authenticated/accounts/$accountId/payment-methods'
import { Route as AuthenticatedAccountsAccountIdMembersRouteImport } from './routes/_authenticated/accounts/$accountId/members'
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const AuthenticatedRoute = AuthenticatedRouteImport.update({
id: '/_authenticated',
getParentRoute: () => rootRouteImport,
} as any)
const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedUsersRoute = AuthenticatedUsersRouteImport.update({
id: '/users',
path: '/users',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedProfileRoute = AuthenticatedProfileRouteImport.update({
id: '/profile',
path: '/profile',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedHelpRoute = AuthenticatedHelpRouteImport.update({
id: '/help',
path: '/help',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({
id: '/roles/',
path: '/roles/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedMembersIndexRoute =
AuthenticatedMembersIndexRouteImport.update({
id: '/members/',
path: '/members/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAccountsIndexRoute =
AuthenticatedAccountsIndexRouteImport.update({
id: '/accounts/',
path: '/accounts/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedRolesNewRoute = AuthenticatedRolesNewRouteImport.update({
id: '/roles/new',
path: '/roles/new',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedRolesRoleIdRoute =
AuthenticatedRolesRoleIdRouteImport.update({
id: '/roles/$roleId',
path: '/roles/$roleId',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedMembersMemberIdRoute =
AuthenticatedMembersMemberIdRouteImport.update({
id: '/members/$memberId',
path: '/members/$memberId',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAccountsNewRoute =
AuthenticatedAccountsNewRouteImport.update({
id: '/accounts/new',
path: '/accounts/new',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAccountsAccountIdRoute =
AuthenticatedAccountsAccountIdRouteImport.update({
id: '/accounts/$accountId',
path: '/accounts/$accountId',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAccountsAccountIdIndexRoute =
AuthenticatedAccountsAccountIdIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
} as any)
const AuthenticatedAccountsAccountIdTaxExemptionsRoute =
AuthenticatedAccountsAccountIdTaxExemptionsRouteImport.update({
id: '/tax-exemptions',
path: '/tax-exemptions',
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
} as any)
const AuthenticatedAccountsAccountIdProcessorLinksRoute =
AuthenticatedAccountsAccountIdProcessorLinksRouteImport.update({
id: '/processor-links',
path: '/processor-links',
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
} as any)
const AuthenticatedAccountsAccountIdPaymentMethodsRoute =
AuthenticatedAccountsAccountIdPaymentMethodsRouteImport.update({
id: '/payment-methods',
path: '/payment-methods',
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
} as any)
const AuthenticatedAccountsAccountIdMembersRoute =
AuthenticatedAccountsAccountIdMembersRouteImport.update({
id: '/members',
path: '/members',
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof AuthenticatedIndexRoute
'/login': typeof LoginRoute
'/help': typeof AuthenticatedHelpRoute
'/profile': typeof AuthenticatedProfileRoute
'/users': typeof AuthenticatedUsersRoute
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
'/accounts/new': typeof AuthenticatedAccountsNewRoute
'/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
'/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
'/roles/new': typeof AuthenticatedRolesNewRoute
'/accounts/': typeof AuthenticatedAccountsIndexRoute
'/members/': typeof AuthenticatedMembersIndexRoute
'/roles/': typeof AuthenticatedRolesIndexRoute
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
'/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
}
export interface FileRoutesByTo {
'/login': typeof LoginRoute
'/help': typeof AuthenticatedHelpRoute
'/profile': typeof AuthenticatedProfileRoute
'/users': typeof AuthenticatedUsersRoute
'/': typeof AuthenticatedIndexRoute
'/accounts/new': typeof AuthenticatedAccountsNewRoute
'/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
'/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
'/roles/new': typeof AuthenticatedRolesNewRoute
'/accounts': typeof AuthenticatedAccountsIndexRoute
'/members': typeof AuthenticatedMembersIndexRoute
'/roles': typeof AuthenticatedRolesIndexRoute
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_authenticated': typeof AuthenticatedRouteWithChildren
'/login': typeof LoginRoute
'/_authenticated/help': typeof AuthenticatedHelpRoute
'/_authenticated/profile': typeof AuthenticatedProfileRoute
'/_authenticated/users': typeof AuthenticatedUsersRoute
'/_authenticated/': typeof AuthenticatedIndexRoute
'/_authenticated/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
'/_authenticated/accounts/new': typeof AuthenticatedAccountsNewRoute
'/_authenticated/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
'/_authenticated/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
'/_authenticated/roles/new': typeof AuthenticatedRolesNewRoute
'/_authenticated/accounts/': typeof AuthenticatedAccountsIndexRoute
'/_authenticated/members/': typeof AuthenticatedMembersIndexRoute
'/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
'/_authenticated/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
'/_authenticated/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/login'
| '/help'
| '/profile'
| '/users'
| '/accounts/$accountId'
| '/accounts/new'
| '/members/$memberId'
| '/roles/$roleId'
| '/roles/new'
| '/accounts/'
| '/members/'
| '/roles/'
| '/accounts/$accountId/members'
| '/accounts/$accountId/payment-methods'
| '/accounts/$accountId/processor-links'
| '/accounts/$accountId/tax-exemptions'
| '/accounts/$accountId/'
fileRoutesByTo: FileRoutesByTo
to:
| '/login'
| '/help'
| '/profile'
| '/users'
| '/'
| '/accounts/new'
| '/members/$memberId'
| '/roles/$roleId'
| '/roles/new'
| '/accounts'
| '/members'
| '/roles'
| '/accounts/$accountId/members'
| '/accounts/$accountId/payment-methods'
| '/accounts/$accountId/processor-links'
| '/accounts/$accountId/tax-exemptions'
| '/accounts/$accountId'
id:
| '__root__'
| '/_authenticated'
| '/login'
| '/_authenticated/help'
| '/_authenticated/profile'
| '/_authenticated/users'
| '/_authenticated/'
| '/_authenticated/accounts/$accountId'
| '/_authenticated/accounts/new'
| '/_authenticated/members/$memberId'
| '/_authenticated/roles/$roleId'
| '/_authenticated/roles/new'
| '/_authenticated/accounts/'
| '/_authenticated/members/'
| '/_authenticated/roles/'
| '/_authenticated/accounts/$accountId/members'
| '/_authenticated/accounts/$accountId/payment-methods'
| '/_authenticated/accounts/$accountId/processor-links'
| '/_authenticated/accounts/$accountId/tax-exemptions'
| '/_authenticated/accounts/$accountId/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
LoginRoute: typeof LoginRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/_authenticated': {
id: '/_authenticated'
path: ''
fullPath: '/'
preLoaderRoute: typeof AuthenticatedRouteImport
parentRoute: typeof rootRouteImport
}
'/_authenticated/': {
id: '/_authenticated/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof AuthenticatedIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/users': {
id: '/_authenticated/users'
path: '/users'
fullPath: '/users'
preLoaderRoute: typeof AuthenticatedUsersRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/profile': {
id: '/_authenticated/profile'
path: '/profile'
fullPath: '/profile'
preLoaderRoute: typeof AuthenticatedProfileRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/help': {
id: '/_authenticated/help'
path: '/help'
fullPath: '/help'
preLoaderRoute: typeof AuthenticatedHelpRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/roles/': {
id: '/_authenticated/roles/'
path: '/roles'
fullPath: '/roles/'
preLoaderRoute: typeof AuthenticatedRolesIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/members/': {
id: '/_authenticated/members/'
path: '/members'
fullPath: '/members/'
preLoaderRoute: typeof AuthenticatedMembersIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/accounts/': {
id: '/_authenticated/accounts/'
path: '/accounts'
fullPath: '/accounts/'
preLoaderRoute: typeof AuthenticatedAccountsIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/roles/new': {
id: '/_authenticated/roles/new'
path: '/roles/new'
fullPath: '/roles/new'
preLoaderRoute: typeof AuthenticatedRolesNewRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/roles/$roleId': {
id: '/_authenticated/roles/$roleId'
path: '/roles/$roleId'
fullPath: '/roles/$roleId'
preLoaderRoute: typeof AuthenticatedRolesRoleIdRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/members/$memberId': {
id: '/_authenticated/members/$memberId'
path: '/members/$memberId'
fullPath: '/members/$memberId'
preLoaderRoute: typeof AuthenticatedMembersMemberIdRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/accounts/new': {
id: '/_authenticated/accounts/new'
path: '/accounts/new'
fullPath: '/accounts/new'
preLoaderRoute: typeof AuthenticatedAccountsNewRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/accounts/$accountId': {
id: '/_authenticated/accounts/$accountId'
path: '/accounts/$accountId'
fullPath: '/accounts/$accountId'
preLoaderRoute: typeof AuthenticatedAccountsAccountIdRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/accounts/$accountId/': {
id: '/_authenticated/accounts/$accountId/'
path: '/'
fullPath: '/accounts/$accountId/'
preLoaderRoute: typeof AuthenticatedAccountsAccountIdIndexRouteImport
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
}
'/_authenticated/accounts/$accountId/tax-exemptions': {
id: '/_authenticated/accounts/$accountId/tax-exemptions'
path: '/tax-exemptions'
fullPath: '/accounts/$accountId/tax-exemptions'
preLoaderRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRouteImport
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
}
'/_authenticated/accounts/$accountId/processor-links': {
id: '/_authenticated/accounts/$accountId/processor-links'
path: '/processor-links'
fullPath: '/accounts/$accountId/processor-links'
preLoaderRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRouteImport
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
}
'/_authenticated/accounts/$accountId/payment-methods': {
id: '/_authenticated/accounts/$accountId/payment-methods'
path: '/payment-methods'
fullPath: '/accounts/$accountId/payment-methods'
preLoaderRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRouteImport
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
}
'/_authenticated/accounts/$accountId/members': {
id: '/_authenticated/accounts/$accountId/members'
path: '/members'
fullPath: '/accounts/$accountId/members'
preLoaderRoute: typeof AuthenticatedAccountsAccountIdMembersRouteImport
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
}
}
}
interface AuthenticatedAccountsAccountIdRouteChildren {
AuthenticatedAccountsAccountIdMembersRoute: typeof AuthenticatedAccountsAccountIdMembersRoute
AuthenticatedAccountsAccountIdPaymentMethodsRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
AuthenticatedAccountsAccountIdProcessorLinksRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
AuthenticatedAccountsAccountIdTaxExemptionsRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
AuthenticatedAccountsAccountIdIndexRoute: typeof AuthenticatedAccountsAccountIdIndexRoute
}
const AuthenticatedAccountsAccountIdRouteChildren: AuthenticatedAccountsAccountIdRouteChildren =
{
AuthenticatedAccountsAccountIdMembersRoute:
AuthenticatedAccountsAccountIdMembersRoute,
AuthenticatedAccountsAccountIdPaymentMethodsRoute:
AuthenticatedAccountsAccountIdPaymentMethodsRoute,
AuthenticatedAccountsAccountIdProcessorLinksRoute:
AuthenticatedAccountsAccountIdProcessorLinksRoute,
AuthenticatedAccountsAccountIdTaxExemptionsRoute:
AuthenticatedAccountsAccountIdTaxExemptionsRoute,
AuthenticatedAccountsAccountIdIndexRoute:
AuthenticatedAccountsAccountIdIndexRoute,
}
const AuthenticatedAccountsAccountIdRouteWithChildren =
AuthenticatedAccountsAccountIdRoute._addFileChildren(
AuthenticatedAccountsAccountIdRouteChildren,
)
interface AuthenticatedRouteChildren {
AuthenticatedHelpRoute: typeof AuthenticatedHelpRoute
AuthenticatedProfileRoute: typeof AuthenticatedProfileRoute
AuthenticatedUsersRoute: typeof AuthenticatedUsersRoute
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
AuthenticatedAccountsAccountIdRoute: typeof AuthenticatedAccountsAccountIdRouteWithChildren
AuthenticatedAccountsNewRoute: typeof AuthenticatedAccountsNewRoute
AuthenticatedMembersMemberIdRoute: typeof AuthenticatedMembersMemberIdRoute
AuthenticatedRolesRoleIdRoute: typeof AuthenticatedRolesRoleIdRoute
AuthenticatedRolesNewRoute: typeof AuthenticatedRolesNewRoute
AuthenticatedAccountsIndexRoute: typeof AuthenticatedAccountsIndexRoute
AuthenticatedMembersIndexRoute: typeof AuthenticatedMembersIndexRoute
AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute
}
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedHelpRoute: AuthenticatedHelpRoute,
AuthenticatedProfileRoute: AuthenticatedProfileRoute,
AuthenticatedUsersRoute: AuthenticatedUsersRoute,
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
AuthenticatedAccountsAccountIdRoute:
AuthenticatedAccountsAccountIdRouteWithChildren,
AuthenticatedAccountsNewRoute: AuthenticatedAccountsNewRoute,
AuthenticatedMembersMemberIdRoute: AuthenticatedMembersMemberIdRoute,
AuthenticatedRolesRoleIdRoute: AuthenticatedRolesRoleIdRoute,
AuthenticatedRolesNewRoute: AuthenticatedRolesNewRoute,
AuthenticatedAccountsIndexRoute: AuthenticatedAccountsIndexRoute,
AuthenticatedMembersIndexRoute: AuthenticatedMembersIndexRoute,
AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute,
}
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
AuthenticatedRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
AuthenticatedRoute: AuthenticatedRouteWithChildren,
LoginRoute: LoginRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

View File

@@ -3,13 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Forte Admin</title>
<title>LunarFront Admin</title>
</head>
<body>
<script>
// Apply mode before React renders to prevent flash
(function() {
var mode = localStorage.getItem('forte-mode') || 'system';
var mode = localStorage.getItem('lunarfront-mode') || 'system';
var isDark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) document.documentElement.classList.add('dark');
})();

View File

@@ -1,5 +1,5 @@
{
"name": "@forte/admin",
"name": "@lunarfront/admin",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -10,7 +10,7 @@
"lint": "eslint ."
},
"dependencies": {
"@forte/shared": "workspace:*",
"@lunarfront/shared": "workspace:*",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { Account } from '@/types/account'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
export const accountKeys = {
all: ['accounts'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { MemberIdentifier } from '@/types/account'
import type { PaginatedResponse } from '@forte/shared/schemas'
import type { PaginatedResponse } from '@lunarfront/shared/schemas'
export const identifierKeys = {
all: (memberId: string) => ['members', memberId, 'identifiers'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { Member } from '@/types/account'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
interface MemberWithAccount extends Member {
accountName: string | null

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { PaymentMethod } from '@/types/account'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
export const paymentMethodKeys = {
all: (accountId: string) => ['accounts', accountId, 'payment-methods'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { ProcessorLink } from '@/types/account'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
export const processorLinkKeys = {
all: (accountId: string) => ['accounts', accountId, 'processor-links'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { Permission, Role } from '@/types/rbac'
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
export const rbacKeys = {
permissions: ['permissions'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { RepairTicket, RepairLineItem, RepairBatch, RepairNote, RepairServiceTemplate } from '@/types/repair'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
// --- Repair Tickets ---

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { StorageFolder, StorageFolderPermission, StorageFile } from '@/types/storage'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
// --- Folders ---

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { TaxExemption } from '@/types/account'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
export const taxExemptionKeys = {
all: (accountId: string) => ['accounts', accountId, 'tax-exemptions'] as const,

View File

@@ -1,6 +1,6 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
export interface UserRole {
id: string

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { VaultStatus, VaultCategory, VaultCategoryPermission, VaultEntry } from '@/types/vault'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
// --- Keys ---

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { AccountCreateSchema } from '@forte/shared/schemas'
import { AccountCreateSchema } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { PaymentMethodCreateSchema } from '@forte/shared/schemas'
import type { PaymentMethodCreateInput } from '@forte/shared/schemas'
import { PaymentMethodCreateSchema } from '@lunarfront/shared/schemas'
import type { PaymentMethodCreateInput } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { ProcessorLinkCreateSchema } from '@forte/shared/schemas'
import type { ProcessorLinkCreateInput } from '@forte/shared/schemas'
import { ProcessorLinkCreateSchema } from '@lunarfront/shared/schemas'
import type { ProcessorLinkCreateInput } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { TaxExemptionCreateSchema } from '@forte/shared/schemas'
import type { TaxExemptionCreateInput } from '@forte/shared/schemas'
import { TaxExemptionCreateSchema } from '@lunarfront/shared/schemas'
import type { TaxExemptionCreateInput } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

View File

@@ -24,7 +24,7 @@ interface GeneratePdfOptions {
companyName?: string
}
export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, companyName = 'Forte Music' }: GeneratePdfOptions): jsPDF {
export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, companyName = 'LunarFront' }: GeneratePdfOptions): jsPDF {
const doc = new jsPDF()
let y = 20
@@ -57,11 +57,11 @@ export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, comp
doc.setFontSize(10)
doc.setFont('helvetica', 'bold')
doc.text('Customer', 14, y)
doc.text('Instrument', 110, y)
doc.text('Item', 110, y)
y += 5
doc.setFont('helvetica', 'normal')
doc.text(ticket.customerName, 14, y)
doc.text(ticket.instrumentDescription ?? '-', 110, y)
doc.text(ticket.itemDescription ?? '-', 110, y)
y += 5
if (ticket.customerPhone) { doc.text(ticket.customerPhone, 14, y); y += 5 }
if (ticket.serialNumber) { doc.text(`S/N: ${ticket.serialNumber}`, 110, y - 5) }

View File

@@ -1,5 +1,5 @@
import { useNavigate, useSearch } from '@tanstack/react-router'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
interface PaginationSearch {
page?: number

View File

@@ -16,6 +16,7 @@ import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/
import { Route as AuthenticatedSettingsRouteImport } from './routes/_authenticated/settings'
import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile'
import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help'
import { Route as AuthenticatedVaultIndexRouteImport } from './routes/_authenticated/vault/index'
import { Route as AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index'
import { Route as AuthenticatedRepairsIndexRouteImport } from './routes/_authenticated/repairs/index'
import { Route as AuthenticatedRepairBatchesIndexRouteImport } from './routes/_authenticated/repair-batches/index'
@@ -72,6 +73,11 @@ const AuthenticatedHelpRoute = AuthenticatedHelpRouteImport.update({
path: '/help',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedVaultIndexRoute = AuthenticatedVaultIndexRouteImport.update({
id: '/vault/',
path: '/vault/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({
id: '/roles/',
path: '/roles/',
@@ -218,6 +224,7 @@ export interface FileRoutesByFullPath {
'/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
'/repairs/': typeof AuthenticatedRepairsIndexRoute
'/roles/': typeof AuthenticatedRolesIndexRoute
'/vault/': typeof AuthenticatedVaultIndexRoute
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
@@ -246,6 +253,7 @@ export interface FileRoutesByTo {
'/repair-batches': typeof AuthenticatedRepairBatchesIndexRoute
'/repairs': typeof AuthenticatedRepairsIndexRoute
'/roles': typeof AuthenticatedRolesIndexRoute
'/vault': typeof AuthenticatedVaultIndexRoute
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
@@ -277,6 +285,7 @@ export interface FileRoutesById {
'/_authenticated/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
'/_authenticated/repairs/': typeof AuthenticatedRepairsIndexRoute
'/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute
'/_authenticated/vault/': typeof AuthenticatedVaultIndexRoute
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
@@ -308,6 +317,7 @@ export interface FileRouteTypes {
| '/repair-batches/'
| '/repairs/'
| '/roles/'
| '/vault/'
| '/accounts/$accountId/members'
| '/accounts/$accountId/payment-methods'
| '/accounts/$accountId/processor-links'
@@ -336,6 +346,7 @@ export interface FileRouteTypes {
| '/repair-batches'
| '/repairs'
| '/roles'
| '/vault'
| '/accounts/$accountId/members'
| '/accounts/$accountId/payment-methods'
| '/accounts/$accountId/processor-links'
@@ -366,6 +377,7 @@ export interface FileRouteTypes {
| '/_authenticated/repair-batches/'
| '/_authenticated/repairs/'
| '/_authenticated/roles/'
| '/_authenticated/vault/'
| '/_authenticated/accounts/$accountId/members'
| '/_authenticated/accounts/$accountId/payment-methods'
| '/_authenticated/accounts/$accountId/processor-links'
@@ -429,6 +441,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedHelpRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/vault/': {
id: '/_authenticated/vault/'
path: '/vault'
fullPath: '/vault/'
preLoaderRoute: typeof AuthenticatedVaultIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/roles/': {
id: '/_authenticated/roles/'
path: '/roles'
@@ -628,6 +647,7 @@ interface AuthenticatedRouteChildren {
AuthenticatedRepairBatchesIndexRoute: typeof AuthenticatedRepairBatchesIndexRoute
AuthenticatedRepairsIndexRoute: typeof AuthenticatedRepairsIndexRoute
AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute
AuthenticatedVaultIndexRoute: typeof AuthenticatedVaultIndexRoute
}
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
@@ -654,6 +674,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedRepairBatchesIndexRoute: AuthenticatedRepairBatchesIndexRoute,
AuthenticatedRepairsIndexRoute: AuthenticatedRepairsIndexRoute,
AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute,
AuthenticatedVaultIndexRoute: AuthenticatedVaultIndexRoute,
}
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(

View File

@@ -34,7 +34,7 @@ const STATUS_LABELS: Record<string, string> = {
const ticketColumns: Column<RepairTicket>[] = [
{ key: 'ticket_number', header: 'Ticket #', sortable: true, render: (t) => <span className="font-mono text-sm">{t.ticketNumber}</span> },
{ key: 'instrument', header: 'Instrument', render: (t) => <>{t.instrumentDescription ?? '-'}</> },
{ key: 'item_description', header: 'Item', render: (t) => <>{t.itemDescription ?? '-'}</> },
{ key: 'problem', header: 'Problem', render: (t) => <span className="truncate max-w-[200px] block">{t.problemDescription}</span> },
{ key: 'status', header: 'Status', sortable: true, render: (t) => <Badge variant="outline">{STATUS_LABELS[t.status] ?? t.status}</Badge> },
{
@@ -111,7 +111,7 @@ function RepairBatchDetailPage() {
doc.setFontSize(18)
doc.setFont('helvetica', 'bold')
doc.text('Forte Music', 14, y)
doc.text('LunarFront', 14, y)
y += 8
doc.setFontSize(12)
doc.setFont('helvetica', 'normal')
@@ -169,7 +169,7 @@ function RepairBatchDetailPage() {
doc.setFillColor(245, 245, 245)
doc.rect(14, y - 3, 182, 6, 'F')
doc.text('Ticket #', 16, y)
doc.text('Instrument', 40, y)
doc.text('Item', 40, y)
doc.text('Problem', 100, y)
doc.text('Status', 155, y)
doc.text('Estimate', 190, y, { align: 'right' })
@@ -179,7 +179,7 @@ function RepairBatchDetailPage() {
for (const ticket of tickets) {
if (y > 270) { doc.addPage(); y = 20 }
doc.text(ticket.ticketNumber ?? '-', 16, y)
doc.text((ticket.instrumentDescription ?? '-').slice(0, 30), 40, y)
doc.text((ticket.itemDescription ?? '-').slice(0, 30), 40, y)
doc.text(ticket.problemDescription.slice(0, 28), 100, y)
doc.text(STATUS_LABELS[ticket.status] ?? ticket.status, 155, y)
doc.text(ticket.estimatedCost ? `$${ticket.estimatedCost}` : '-', 190, y, { align: 'right' })

View File

@@ -49,9 +49,9 @@ const columns: Column<RepairBatch>[] = [
},
},
{
key: 'instruments',
header: 'Instruments',
render: (b) => <>{b.receivedCount}/{b.instrumentCount}</>,
key: 'items',
header: 'Items',
render: (b) => <>{b.receivedCount}/{b.itemCount}</>,
},
{
key: 'due_date',

View File

@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
import { useQuery, useMutation } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { RepairBatchCreateSchema } from '@forte/shared/schemas'
import { RepairBatchCreateSchema } from '@lunarfront/shared/schemas'
import { repairBatchMutations } from '@/api/repairs'
import { accountListOptions } from '@/api/accounts'
import { Button } from '@/components/ui/button'
@@ -167,7 +167,7 @@ function NewRepairBatchPage() {
<div className="space-y-2">
<Label>Notes</Label>
<Textarea {...register('notes')} rows={3} placeholder="e.g. Annual instrument checkup, multiple guitars needing setups" />
<Textarea {...register('notes')} rows={3} placeholder="e.g. Annual checkup, multiple items needing service" />
</div>
</CardContent>
</Card>

View File

@@ -118,7 +118,7 @@ function RepairTicketDetailPage() {
setEditFields({
customerName: ticket!.customerName,
customerPhone: ticket!.customerPhone ?? '',
instrumentDescription: ticket!.instrumentDescription ?? '',
itemDescription: ticket!.itemDescription ?? '',
serialNumber: ticket!.serialNumber ?? '',
conditionIn: ticket!.conditionIn ?? '',
conditionInNotes: ticket!.conditionInNotes ?? '',
@@ -134,7 +134,7 @@ function RepairTicketDetailPage() {
const data: Record<string, unknown> = {}
if (editFields.customerName !== ticket!.customerName) data.customerName = editFields.customerName
if (editFields.customerPhone !== (ticket!.customerPhone ?? '')) data.customerPhone = editFields.customerPhone || undefined
if (editFields.instrumentDescription !== (ticket!.instrumentDescription ?? '')) data.instrumentDescription = editFields.instrumentDescription || undefined
if (editFields.itemDescription !== (ticket!.itemDescription ?? '')) data.itemDescription = editFields.itemDescription || undefined
if (editFields.serialNumber !== (ticket!.serialNumber ?? '')) data.serialNumber = editFields.serialNumber || undefined
if (editFields.conditionIn !== (ticket!.conditionIn ?? '')) data.conditionIn = editFields.conditionIn || undefined
if (editFields.conditionInNotes !== (ticket!.conditionInNotes ?? '')) data.conditionInNotes = editFields.conditionInNotes || undefined
@@ -180,7 +180,7 @@ function RepairTicketDetailPage() {
</Button>
<div className="flex-1">
<h1 className="text-2xl font-bold">Ticket #{ticket.ticketNumber}</h1>
<p className="text-sm text-muted-foreground">{ticket.customerName} {ticket.instrumentDescription ?? 'No instrument'}</p>
<p className="text-sm text-muted-foreground">{ticket.customerName} {ticket.itemDescription ?? 'No item description'}</p>
</div>
<PdfModal ticket={ticket} lineItems={lineItemsData?.data ?? []} ticketId={ticketId} />
</div>
@@ -278,7 +278,7 @@ function RepairTicketDetailPage() {
<div className="space-y-2"><Label>Phone</Label><Input value={editFields.customerPhone} onChange={(e) => setEditFields((p) => ({ ...p, customerPhone: e.target.value }))} /></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"><Label>Instrument</Label><Input value={editFields.instrumentDescription} onChange={(e) => setEditFields((p) => ({ ...p, instrumentDescription: e.target.value }))} /></div>
<div className="space-y-2"><Label>Item Description</Label><Input value={editFields.itemDescription} onChange={(e) => setEditFields((p) => ({ ...p, itemDescription: e.target.value }))} /></div>
<div className="space-y-2"><Label>Serial Number</Label><Input value={editFields.serialNumber} onChange={(e) => setEditFields((p) => ({ ...p, serialNumber: e.target.value }))} /></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -310,7 +310,7 @@ function RepairTicketDetailPage() {
<div><span className="text-muted-foreground">Account:</span> {ticket.accountId ?? 'Walk-in'}</div>
</div>
<div className="space-y-2 text-sm">
<div><span className="text-muted-foreground">Instrument:</span> {ticket.instrumentDescription ?? '-'}</div>
<div><span className="text-muted-foreground">Item:</span> {ticket.itemDescription ?? '-'}</div>
<div><span className="text-muted-foreground">Serial:</span> {ticket.serialNumber ?? '-'}</div>
<div><span className="text-muted-foreground">Condition:</span> {ticket.conditionIn ?? '-'}</div>
</div>
@@ -411,8 +411,8 @@ function AddLineItemDialog({ ticketId, open, onOpenChange }: { ticketId: string;
setDescription(''); setQty('1'); setUnitPrice('0'); setCost(''); setItemType('labor'); setTemplateSearch(''); setShowTemplates(false)
}
function selectTemplate(template: { name: string; instrumentType: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ')
function selectTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
const desc = [template.name, template.itemCategory, template.size].filter(Boolean).join(' — ')
setDescription(desc); setItemType(template.itemType); setUnitPrice(template.defaultPrice); setCost(template.defaultCost ?? ''); setShowTemplates(false); setTemplateSearch('')
}
@@ -443,7 +443,7 @@ function AddLineItemDialog({ ticketId, open, onOpenChange }: { ticketId: string;
<div className="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg max-h-48 overflow-auto">
{templates.length === 0 ? <div className="p-3 text-sm text-muted-foreground">No templates found</div> : templates.map((t) => (
<button key={t.id} type="button" className="w-full text-left px-3 py-2 text-sm hover:bg-accent flex justify-between" onClick={() => selectTemplate(t)}>
<span>{t.name}{t.instrumentType ? `${t.instrumentType}` : ''}{t.size ? ` ${t.size}` : ''}</span>
<span>{t.name}{t.itemCategory ? `${t.itemCategory}` : ''}{t.size ? ` ${t.size}` : ''}</span>
<span className="text-muted-foreground">${t.defaultPrice}</span>
</button>
))}

View File

@@ -11,7 +11,7 @@ import { Badge } from '@/components/ui/badge'
import { Plus, Search } from 'lucide-react'
import { useAuthStore } from '@/stores/auth.store'
import type { RepairTicket } from '@/types/repair'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
export const Route = createFileRoute('/_authenticated/repairs/')({
validateSearch: (search: Record<string, unknown>) => ({
@@ -70,9 +70,9 @@ const columns: Column<RepairTicket>[] = [
render: (t) => <span className="font-medium">{t.customerName}</span>,
},
{
key: 'instrument',
header: 'Instrument',
render: (t) => <>{t.instrumentDescription ?? '-'}</>,
key: 'item_description',
header: 'Item',
render: (t) => <>{t.itemDescription ?? '-'}</>,
},
{
key: 'status',

View File

@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
import { useQuery, useMutation } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { RepairTicketCreateSchema } from '@forte/shared/schemas'
import { RepairTicketCreateSchema } from '@lunarfront/shared/schemas'
import { repairTicketMutations, repairLineItemMutations, repairServiceTemplateListOptions } from '@/api/repairs'
import { accountListOptions } from '@/api/accounts'
import { useAuthStore } from '@/stores/auth.store'
@@ -88,7 +88,7 @@ function NewRepairPage() {
defaultValues: {
customerName: linkedContactName ?? '',
customerPhone: '',
instrumentDescription: '',
itemDescription: '',
serialNumber: '',
problemDescription: '',
conditionIn: undefined,
@@ -157,8 +157,8 @@ function NewRepairPage() {
setValue('customerPhone', '')
}
function addFromTemplate(template: { name: string; instrumentType: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ')
function addFromTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
const desc = [template.name, template.itemCategory, template.size].filter(Boolean).join(' — ')
setLineItems((prev) => [...prev, {
itemType: template.itemType,
description: desc,
@@ -297,14 +297,14 @@ function NewRepairPage() {
</CardContent>
</Card>
{/* Instrument Section */}
{/* Item Section */}
<Card>
<CardHeader><CardTitle className="text-lg">Instrument</CardTitle></CardHeader>
<CardHeader><CardTitle className="text-lg">Item</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Instrument Description</Label>
<Input {...register('instrumentDescription')} placeholder="e.g. Yamaha Trumpet YTR-2330" />
<Label>Item Description</Label>
<Input {...register('itemDescription')} placeholder="e.g. Brand, model, description" />
</div>
<div className="space-y-2">
<Label>Serial Number</Label>
@@ -364,7 +364,7 @@ function NewRepairPage() {
) : (
templates.map((t) => (
<button key={t.id} type="button" className="w-full text-left px-3 py-2 text-sm hover:bg-accent flex justify-between" onClick={() => addFromTemplate(t)}>
<span>{t.name}{t.instrumentType ? `${t.instrumentType}` : ''}{t.size ? ` ${t.size}` : ''}</span>
<span>{t.name}{t.itemCategory ? `${t.itemCategory}` : ''}{t.size ? ` ${t.size}` : ''}</span>
<span className="text-muted-foreground">${t.defaultPrice}</span>
</button>
))
@@ -462,7 +462,7 @@ function NewRepairPage() {
<Card>
<CardHeader><CardTitle className="text-lg">Intake Photos</CardTitle></CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">Optional document instrument condition at intake</p>
<p className="text-sm text-muted-foreground">Optional document item condition at intake</p>
<div className="flex flex-wrap gap-3">
{photos.map((photo, i) => (
<div key={i} className="relative group">

View File

@@ -28,7 +28,7 @@ export const Route = createFileRoute('/_authenticated/repairs/templates')({
const columns: Column<RepairServiceTemplate>[] = [
{ key: 'name', header: 'Name', sortable: true, render: (t) => <span className="font-medium">{t.name}</span> },
{ key: 'instrument_type', header: 'Instrument', sortable: true, render: (t) => <>{t.instrumentType ?? '-'}</> },
{ key: 'item_category', header: 'Item Category', sortable: true, render: (t) => <>{t.itemCategory ?? '-'}</> },
{ key: 'size', header: 'Size', render: (t) => <>{t.size ?? '-'}</> },
{ key: 'item_type', header: 'Type', render: (t) => <Badge variant="outline">{t.itemType.replace('_', ' ')}</Badge> },
{ key: 'default_price', header: 'Price', sortable: true, render: (t) => <>${t.defaultPrice}</> },
@@ -110,7 +110,7 @@ function RepairTemplatesPage() {
function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
const queryClient = useQueryClient()
const [name, setName] = useState('')
const [instrumentType, setInstrumentType] = useState('')
const [itemCategory, setItemCategory] = useState('')
const [size, setSize] = useState('')
const [description, setDescription] = useState('')
const [itemType, setItemType] = useState('flat_rate')
@@ -131,7 +131,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
function resetForm() {
setName('')
setInstrumentType('')
setItemCategory('')
setSize('')
setDescription('')
setItemType('flat_rate')
@@ -144,7 +144,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
e.preventDefault()
mutation.mutate({
name,
instrumentType: instrumentType || undefined,
itemCategory: itemCategory || undefined,
size: size || undefined,
description: description || undefined,
itemType,
@@ -168,8 +168,8 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Instrument Type</Label>
<Input value={instrumentType} onChange={(e) => setInstrumentType(e.target.value)} placeholder="e.g. Violin, Trumpet, Guitar" />
<Label>Item Category</Label>
<Input value={itemCategory} onChange={(e) => setItemCategory(e.target.value)} placeholder="e.g. Electronics, Appliances, Furniture" />
</div>
<div className="space-y-2">
<Label>Size</Label>

View File

@@ -48,8 +48,8 @@ function LoginPage() {
style={{ backgroundColor: '#131c2e', borderColor: '#1e2d45' }}
>
<div className="text-center mb-8">
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>Forte</h1>
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Music Store Management</p>
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>LunarFront</h1>
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Small Business Management</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">

View File

@@ -50,7 +50,7 @@ function expandPermissions(slugs: string[]): Set<string> {
function loadSession(): { token: string; user: User; permissions?: string[] } | null {
try {
const raw = sessionStorage.getItem('forte-auth')
const raw = sessionStorage.getItem('lunarfront-auth')
if (!raw) return null
return JSON.parse(raw)
} catch {
@@ -59,11 +59,11 @@ function loadSession(): { token: string; user: User; permissions?: string[] } |
}
function saveSession(token: string, user: User, permissions?: string[]) {
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, permissions }))
sessionStorage.setItem('lunarfront-auth', JSON.stringify({ token, user, permissions }))
}
function clearSession() {
sessionStorage.removeItem('forte-auth')
sessionStorage.removeItem('lunarfront-auth')
}
export const useAuthStore = create<AuthState>((set, get) => {

View File

@@ -22,8 +22,8 @@ function apply(mode: Mode, colorTheme: string) {
}
export const useThemeStore = create<ThemeState>((set) => {
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('forte-mode') as Mode : null) ?? 'system'
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('forte-color-theme') : null) ?? 'slate'
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-mode') as Mode : null) ?? 'system'
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-color-theme') : null) ?? 'slate'
if (typeof window !== 'undefined') {
apply(initialMode, initialColor)
@@ -34,14 +34,14 @@ export const useThemeStore = create<ThemeState>((set) => {
colorTheme: initialColor,
setMode: (mode) => {
localStorage.setItem('forte-mode', mode)
localStorage.setItem('lunarfront-mode', mode)
const colorTheme = useThemeStore.getState().colorTheme
apply(mode, colorTheme)
set({ mode })
},
setColorTheme: (name) => {
localStorage.setItem('forte-color-theme', name)
localStorage.setItem('lunarfront-color-theme', name)
const mode = useThemeStore.getState().mode
apply(mode, name)
set({ colorTheme: name })

View File

@@ -7,7 +7,7 @@ export interface RepairTicket {
customerName: string
customerPhone: string | null
inventoryUnitId: string | null
instrumentDescription: string | null
itemDescription: string | null
serialNumber: string | null
conditionIn: 'excellent' | 'good' | 'fair' | 'poor' | null
conditionInNotes: string | null
@@ -55,7 +55,7 @@ export interface RepairBatch {
dueDate: string | null
completedDate: string | null
deliveredDate: string | null
instrumentCount: number
itemCount: number
receivedCount: number
estimatedTotal: string | null
actualTotal: string | null
@@ -79,7 +79,7 @@ export interface RepairNote {
export interface RepairServiceTemplate {
id: string
name: string
instrumentType: string | null
itemCategory: string | null
size: string | null
description: string | null
itemType: 'labor' | 'part' | 'flat_rate' | 'misc'

View File

@@ -16,17 +16,17 @@ const pages: WikiPage[] = [
title: 'Getting Started',
category: 'General',
content: `
# Getting Started with Forte
# Getting Started with LunarFront
Welcome to Forte — your music store management platform.
Welcome to LunarFront — your small business management platform.
## Signing In
1. Open Forte in your browser
1. Open LunarFront in your browser
2. Enter your email and password
3. Click **Sign in**
If you don't have an account, ask your store manager to create one for you.
If you don't have an account, ask your admin to create one for you.
## Navigation
@@ -34,13 +34,13 @@ Use the sidebar on the left to navigate between sections:
- **Accounts** — manage customer accounts and their members
- **Members** — find and manage individual people across all accounts
- **Repairs** — track instrument repair tickets
- **Repairs** — track repair tickets
- **Repair Batches** — manage bulk school repair jobs
- **Help** — you're here!
## Need Help?
If you can't find what you're looking for, contact your store manager or system administrator.
If you can't find what you're looking for, contact your admin or system administrator.
`.trim(),
},
{
@@ -89,7 +89,7 @@ Every account gets a unique 6-digit number automatically. This number appears in
content: `
# Members
A **member** is an individual person associated with an account. This could be a parent, a child, a student, a band director — anyone who takes lessons, rents instruments, or needs to be tracked.
A **member** is an individual person associated with an account. This could be a parent, a child, a student, a staff member — anyone who takes lessons, uses services, or needs to be tracked.
## Adding a Member
@@ -136,7 +136,7 @@ Payment methods are cards on file for an account. These are used for recurring b
5. Optionally enter card brand, last four digits, and expiration
6. Click **Add Payment Method**
**Note:** Card details are stored securely with the payment processor — Forte only keeps a reference and display info (last 4 digits, brand).
**Note:** Card details are stored securely with the payment processor — LunarFront only keeps a reference and display info (last 4 digits, brand).
## Default Payment Method
@@ -154,7 +154,7 @@ If a payment method was migrated from an old system, it may show a "Needs Update
content: `
# Tax Exemptions
Schools, churches, and resellers may be exempt from sales tax. Forte tracks tax exemption certificates per account.
Schools, churches, and resellers may be exempt from sales tax. LunarFront tracks tax exemption certificates per account.
## Adding a Tax Exemption
@@ -191,7 +191,7 @@ All approvals and revocations are logged with who did it and when.
content: `
# Identity Documents
You can store identity documents (driver's license, passport, school ID) for any member. This is useful for verifying identity during instrument pickups, rentals, or trade-ins.
You can store identity documents (driver's license, passport, school ID) for any member. This is useful for verifying identity during pickups, rentals, or trade-ins.
## Adding an ID
@@ -225,7 +225,7 @@ If a member has multiple IDs, mark one as **Primary** — this is the one shown
content: `
# Users & Roles
Forte uses a permission-based access control system. **Permissions** are specific actions (like "view accounts" or "edit inventory"). **Roles** are named groups of permissions that you assign to users.
LunarFront uses a permission-based access control system. **Permissions** are specific actions (like "view accounts" or "edit inventory"). **Roles** are named groups of permissions that you assign to users.
## Managing Users
@@ -306,16 +306,16 @@ Your preferences are saved in your browser and persist across sessions.
content: `
# Repairs
The Repairs module tracks instrument repair tickets from intake through completion. It supports walk-in customers, account-linked repairs, and bulk school batch jobs.
The Repairs module tracks repair tickets from intake through completion. It supports walk-in customers, account-linked repairs, and bulk school batch jobs.
## Creating a Repair Ticket
1. Go to **Repairs** in the sidebar
2. Click **New Repair**
3. Search for an existing account or enter customer details manually for walk-ins
4. Describe the instrument and the problem
4. Describe the item and the problem
5. Optionally add line items for the estimate (use templates for common services)
6. Add intake photos to document the instrument's condition
6. Add intake photos to document the item's condition
7. Click **Create Ticket**
## Ticket Status Flow
@@ -323,16 +323,16 @@ The Repairs module tracks instrument repair tickets from intake through completi
Each ticket moves through these stages:
- **New** — ticket just created, not yet examined
- **In Transit** — instrument being transported to the shop (for school pickups or shipped instruments)
- **Intake** — instrument received, condition documented
- **Diagnosing** — technician examining the instrument
- **In Transit** — item being transported to the shop (for pickups or shipped items)
- **Intake** — item received, condition documented
- **Diagnosing** — technician examining the item
- **Pending Approval** — estimate provided, waiting for customer OK
- **Approved** — customer authorized the work
- **In Progress** — actively being repaired
- **Pending Parts** — waiting on parts order
- **Ready** — repair complete, awaiting pickup
- **Picked Up** — customer collected the instrument
- **Delivered** — instrument returned via delivery (for school batches)
- **Picked Up** — customer collected the item
- **Delivered** — item returned via delivery (for school batches)
Click the status buttons on the ticket detail page to advance through the workflow. You can also click steps on the progress bar.
@@ -340,7 +340,7 @@ Click the status buttons on the ticket detail page to advance through the workfl
The ticket detail has four tabs:
- **Details** — customer info, instrument, condition, costs. Click **Edit** to modify.
- **Details** — customer info, item, condition, costs. Click **Edit** to modify.
- **Line Items** — labor, parts, flat-rate services, and misc charges. Use the template picker for common repairs.
- **Notes** — running journal of notes. Choose **Internal** (staff only) or **Customer Visible**. You can attach photos to notes.
- **Photos & Docs** — photos organized by repair phase (intake, in progress, completed) plus a documents section for signed approvals, quotes, and receipts.
@@ -372,7 +372,7 @@ Templates are pre-defined common repairs (e.g. "Bow Rehair — Violin — 4/4")
2. Click **New Template**
3. Fill in:
- **Name** — e.g. "Bow Rehair", "String Change", "Valve Overhaul"
- **Instrument Type** — e.g. "Violin", "Guitar", "Trumpet"
- **Item Category** — e.g. "Violin", "Guitar", "Trumpet"
- **Size** — e.g. "4/4", "3/4", "Full"
- **Type** — Labor, Part, Flat Rate, or Misc
- **Default Price** — the customer-facing price
@@ -400,19 +400,19 @@ When creating a ticket or adding line items, type in the **Quick Add from Templa
content: `
# Repair Batches
Batches group multiple repair tickets under one job — typically for schools bringing in many instruments at once.
Batches group multiple repair tickets under one job — typically for schools bringing in many items at once.
## Creating a Batch
1. Go to **Repair Batches** in the sidebar
2. Click **New Batch**
3. Select the school's account
4. Enter contact info, instrument count, and any notes
4. Enter contact info, item count, and any notes
5. Click **Create Batch**
## Adding Tickets to a Batch
When creating new repair tickets, select the batch in the form. Each instrument gets its own ticket linked to the batch.
When creating new repair tickets, select the batch in the form. Each item gets its own ticket linked to the batch.
## Batch Approval
@@ -426,10 +426,10 @@ Only admins can approve or reject batches.
## Batch Status
- **Intake** — receiving instruments
- **Intake** — receiving items
- **In Progress** — work underway
- **Completed** — all repairs done
- **Delivered** — instruments returned to school
- **Delivered** — items returned to customer
## Filtering
@@ -468,7 +468,7 @@ Photos appear inline in the note entry. Click to view full size.
The Photos & Docs tab organizes photos into categories:
- **Intake Photos** — document instrument condition when received
- **Intake Photos** — document item condition when received
- **Work in Progress** — during the repair
- **Completed** — final result after repair
- **Documents** — signed approvals, quotes, receipts (accepts PDFs)

View File

@@ -7,9 +7,9 @@ import { createClient } from './lib/client.js'
// --- Config ---
const DB_HOST = process.env.DB_HOST ?? 'localhost'
const DB_PORT = Number(process.env.DB_PORT ?? '5432')
const DB_USER = process.env.DB_USER ?? 'forte'
const DB_PASS = process.env.DB_PASS ?? 'forte'
const TEST_DB = 'forte_api_test'
const DB_USER = process.env.DB_USER ?? 'lunarfront'
const DB_PASS = process.env.DB_PASS ?? 'lunarfront'
const TEST_DB = 'lunarfront_api_test'
const TEST_PORT = 8001
const BASE_URL = `http://localhost:${TEST_PORT}`
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
@@ -60,7 +60,7 @@ async function setupDatabase() {
`)
// Seed company + location (company table stays as store settings)
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Music Co.', 'America/Chicago')`
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Store', 'America/Chicago')`
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
// Seed lookup tables
@@ -96,8 +96,8 @@ async function setupDatabase() {
{ slug: 'inventory', name: 'Inventory', description: 'Product catalog, stock tracking, and unit management', enabled: true },
{ slug: 'pos', name: 'Point of Sale', description: 'Sales transactions, cash drawer, and receipts', enabled: true },
{ slug: 'repairs', name: 'Repairs', description: 'Repair ticket management, batches, and service templates', enabled: true },
{ slug: 'rentals', name: 'Rentals', description: 'Instrument rental agreements and billing', enabled: false },
{ slug: 'lessons', name: 'Lessons', description: 'Lesson scheduling, instructor management, and billing', enabled: false },
{ slug: 'rentals', name: 'Rentals', description: 'Rental agreements and billing', enabled: false },
{ slug: 'lessons', name: 'Lessons', description: 'Scheduling, staff management, and billing', enabled: false },
{ slug: 'files', name: 'Files', description: 'Shared file storage with folder organization', enabled: true },
{ slug: 'vault', name: 'Vault', description: 'Encrypted password and secret manager', enabled: true },
{ slug: 'email', name: 'Email', description: 'Email campaigns, templates, and sending', enabled: false },
@@ -134,7 +134,7 @@ async function startBackend(): Promise<Subprocess> {
HOST: '0.0.0.0',
NODE_ENV: 'development',
LOG_LEVEL: 'error',
STORAGE_LOCAL_PATH: '/tmp/forte-test-files',
STORAGE_LOCAL_PATH: '/tmp/lunarfront-test-files',
},
stdout: 'pipe',
stderr: 'pipe',
@@ -170,7 +170,7 @@ async function registerTestUser(): Promise<string> {
method: 'POST',
headers,
body: JSON.stringify({
email: 'test@forte.dev',
email: 'test@lunarfront.dev',
password: testPassword,
firstName: 'Test',
lastName: 'Runner',
@@ -193,7 +193,7 @@ async function registerTestUser(): Promise<string> {
const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@forte.dev', password: testPassword }),
body: JSON.stringify({ email: 'test@lunarfront.dev', password: testPassword }),
})
const loginData = await loginRes.json() as { token?: string }
if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`)

View File

@@ -304,7 +304,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
t.test('cannot disable yourself', { tags: ['users', 'status'] }, async () => {
// Get current user ID from the users list
const usersRes = await t.api.get('/v1/users')
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@forte.dev')
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@lunarfront.dev')
t.assert.ok(currentUser)
const res = await t.api.patch(`/v1/users/${currentUser.id}/status`, { isActive: false })

View File

@@ -7,8 +7,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-tickets', {
customerName: 'Walk-In Customer',
customerPhone: '555-0100',
instrumentDescription: 'Yamaha Trumpet',
problemDescription: 'Stuck valve, needs cleaning',
itemDescription: 'Samsung Galaxy S24',
problemDescription: 'Cracked screen, touch not working',
conditionIn: 'fair',
})
t.assert.status(res, 201)
@@ -25,8 +25,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-tickets', {
customerName: 'Repair Customer',
accountId: acct.data.id,
problemDescription: 'Broken bridge on violin',
instrumentDescription: 'Student Violin 4/4',
problemDescription: 'Screen flickering intermittently',
itemDescription: 'Dell XPS 15 Laptop',
conditionIn: 'poor',
})
t.assert.status(res, 201)
@@ -164,12 +164,12 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
t.assert.ok(res.data.data.some((t: { customerName: string }) => t.customerName === 'Searchable Trumpet Guy'))
})
t.test('searches tickets by instrument description', { tags: ['tickets', 'search'] }, async () => {
await t.api.post('/v1/repair-tickets', { customerName: 'Instrument Search', problemDescription: 'Test', instrumentDescription: 'Selmer Mark VI Saxophone' })
t.test('searches tickets by item description', { tags: ['tickets', 'search'] }, async () => {
await t.api.post('/v1/repair-tickets', { customerName: 'Item Search', problemDescription: 'Test', itemDescription: 'Samsung Galaxy S24 Ultra' })
const res = await t.api.get('/v1/repair-tickets', { q: 'Mark VI' })
const res = await t.api.get('/v1/repair-tickets', { q: 'Galaxy S24' })
t.assert.status(res, 200)
t.assert.ok(res.data.data.some((t: { instrumentDescription: string }) => t.instrumentDescription?.includes('Mark VI')))
t.assert.ok(res.data.data.some((t: { itemDescription: string }) => t.itemDescription?.includes('Galaxy S24')))
})
t.test('sorts tickets by customer name descending', { tags: ['tickets', 'sort'] }, async () => {
@@ -289,7 +289,7 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Customer Note', problemDescription: 'Test' })
const res = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, {
content: 'Your instrument is ready for pickup',
content: 'Your item is ready for pickup',
visibility: 'customer',
})
t.assert.status(res, 201)
@@ -341,17 +341,17 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-batches', {
accountId: acct.data.id,
contactName: 'Band Director',
contactName: 'IT Director',
contactPhone: '555-0200',
instrumentCount: 15,
notes: 'Annual instrument checkup',
itemCount: 15,
notes: 'Annual equipment checkup',
})
t.assert.status(res, 201)
t.assert.ok(res.data.batchNumber)
t.assert.equal(res.data.batchNumber.length, 6)
t.assert.equal(res.data.status, 'intake')
t.assert.equal(res.data.approvalStatus, 'pending')
t.assert.equal(res.data.instrumentCount, 15)
t.assert.equal(res.data.itemCount, 15)
})
t.test('returns 404 for missing batch', { tags: ['batches', 'read'] }, async () => {
@@ -377,19 +377,19 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
t.test('updates a batch', { tags: ['batches', 'update'] }, async () => {
const acct = await t.api.post('/v1/accounts', { name: 'Update Batch School', billingMode: 'consolidated' })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 5 })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 5 })
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { instrumentCount: 10, contactName: 'Updated Director' })
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { itemCount: 10, contactName: 'Updated Director' })
t.assert.status(res, 200)
t.assert.equal(res.data.contactName, 'Updated Director')
})
t.test('adds tickets to a batch and lists them', { tags: ['batches', 'tickets'] }, async () => {
const acct = await t.api.post('/v1/accounts', { name: 'Batch Tickets School', billingMode: 'consolidated' })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 2 })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 2 })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Flute pads', instrumentDescription: 'Flute' })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Clarinet cork', instrumentDescription: 'Clarinet' })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Screen cracked', itemDescription: 'Chromebook #1' })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Battery dead', itemDescription: 'Chromebook #2' })
const tickets = await t.api.get(`/v1/repair-batches/${batch.data.id}/tickets`, { limit: 100 })
t.assert.status(tickets, 200)
@@ -437,36 +437,36 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
t.test('creates a service template', { tags: ['templates', 'create'] }, async () => {
const res = await t.api.post('/v1/repair-service-templates', {
name: 'Bow Rehair',
instrumentType: 'Violin',
size: '4/4',
name: 'Screen Repair',
itemCategory: 'Electronics',
size: 'Phone',
itemType: 'flat_rate',
defaultPrice: 65,
defaultCost: 15,
})
t.assert.status(res, 201)
t.assert.equal(res.data.name, 'Bow Rehair')
t.assert.equal(res.data.instrumentType, 'Violin')
t.assert.equal(res.data.size, '4/4')
t.assert.equal(res.data.name, 'Screen Repair')
t.assert.equal(res.data.itemCategory, 'Electronics')
t.assert.equal(res.data.size, 'Phone')
t.assert.equal(res.data.defaultPrice, '65.00')
t.assert.equal(res.data.defaultCost, '15.00')
})
t.test('lists service templates with search', { tags: ['templates', 'read'] }, async () => {
await t.api.post('/v1/repair-service-templates', { name: 'String Change', instrumentType: 'Guitar', defaultPrice: 25 })
await t.api.post('/v1/repair-service-templates', { name: 'Battery Replacement', itemCategory: 'Electronics', defaultPrice: 25 })
const res = await t.api.get('/v1/repair-service-templates', { q: 'String', limit: 100 })
const res = await t.api.get('/v1/repair-service-templates', { q: 'Battery', limit: 100 })
t.assert.status(res, 200)
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'String Change'))
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'Battery Replacement'))
t.assert.ok(res.data.pagination)
})
t.test('updates a service template', { tags: ['templates', 'update'] }, async () => {
const created = await t.api.post('/v1/repair-service-templates', { name: 'Pad Replace', defaultPrice: 30 })
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, instrumentType: 'Clarinet' })
const created = await t.api.post('/v1/repair-service-templates', { name: 'Tune-Up', defaultPrice: 30 })
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, itemCategory: 'Bicycles' })
t.assert.status(res, 200)
t.assert.equal(res.data.defaultPrice, '35.00')
t.assert.equal(res.data.instrumentType, 'Clarinet')
t.assert.equal(res.data.itemCategory, 'Bicycles')
})
t.test('soft-deletes a service template', { tags: ['templates', 'delete'] }, async () => {

View File

@@ -96,14 +96,14 @@ suite('Vault', { tags: ['vault'] }, (t) => {
const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
name: 'Store WiFi',
username: 'ForteMusic',
username: 'DemoUser',
url: 'http://192.168.1.1',
notes: 'Router admin panel',
secret: 'supersecretpassword123',
})
t.assert.status(res, 201)
t.assert.equal(res.data.name, 'Store WiFi')
t.assert.equal(res.data.username, 'ForteMusic')
t.assert.equal(res.data.username, 'DemoUser')
t.assert.equal(res.data.hasSecret, true)
// Secret value should NOT be in the response
t.assert.falsy(res.data.encryptedValue)

View File

@@ -27,7 +27,7 @@ async function dav(
suite('WebDAV', { tags: ['webdav', 'storage'] }, (t) => {
// Use the same test user created by the test runner
const email = 'test@forte.dev'
const email = 'test@lunarfront.dev'
const password = 'testpassword1234'
const basicAuth = 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64')
const badAuth = 'Basic ' + Buffer.from(`${email}:wrongpassword`).toString('base64')

View File

@@ -5,6 +5,6 @@ export default defineConfig({
out: './src/db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte',
url: process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront',
},
})

View File

@@ -1,5 +1,5 @@
{
"name": "@forte/backend",
"name": "@lunarfront/backend",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -13,6 +13,8 @@
"db:generate": "bunx drizzle-kit generate",
"db:migrate": "bunx drizzle-kit migrate",
"db:seed-dev": "bun run src/db/seeds/dev-seed.ts",
"db:seed-music": "bun run src/db/seeds/music-store-seed.ts",
"db:seed-reset-repairs": "bun run src/db/seeds/reset-repairs.ts",
"db:seed": "bun run src/db/seed.ts"
},
"dependencies": {
@@ -20,7 +22,7 @@
"@fastify/jwt": "^9",
"@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0",
"@forte/shared": "workspace:*",
"@lunarfront/shared": "workspace:*",
"bcrypt": "^6",
"drizzle-orm": "^0.38",
"fastify": "^5",

View File

@@ -0,0 +1,14 @@
-- Generalize music-specific column names to generic terminology
-- repair_ticket: instrument_description -> item_description
ALTER TABLE repair_ticket RENAME COLUMN instrument_description TO item_description;
-- repair_batch: instrument_count -> item_count
ALTER TABLE repair_batch RENAME COLUMN instrument_count TO item_count;
-- repair_service_template: instrument_type -> item_category
ALTER TABLE repair_service_template RENAME COLUMN instrument_type TO item_category;
-- Update module descriptions to be industry-agnostic
UPDATE module_config SET description = 'Rental agreements and billing' WHERE slug = 'rentals';
UPDATE module_config SET description = 'Scheduling, staff management, and billing' WHERE slug = 'lessons';

View File

@@ -190,6 +190,13 @@
"when": 1774860000000,
"tag": "0026_modules",
"breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1774870000000,
"tag": "0027_generalize_terminology",
"breakpoints": true
}
]
}

View File

@@ -82,7 +82,7 @@ export const repairBatches = pgTable('repair_batch', {
dueDate: timestamp('due_date', { withTimezone: true }),
completedDate: timestamp('completed_date', { withTimezone: true }),
deliveredDate: timestamp('delivered_date', { withTimezone: true }),
instrumentCount: integer('instrument_count').notNull().default(0),
itemCount: integer('item_count').notNull().default(0),
receivedCount: integer('received_count').notNull().default(0),
estimatedTotal: numeric('estimated_total', { precision: 10, scale: 2 }),
actualTotal: numeric('actual_total', { precision: 10, scale: 2 }),
@@ -101,7 +101,7 @@ export const repairTickets = pgTable('repair_ticket', {
customerName: varchar('customer_name', { length: 255 }).notNull(),
customerPhone: varchar('customer_phone', { length: 50 }),
inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id),
instrumentDescription: text('instrument_description'),
itemDescription: text('item_description'),
serialNumber: varchar('serial_number', { length: 255 }),
conditionIn: repairConditionInEnum('condition_in'),
conditionInNotes: text('condition_in_notes'),
@@ -158,7 +158,7 @@ export type RepairNoteInsert = typeof repairNotes.$inferInsert
export const repairServiceTemplates = pgTable('repair_service_template', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 255 }).notNull(),
instrumentType: varchar('instrument_type', { length: 100 }),
itemCategory: varchar('item_category', { length: 100 }),
size: varchar('size', { length: 50 }),
description: text('description'),
itemType: repairLineItemTypeEnum('item_type').notNull().default('flat_rate'),

View File

@@ -7,7 +7,7 @@ const DEV_LOCATION_ID = '00000000-0000-0000-0000-000000000010'
async function seed() {
const connectionString =
process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
const sql = postgres(connectionString)
const db = drizzle(sql)
@@ -17,7 +17,7 @@ async function seed() {
.insert(companies)
.values({
id: DEV_COMPANY_ID,
name: 'Dev Music Co.',
name: 'Dev Store',
timezone: 'America/Chicago',
})
.onConflictDoNothing()

View File

@@ -6,7 +6,7 @@
*/
import postgres from 'postgres'
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
const sql = postgres(DB_URL)
@@ -17,7 +17,7 @@ async function seed() {
// Create company and location if they don't exist
const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
if (!company) {
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Forte Music Store', 'America/Chicago')`
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Demo Store', 'America/Chicago')`
await sql`INSERT INTO location (id, name) VALUES ('a0000000-0000-0000-0000-000000000002', 'Main Store')`
console.log(' Created company and location')
@@ -39,16 +39,16 @@ async function seed() {
}
// --- Admin user (if not exists) ---
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@forte.dev'`
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@lunarfront.dev'`
if (!adminUser) {
const bcrypt = await import('bcrypt')
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@forte.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@lunarfront.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
}
console.log(' Created admin user: admin@forte.dev / admin1234')
console.log(' Created admin user: admin@lunarfront.dev / admin1234')
} else {
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
@@ -61,11 +61,11 @@ async function seed() {
const accounts = [
{ name: 'Smith Family', email: 'smith@example.com', phone: '555-0101' },
{ name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102' },
{ name: 'Lincoln High School', email: 'band@lincoln.edu', phone: '555-0200' },
{ name: 'Garcia Music Studio', email: 'garcia@studio.com', phone: '555-0103' },
{ name: 'Lincoln High School', email: 'office@lincoln.edu', phone: '555-0200' },
{ name: 'Garcia Workshop', email: 'garcia@studio.com', phone: '555-0103' },
{ name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104' },
{ name: 'Emily Chen', email: 'emily.chen@email.com', phone: '555-0105' },
{ name: 'Westside Church', email: 'music@westsidechurch.org', phone: '555-0300' },
{ name: 'Westside Church', email: 'admin@westsidechurch.org', phone: '555-0300' },
{ name: 'Oak Elementary', email: 'office@oakelementary.edu', phone: '555-0201' },
]
@@ -89,7 +89,7 @@ async function seed() {
{ accountName: 'Smith Family', firstName: 'Tommy', lastName: 'Smith', isMinor: true },
{ accountName: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com' },
{ accountName: 'Johnson Family', firstName: 'Jake', lastName: 'Johnson', isMinor: true },
{ accountName: 'Garcia Music Studio', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
{ accountName: 'Garcia Workshop', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
{ accountName: 'Mike Thompson', firstName: 'Mike', lastName: 'Thompson', email: 'mike.t@email.com' },
{ accountName: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com' },
]
@@ -105,39 +105,37 @@ async function seed() {
// --- Repair Service Templates ---
const templates = [
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '65.00', cost: '15.00' },
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '3/4', itemType: 'flat_rate', price: '55.00', cost: '12.00' },
{ name: 'Bow Rehair', instrumentType: 'Cello', size: null, itemType: 'flat_rate', price: '80.00', cost: '20.00' },
{ name: 'Bow Rehair', instrumentType: 'Bass', size: null, itemType: 'flat_rate', price: '90.00', cost: '25.00' },
{ name: 'String Change', instrumentType: 'Guitar', size: 'Acoustic', itemType: 'flat_rate', price: '25.00', cost: '8.00' },
{ name: 'String Change', instrumentType: 'Guitar', size: 'Electric', itemType: 'flat_rate', price: '25.00', cost: '7.00' },
{ name: 'String Change', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '35.00', cost: '12.00' },
{ name: 'Valve Overhaul', instrumentType: 'Trumpet', size: null, itemType: 'labor', price: '85.00', cost: null },
{ name: 'Pad Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '120.00', cost: '30.00' },
{ name: 'Pad Replacement', instrumentType: 'Flute', size: null, itemType: 'flat_rate', price: '110.00', cost: '25.00' },
{ name: 'Cork Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '45.00', cost: '5.00' },
{ name: 'Slide Repair', instrumentType: 'Trombone', size: null, itemType: 'labor', price: '75.00', cost: null },
{ name: 'Bridge Setup', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '40.00', cost: '10.00' },
{ name: 'Guitar Setup', instrumentType: 'Guitar', size: null, itemType: 'flat_rate', price: '65.00', cost: '5.00' },
{ name: 'Dent Removal', instrumentType: 'Brass', size: null, itemType: 'labor', price: '50.00', cost: null },
{ name: 'General Cleaning', instrumentType: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '89.00', cost: '25.00' },
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Tablet', itemType: 'flat_rate', price: '129.00', cost: '45.00' },
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '59.00', cost: '15.00' },
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Laptop', itemType: 'flat_rate', price: '99.00', cost: '35.00' },
{ name: 'Tune-Up', itemCategory: 'Bicycles', size: 'Standard', itemType: 'flat_rate', price: '65.00', cost: '10.00' },
{ name: 'Brake Adjustment', itemCategory: 'Bicycles', size: null, itemType: 'flat_rate', price: '35.00', cost: '5.00' },
{ name: 'Blade Sharpening', itemCategory: 'Tools', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
{ name: 'Motor Repair', itemCategory: 'Appliances', size: null, itemType: 'labor', price: '85.00', cost: null },
{ name: 'Zipper Replacement', itemCategory: 'Clothing', size: null, itemType: 'flat_rate', price: '25.00', cost: '5.00' },
{ name: 'Sole Replacement', itemCategory: 'Footwear', size: null, itemType: 'flat_rate', price: '55.00', cost: '15.00' },
{ name: 'Watch Battery', itemCategory: 'Watches', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
{ name: 'Furniture Refinishing', itemCategory: 'Furniture', size: null, itemType: 'labor', price: '150.00', cost: null },
{ name: 'Diagnostic Check', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
{ name: 'General Cleaning', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
]
for (const t of templates) {
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(instrument_type, '') = ${t.instrumentType ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(item_category, '') = ${t.itemCategory ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
if (existing.length > 0) continue
await sql`INSERT INTO repair_service_template (name, instrument_type, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.instrumentType}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
console.log(` Template: ${t.name} ${t.instrumentType ?? ''} ${t.size ?? ''}`)
await sql`INSERT INTO repair_service_template (name, item_category, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.itemCategory}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
console.log(` Template: ${t.name} ${t.itemCategory ?? ''} ${t.size ?? ''}`)
}
// --- Repair Tickets ---
const tickets = [
{ customer: 'Mike Thompson', instrument: 'Fender Stratocaster', serial: 'US22-045891', problem: 'Fret buzz on 3rd and 5th fret, needs setup', condition: 'good', status: 'in_progress', estimate: '65.00' },
{ customer: 'Emily Chen', instrument: 'Yamaha YTR-2330 Trumpet', serial: 'YTR-78432', problem: 'Stuck 2nd valve, sluggish action on all valves', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
{ customer: 'David Smith', instrument: 'Stradivarius Copy Violin', serial: null, problem: 'Bow needs rehair, bridge slightly warped', condition: 'fair', status: 'ready', estimate: '105.00' },
{ customer: 'Carlos Garcia', instrument: 'Martin D-28 Acoustic Guitar', serial: 'M2284563', problem: 'Broken tuning peg, needs replacement', condition: 'good', status: 'new', estimate: null },
{ customer: 'Lisa Johnson', instrument: 'Yamaha YCL-255 Clarinet', serial: null, problem: 'Several pads worn, keys sticking', condition: 'poor', status: 'diagnosing', estimate: null },
{ customer: 'Walk-In Customer', instrument: 'Unknown Flute', serial: null, problem: 'Customer says it squeaks on high notes', condition: 'fair', status: 'intake', estimate: null },
{ customer: 'Mike Thompson', item: 'Samsung Galaxy S24', serial: 'IMEI-354789102', problem: 'Cracked screen, touch not responsive in bottom half', condition: 'good', status: 'in_progress', estimate: '89.00' },
{ customer: 'Emily Chen', item: 'HP LaserJet Pro M404', serial: 'HP-CNB3K12345', problem: 'Paper jam sensor error, won\'t feed from tray 2', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
{ customer: 'David Smith', item: 'Trek Marlin 7 Mountain Bike', serial: null, problem: 'Rear derailleur bent, chain skipping gears', condition: 'fair', status: 'ready', estimate: '65.00' },
{ customer: 'Carlos Garcia', item: 'KitchenAid Stand Mixer KSM150', serial: 'W10807813', problem: 'Motor making grinding noise at low speeds', condition: 'good', status: 'new', estimate: null },
{ customer: 'Lisa Johnson', item: 'Apple MacBook Pro 14"', serial: null, problem: 'Battery draining rapidly, trackpad click intermittent', condition: 'poor', status: 'diagnosing', estimate: null },
{ customer: 'Walk-In Customer', item: 'Leather Work Boots', serial: null, problem: 'Sole separating from upper on both shoes', condition: 'fair', status: 'intake', estimate: null },
]
for (const t of tickets) {
@@ -145,8 +143,8 @@ async function seed() {
if (existing.length > 0) continue
const num = String(Math.floor(100000 + Math.random() * 900000))
const acctId = acctIds[t.customer] ?? null
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, instrument_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.instrument}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
console.log(` Ticket: ${t.customer}${t.instrument} [${t.status}]`)
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, item_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.item}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
console.log(` Ticket: ${t.customer}${t.item} [${t.status}]`)
}
// --- Repair Batch ---
@@ -154,22 +152,22 @@ async function seed() {
if (batchExists.length === 0) {
const batchNum = String(Math.floor(100000 + Math.random() * 900000))
const schoolId = acctIds['Lincoln High School']
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, instrument_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual band instrument checkup — 5 instruments', 'intake') RETURNING id`
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, item_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual equipment checkup — 5 items', 'intake') RETURNING id`
const batchTickets = [
{ instrument: 'Student Flute', problem: 'Pads worn, needs replacement check', condition: 'fair' },
{ instrument: 'Student Clarinet #1', problem: 'Keys sticking, cork dried out', condition: 'fair' },
{ instrument: 'Student Clarinet #2', problem: 'Barrel crack, needs assessment', condition: 'poor' },
{ instrument: 'Student Trumpet', problem: 'Valve oil needed, general checkup', condition: 'good' },
{ instrument: 'Student Trombone', problem: 'Slide dent, sluggish movement', condition: 'fair' },
{ item: 'Chromebook #101', problem: 'Screen flickering, hinge loose', condition: 'fair' },
{ item: 'Chromebook #102', problem: 'Keyboard unresponsive, several keys stuck', condition: 'fair' },
{ item: 'Projector — Epson EB-X51', problem: 'Lamp dim, color wheel noise', condition: 'poor' },
{ item: 'Label Printer — Dymo 450', problem: 'Feed mechanism jammed', condition: 'good' },
{ item: 'PA Speaker — JBL EON715', problem: 'Crackling at high volume', condition: 'fair' },
]
for (const bt of batchTickets) {
const num = String(Math.floor(100000 + Math.random() * 900000))
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, instrument_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.instrument}, ${bt.problem}, ${bt.condition}, 'new')`
console.log(` Batch ticket: ${bt.instrument}`)
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, item_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.item}, ${bt.problem}, ${bt.condition}, 'new')`
console.log(` Batch ticket: ${bt.item}`)
}
console.log(` Batch: Lincoln High School — 5 instruments`)
console.log(` Batch: Lincoln High School — 5 items`)
}
console.log('\nDev seed complete!')

View File

@@ -20,14 +20,14 @@ export const SYSTEM_PERMISSIONS = [
{ slug: 'pos.admin', domain: 'pos', action: 'admin', description: 'Void transactions, override prices, manage discounts' },
// Rentals
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, fleet, billing' },
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage fleet' },
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, inventory, billing' },
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage rental inventory' },
{ slug: 'rentals.admin', domain: 'rentals', action: 'admin', description: 'Override terms, adjust equity, cancel contracts' },
// Lessons
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View lesson schedules, enrollments, attendance' },
// Lessons / Scheduling
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View schedules, enrollments, attendance' },
{ slug: 'lessons.edit', domain: 'lessons', action: 'edit', description: 'Manage scheduling, enrollment, attendance' },
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure lesson settings, manage instructors' },
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure scheduling settings, manage staff' },
// Repairs
{ slug: 'repairs.view', domain: 'repairs', action: 'view', description: 'View repair tickets, parts inventory' },

View File

@@ -80,7 +80,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
const authHeader = request.headers.authorization
if (!authHeader || !authHeader.startsWith('Basic ')) {
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Authentication required')
}
@@ -88,7 +88,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
const colonIndex = decoded.indexOf(':')
if (colonIndex === -1) {
recordFailure(ip)
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Invalid credentials')
}
@@ -103,14 +103,14 @@ export function webdavBasicAuth(app: FastifyInstance) {
if (!user || !user.isActive) {
recordFailure(ip)
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Invalid credentials')
}
const valid = await bcrypt.compare(password, user.passwordHash)
if (!valid) {
recordFailure(ip)
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Invalid credentials')
}

View File

@@ -13,7 +13,7 @@ import {
TaxExemptionUpdateSchema,
MemberIdentifierCreateSchema,
MemberIdentifierUpdateSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
AccountService,
MemberService,

View File

@@ -1,7 +1,7 @@
import type { FastifyPluginAsync } from 'fastify'
import { eq } from 'drizzle-orm'
import bcrypt from 'bcrypt'
import { RegisterSchema, LoginSchema } from '@forte/shared/schemas'
import { RegisterSchema, LoginSchema } from '@lunarfront/shared/schemas'
import { users } from '../../db/schema/users.js'
const SALT_ROUNDS = 10

View File

@@ -5,7 +5,7 @@ import {
SupplierCreateSchema,
SupplierUpdateSchema,
PaginationSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import { CategoryService, SupplierService } from '../../services/inventory.service.js'
export const inventoryRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from 'fastify'
import { LookupCreateSchema, LookupUpdateSchema } from '@forte/shared/schemas'
import { LookupCreateSchema, LookupUpdateSchema } from '@lunarfront/shared/schemas'
import { UnitStatusService, ItemConditionService } from '../../services/lookup.service.js'
import { ConflictError, ValidationError } from '../../lib/errors.js'

View File

@@ -5,7 +5,7 @@ import {
InventoryUnitCreateSchema,
InventoryUnitUpdateSchema,
PaginationSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import { ProductService, InventoryUnitService } from '../../services/product.service.js'
export const productRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsync } from 'fastify'
import { eq, count, sql, type Column } from 'drizzle-orm'
import { PaginationSchema } from '@forte/shared/schemas'
import { PaginationSchema } from '@lunarfront/shared/schemas'
import { RbacService } from '../../services/rbac.service.js'
import { ValidationError } from '../../lib/errors.js'
import { users } from '../../db/schema/users.js'

View File

@@ -12,7 +12,7 @@ import {
RepairNoteCreateSchema,
RepairServiceTemplateCreateSchema,
RepairServiceTemplateUpdateSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js'
export const repairRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsync } from 'fastify'
import multipart from '@fastify/multipart'
import { PaginationSchema } from '@forte/shared/schemas'
import { PaginationSchema } from '@lunarfront/shared/schemas'
import { StorageFolderService, StorageFileService, StoragePermissionService } from '../../services/storage.service.js'
import { ValidationError } from '../../lib/errors.js'

View File

@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from 'fastify'
import { PaginationSchema } from '@forte/shared/schemas'
import { PaginationSchema } from '@lunarfront/shared/schemas'
import { VaultKeyService, VaultPermissionService, VaultCategoryService, VaultEntryService } from '../../services/vault.service.js'
import { ValidationError } from '../../lib/errors.js'

View File

@@ -20,8 +20,8 @@ import type {
TaxExemptionCreateInput,
TaxExemptionUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
import { isMinor, normalizeStateCode } from '@forte/shared/utils'
} from '@lunarfront/shared/schemas'
import { isMinor, normalizeStateCode } from '@lunarfront/shared/utils'
import {
withPagination,
withSort,

View File

@@ -7,7 +7,7 @@ import type {
SupplierCreateInput,
SupplierUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
withPagination,
withSort,

View File

@@ -7,7 +7,7 @@ import {
SYSTEM_UNIT_STATUSES,
SYSTEM_ITEM_CONDITIONS,
} from '../db/schema/lookups.js'
import type { LookupCreateInput, LookupUpdateInput } from '@forte/shared/schemas'
import type { LookupCreateInput, LookupUpdateInput } from '@lunarfront/shared/schemas'
function createLookupService(
table: typeof inventoryUnitStatuses | typeof itemConditions,

View File

@@ -8,7 +8,7 @@ import type {
InventoryUnitCreateInput,
InventoryUnitUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
withPagination,
withSort,

View File

@@ -1,6 +1,6 @@
import { eq, and, inArray, count, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
import { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js'
import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js'
import { ForbiddenError } from '../lib/errors.js'

View File

@@ -18,7 +18,7 @@ import type {
RepairServiceTemplateCreateInput,
RepairServiceTemplateUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
withPagination,
withSort,
@@ -59,7 +59,7 @@ export const RepairTicketService = {
locationId: input.locationId,
repairBatchId: input.repairBatchId,
inventoryUnitId: input.inventoryUnitId,
instrumentDescription: input.instrumentDescription,
itemDescription: input.itemDescription,
serialNumber: input.serialNumber,
conditionIn: input.conditionIn,
conditionInNotes: input.conditionInNotes,
@@ -101,7 +101,7 @@ export const RepairTicketService = {
repairTickets.ticketNumber,
repairTickets.customerName,
repairTickets.customerPhone,
repairTickets.instrumentDescription,
repairTickets.itemDescription,
repairTickets.serialNumber,
])
if (search) conditions.push(search)
@@ -151,7 +151,7 @@ export const RepairTicketService = {
async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) {
const baseWhere = eq(repairTickets.repairBatchId, batchId)
const searchCondition = params.q
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription])
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.itemDescription])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
@@ -292,7 +292,7 @@ export const RepairBatchService = {
contactEmail: input.contactEmail,
pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined,
dueDate: input.dueDate ? new Date(input.dueDate) : undefined,
instrumentCount: input.instrumentCount,
itemCount: input.itemCount,
notes: input.notes,
})
.returning()
@@ -391,7 +391,7 @@ export const RepairServiceTemplateService = {
.insert(repairServiceTemplates)
.values({
name: input.name,
instrumentType: input.instrumentType,
itemCategory: input.itemCategory,
size: input.size,
description: input.description,
itemType: input.itemType,
@@ -406,13 +406,13 @@ export const RepairServiceTemplateService = {
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
const baseWhere = eq(repairServiceTemplates.isActive, true)
const searchCondition = params.q
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.instrumentType, repairServiceTemplates.size, repairServiceTemplates.description])
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.itemCategory, repairServiceTemplates.size, repairServiceTemplates.description])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
name: repairServiceTemplates.name,
instrument_type: repairServiceTemplates.instrumentType,
item_category: repairServiceTemplates.itemCategory,
default_price: repairServiceTemplates.defaultPrice,
sort_order: repairServiceTemplates.sortOrder,
created_at: repairServiceTemplates.createdAt,

View File

@@ -5,7 +5,7 @@ import { userRoles } from '../db/schema/rbac.js'
import type { StorageProvider } from '../storage/index.js'
import { randomUUID } from 'crypto'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
const MAX_PARENT_DEPTH = 50

View File

@@ -3,7 +3,7 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { vaultConfig, vaultCategories, vaultCategoryPermissions, vaultEntries } from '../db/schema/vault.js'
import { userRoles } from '../db/schema/rbac.js'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'crypto'
import bcrypt from 'bcrypt'

View File

@@ -1,6 +1,6 @@
import { sql, asc, desc, ilike, or, type SQL, type Column } from 'drizzle-orm'
import type { PgSelect } from 'drizzle-orm/pg-core'
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
/**
* Apply pagination (offset + limit) to a Drizzle query.

View File

@@ -1,5 +1,5 @@
{
"name": "@forte/shared",
"name": "@lunarfront/shared",
"version": "0.0.1",
"private": true,
"type": "module",

View File

@@ -36,7 +36,7 @@ export const RepairTicketCreateSchema = z.object({
locationId: opt(z.string().uuid()),
repairBatchId: opt(z.string().uuid()),
inventoryUnitId: opt(z.string().uuid()),
instrumentDescription: opt(z.string()),
itemDescription: opt(z.string()),
serialNumber: opt(z.string().max(255)),
conditionIn: opt(RepairConditionIn),
conditionInNotes: opt(z.string()),
@@ -84,7 +84,7 @@ export const RepairBatchCreateSchema = z.object({
contactEmail: opt(z.string().email()),
pickupDate: opt(z.string()),
dueDate: opt(z.string()),
instrumentCount: z.coerce.number().int().min(0).default(0),
itemCount: z.coerce.number().int().min(0).default(0),
notes: opt(z.string()),
})
export type RepairBatchCreateInput = z.infer<typeof RepairBatchCreateSchema>
@@ -112,7 +112,7 @@ export type RepairNoteCreateInput = z.infer<typeof RepairNoteCreateSchema>
export const RepairServiceTemplateCreateSchema = z.object({
name: z.string().min(1).max(255),
instrumentType: opt(z.string().max(100)),
itemCategory: opt(z.string().max(100)),
size: opt(z.string().max(50)),
description: opt(z.string()),
itemType: RepairLineItemType.default('flat_rate'),

View File

@@ -1,4 +1,4 @@
// @forte/shared types
// @lunarfront/shared types
// Domain types will be added as each domain is implemented
export type StoreId = string

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform
LunarFront — Small Business Management Platform
Implementation Roadmap
@@ -8,23 +8,23 @@ Version 1.0 | Draft
# 1. Purpose
This document defines the phased implementation order for Forte — a music store management platform built by Lunarfront Tech LLC. Each phase builds on the previous, produces independently testable output, and is scoped for a solo developer working in 2-4 week increments. The goal is a working POS as early as possible, then layer on domain complexity.
This document defines the phased implementation order for LunarFront — a music store management platform built by Lunarfront Tech LLC. Each phase builds on the previous, produces independently testable output, and is scoped for a solo developer working in 2-4 week increments. The goal is a working POS as early as possible, then layer on domain complexity.
Tech stack: TypeScript / Bun / Fastify / Drizzle ORM / Zod / BullMQ / PostgreSQL 16 / Valkey 8 / Turborepo monorepo. See `17_Backend_Technical_Architecture.md` for full stack details.
## 1.1 Project Conventions
Name | Value
App name | Forte
Package namespace | `@forte/shared`, `@forte/backend`, etc.
App name | LunarFront
Package namespace | `@lunarfront/shared`, `@lunarfront/backend`, etc.
Database (dev) | `forte`
Database (test) | `forte_test`
Database (test) | `lunarfront_test`
Logging | JSON structured logging via Pino (Fastify built-in)
Linting | ESLint + Prettier at monorepo root
Auth (Phase 1-2) | Dev bypass via `X-Dev-User` header — JWT planned, wired in Phase 2
Auth (Phase 2+) | Self-issued JWTs + bcrypt, swap to Clerk/Auth0 later
Request tracing | Auto-generated request IDs via Fastify (included from Phase 1)
Test strategy | Separate `forte_test` database, reset between test runs
Test strategy | Separate `lunarfront_test` database, reset between test runs
Dev ports | API: 8000, Postgres: 5432, Valkey: 6379 (all exposed to host for dev tooling)
Multi-tenant | `company_id` on all tables for tenant scoping, `location_id` where per-location tracking needed (inventory, transactions, drawer, delivery)
@@ -74,18 +74,18 @@ Area | Files / Artifacts
Root config | `turbo.json`, root `package.json` (workspaces), `tsconfig.base.json`, `.env.example`, `CLAUDE.md`
Linting | `.eslintrc.cjs`, `.prettierrc`, root lint/format scripts in Turborepo pipeline
Docker | `docker-compose.dev.yml` — PostgreSQL 16 (`forte` database) + Valkey 8, ports exposed to host
Shared package | `packages/shared/package.json` (`@forte/shared`), `packages/shared/src/types/index.ts`, `packages/shared/src/schemas/index.ts`, `packages/shared/src/utils/currency.ts`, `packages/shared/src/utils/dates.ts`
Backend package | `packages/backend/package.json` (`@forte/backend`), `packages/backend/src/main.ts` (Fastify entry with Pino JSON logging + request ID tracing), `packages/backend/src/plugins/database.ts` (Drizzle connection), `packages/backend/src/plugins/redis.ts`, `packages/backend/src/plugins/error-handler.ts`, `packages/backend/src/plugins/cors.ts`, `packages/backend/src/plugins/dev-auth.ts` (dev bypass via `X-Dev-User` header), `packages/backend/src/routes/v1/health.ts`
Shared package | `packages/shared/package.json` (`@lunarfront/shared`), `packages/shared/src/types/index.ts`, `packages/shared/src/schemas/index.ts`, `packages/shared/src/utils/currency.ts`, `packages/shared/src/utils/dates.ts`
Backend package | `packages/backend/package.json` (`@lunarfront/backend`), `packages/backend/src/main.ts` (Fastify entry with Pino JSON logging + request ID tracing), `packages/backend/src/plugins/database.ts` (Drizzle connection), `packages/backend/src/plugins/redis.ts`, `packages/backend/src/plugins/error-handler.ts`, `packages/backend/src/plugins/cors.ts`, `packages/backend/src/plugins/dev-auth.ts` (dev bypass via `X-Dev-User` header), `packages/backend/src/routes/v1/health.ts`
Database | `packages/backend/src/db/index.ts` (client export), `packages/backend/src/db/schema/companies.ts` (company table — the tenant anchor), `packages/backend/src/db/schema/locations.ts` (location table — physical store locations), `drizzle.config.ts`
Seed script | `packages/backend/src/db/seed.ts` — creates a test company + location + admin user for local dev
Testing | `vitest.config.ts`, health endpoint integration test, `forte_test` database for test isolation
Testing | `vitest.config.ts`, health endpoint integration test, `lunarfront_test` database for test isolation
### Architecture Decisions Settled
- `company_id` (tenant) + `location_id` (physical store) scoping pattern established on the first domain tables
- Inventory and transaction tables use both `company_id` and `location_id`; other tables use `company_id` only
- Drizzle migration workflow (generate → migrate)
- Shared package import convention (`@forte/shared`)
- Shared package import convention (`@lunarfront/shared`)
- Standardized error response format (consistent JSON shape for errors)
- Request context pattern (`companyId`, `locationId`, and `user` on request)
- JSON structured logging via Pino with request ID on every log line

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform
LunarFront — Small Business Management Platform
Domain Design: Personnel Management

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform
LunarFront — Small Business Management Platform
Domain Design: Consignment

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform
LunarFront — Small Business Management Platform
Domain Design: Sales Commission

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform
LunarFront — Small Business Management Platform
Phase 2 Audit: Code Review, Security, and Feature Gap Analysis

View File

@@ -8,9 +8,9 @@ Version 1.0 | Draft
# 1. Overview
Forte includes a built-in password vault for on-premise deployments. Music stores deal with dozens of credentials — supplier portals, distributor accounts, payment processor dashboards, shipping services, insurance logins, ASCAP/BMI licensing portals — and most store owners are not technical enough to adopt a separate password manager.
LunarFront includes a built-in password vault for on-premise deployments. Music stores deal with dozens of credentials — supplier portals, distributor accounts, payment processor dashboards, shipping services, insurance logins, ASCAP/BMI licensing portals — and most store owners are not technical enough to adopt a separate password manager.
The vault is a simple, encrypted credential store baked into Forte. It encrypts all secrets at rest using AES-256-GCM with a key derived from a master passphrase. The key exists only in server memory and is discarded on restart.
The vault is a simple, encrypted credential store baked into LunarFront. It encrypts all secrets at rest using AES-256-GCM with a key derived from a master passphrase. The key exists only in server memory and is discarded on restart.
**On-premise only.** Cloud/hosted deployments would require a more sophisticated key management approach (HSM, envelope encryption) and are out of scope for this design.
@@ -256,7 +256,7 @@ This is an expensive operation (scales with entry count) but should be rare.
After restart:
- Vault status: locked
- All vault API calls: 423
- Everything else in Forte: fully functional
- Everything else in LunarFront: fully functional
- Admin unlocks when ready — no urgency, no data loss
## 7.5 Multiple Servers