Phase 1: Station shell
- /station route replaces /pos (with redirect)
- Shared lock screen, activity tracking, auto-lock timer
- Permission-gated tab bar (POS | Repairs | Lessons)
- POSRegister embedded mode (skip lock/timer/topbar)
- Sidebar navigates to /station
Phase 2: Repairs station — front desk
- Status bar with count badges per status group, clickable filters
- Ticket queue panel with search and status filtering
- Ticket detail panel with status progress, notes, photos, line items
- Full stepped intake form: customer → item → problem/estimate → review
- Service template quick-add for line items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend:
- Server-side HTML email templates (receipt + estimate) with inline CSS
- POST /v1/transactions/:id/email-receipt with per-transaction rate limiting
- POST /v1/repair-tickets/:id/email-estimate with per-ticket rate limiting
- customerEmail field added to receipt and ticket detail responses
- Test email provider for API tests (logs instead of sending)
Frontend:
- POS payment dialog Email button enabled with inline email input
- Pre-fills customer email from linked account
- Repair ticket detail page has Email Estimate button with dialog
- Pre-fills from account email
Tests:
- 12 unit tests for email template renderers
- 8 API tests for email receipt/estimate endpoints and validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the alert banner with a blocking modal dialog that requires
users to set a PIN before they can use the app. Cannot be dismissed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST /auth/forgot-password with welcome/reset email templates
- POST /auth/reset-password with Zod validation, 4-hour tokens
- Per-email rate limiting (3/hr) via Valkey, no user enumeration
- Login page "Forgot password?" toggle with inline form
- /reset-password page for setting new password from email link
- Initial user seed sends welcome email instead of requiring password
- CLI script for force-resetting passwords via kubectl exec
- APP_URL env var in chart, removed INITIAL_USER_PASSWORD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds public /v1/store/branding and /v1/store/logo endpoints so the
login page can display the customer's name and logo without auth,
with "Powered by LunarFront" underneath — matching the sidebar style.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Registers:
- New register table with location association
- CRUD service + API routes (POST/GET/PATCH/DELETE /registers)
- Drawer sessions now link to a register via registerId
- Register ID persisted in localStorage per device
X/Z Reports:
- ReportService with getDrawerReport() (X or Z depending on session state)
- Z report auto-displayed on drawer close in the drawer dialog
- X report (Current Shift Report) button on open drawer view
- Report shows: sales summary, payment breakdown, discounts, cash accountability, adjustments
Daily Rollup:
- ReportService.getDailyReport() aggregates all sessions at a location for a date
- New /reports/daily endpoint with locationId + date params
- Frontend daily report page with date picker, location selector, session breakdown
Critical Fix:
- drawerSessionId is now populated on transactions when completing (was never set before)
- This enables accurate per-drawer reporting and cash accountability
Migration 0044: register table, drawer_session.register_id column
Tests: 14 new (register CRUD, drawer report X/Z, drawerSessionId population, daily rollup, register-drawer link)
Full suite: 367 passed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- "Orders" button in POS quick actions shows recent transactions
- Search by transaction number, tap to view receipt, print or save PDF
- Product stock counts refresh after completing a sale
- Invalidate product search queries on payment completion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace all Tailwind classes with inline styles (fixes oklch color error in html2pdf)
- Narrow receipt to 260px / 10px font for 72mm thermal paper
- Print uses hidden iframe instead of window.open (fixes Safari about:blank)
- PDF canvas width matches thermal format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Save PDF button downloads receipt directly
- Print button opens PDF in new window and triggers print dialog
- Replaces previous window.print() approach that lost styles
- Receipt generated on demand from transaction data, no file storage needed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New Receipt tab in Settings page with editable fields
- receipt_header: text below logo (e.g. tagline)
- receipt_footer: thank you message
- receipt_return_policy: return policy text
- receipt_social: website/social media
- All stored in app_config, rendered on printed receipts
- Seeded in migration with empty defaults
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Receipt component with thermal (80mm) and full-page layout support
- Code 128 barcode from transaction number via JsBarcode
- Store name, address, line items, totals, payment info, barcode
- Print button on sale complete screen (browser print dialog)
- Email button placeholder (disabled, ready for SMTP integration)
- @media print CSS hides everything except receipt content
- Receipt data fetched from GET /transactions/:id/receipt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix customerHistoryOptions closure bug (historySearch was inaccessible)
- Pass itemSearch as parameter instead of capturing from outer scope
- Seed 5 completed transactions tied to accounts (Smith, Johnson, Garcia, Chen)
- Seed admin user with employee number 1001 and PIN 1234
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Customer dialog in cart panel: search accounts by name, phone, email, account #
- Selected customer shown with name, phone, email in cart header
- accountId passed when creating transactions
- Order history view: tap a transaction to expand and see line items
- Item search in history (e.g. "strings") — filters orders containing that item
- Backend: add accountId and itemSearch filters to transaction list endpoint
- itemSearch uses EXISTS subquery on line item descriptions (ILIKE)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add employeeNumber and pinHash fields to users table
- POST /auth/pin-login: takes combined code (4-digit employee# + 4-digit PIN)
- POST /auth/set-pin: employee sets their own PIN (requires full auth)
- DELETE /auth/pin: remove PIN
- Lock screen with numpad, auto-submits on 8 digits, visual dot separator
- POS uses its own auth token separate from admin session
- Admin "POS" link clears admin session before navigating
- /pos route has no auth guard — lock screen is the auth
- API client uses POS token when available, admin token otherwise
- Auto-lock timer reads pos_lock_timeout from app_config (default 15 min)
- Lock button in POS top bar, shows current cashier name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Return null instead of throwing on 404 for drawer current query
- Sync drawer session ID to null when drawer closes
- Await query invalidation before closing dialog
- Fix unused approvedBy lint error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cash In / Cash Out buttons in drawer dialog when open
- Amount + reason form, adjustment history with IN/OUT badges
- Drawer badge shows "Drawer Open" without balance (manager info only)
- API helpers for addAdjustment and getAdjustments
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- use-pagination.ts: ts-expect-error on navigate call — search type resolves as never without route context, safe with strict:false
- $enrollmentId.tsx: wrap onValueChange to cast string to billingUnit union type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each destination route's search must match its validateSearch shape exactly:
- Detail pages (tab-based): { tab: '...' }
- List pages with extra filters: include status, instructorId, view, categoryId etc.
- Form pages (enrollments/new, repairs/new): include only their specific fields
- use-pagination.ts: fix search reducer to use (prev: any) instead of invalid cast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Newer TanStack Router enforces strict types on search params — 'search: {} as Record<string, unknown>' no longer satisfies routes with validateSearch. Replace all occurrences with the correct search shape for each destination route (pagination defaults for list routes, tab/field defaults for detail routes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all `any` types with proper types across 36 files:
- TanStack Router search params: `{} as Record<string, unknown>`
- API response pagination: proper typed interface
- DataTable column casts: remove unnecessary `as any`
- Function params and event handlers: use specific types
- Remove unused imports and variables in POS components
Frontend lint now passes with 0 errors and 0 warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend enforces open drawer at location before completing any transaction
- Frontend disables payment buttons when drawer is closed with warning message
- Fix product price field name (price, not sellingPrice) in POS API types
- Fix seed UUIDs to use valid UUID v4 format (version nibble must be 1-8)
- Fix Vite allowedHosts for dev.lunarfront.tech access
- Add e2e test for drawer enforcement (39 POS tests now pass)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standalone register at /pos bypassing the admin sidebar layout:
- Two-panel layout: product search/grid (60%) + cart/payment (40%)
- Product search with barcode scan support (UPC lookup on Enter)
- Custom item entry dialog for ad-hoc items
- Cart with line items, tax, totals, and remove-item support
- Payment dialogs: cash (quick amounts + change calc), card, check
- Drawer open/close with balance reconciliation and over/short
- Auto-creates pending transaction on first item added
- POS link added to admin sidebar nav (module-gated)
- Zustand store for POS session state, React Query for server data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log level dropdown on the settings page lets admins change the application
log verbosity at runtime without restarting. Uses the new /v1/config API
with the existing settings.view/settings.edit permissions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Full inventory UI: product list with search/filter, product detail with
tabs (details, units, suppliers, stock receipts, price history)
- Product filters: category, type (serialized/rental/repair), low stock,
active/inactive — all server-side with URL-synced state
- Product-supplier junction: link products to multiple suppliers with
preferred flag, joined supplier details in UI
- Stock receipts: record incoming stock with supplier, qty, cost per unit,
invoice number; auto-increments qty_on_hand for non-serialized products
- Price history tab on product detail page
- categories/all endpoint to avoid pagination limit on dropdown fetches
- categoryId filter on product list endpoint
- Repair parts and additional inventory items in music store seed data
- isDualUseRepair corrected: instruments set to false, strings/parts true
- Product-supplier links and stock receipts in seed data
- Price history seed data simulating cost increases over past year
- 37 API tests covering categories, suppliers, products, units,
product-suppliers, and stock receipts
- alert-dialog and checkbox UI components
- sync-and-deploy.sh script for rsync + remote deploy
- Sidebar collapses to icon-only mode with toggle button
- Nav items grouped into sections (Customers, Repairs, Storage, Admin)
- Each section is independently collapsible
- Middle nav area scrolls when items overflow
- Help, profile, and sign out pinned to bottom
Stores can enable/disable feature modules from Settings. When disabled,
nav links are hidden and API routes return 403. Designed as the
foundation for future license-based gating (licensed + enabled flags).
Core modules (Accounts, Members, Users, Roles, Settings) are always on.
- module_config table with slug, name, description, licensed, enabled
- In-memory cache for fast per-request module checks
- requireModule middleware wraps route groups in main.ts
- Settings page Modules card with toggle switches
- Sidebar hides nav links for disabled modules
- Default modules seeded: inventory, pos, repairs, rentals, lessons,
files, vault, email, reports
Three-state page: not initialized → locked → unlocked.
Any user with vault.view can unlock (for store opening).
Admins can lock and change master password.
- Two-panel layout: categories on left, entries on right
- Entry reveal button shows decrypted value for 30s with copy
- Add/edit/delete entries and categories
- KeyRound icon in sidebar navigation
When a permission is set on a nested folder, traverse is automatically
granted on all ancestor folders so users can navigate to it. Traverse
only shows subfolders in listings — files are hidden. This prevents
orphaned permissions where a user has access to a nested folder but
can't reach it.
Hierarchy: traverse < view < edit < admin
Sidebar header now loads the store logo from the files API and
displays it scaled to fit. Below the logo: "Amplified by Forte"
in subtle text. Falls back to store name as text if no logo is
uploaded. Logo fetched via authenticated request with blob URL
and proper cleanup.