Last updated: 2026-07-12
Stack
| Layer | Technology |
|---|---|
| Frontend | React 18 + TypeScript — Vite 5 — Cloudflare Pages |
| Backend API | Cloudflare Worker (TypeScript) — env: bernies |
| Database | Cloudflare D1 — lawncare_db |
| Auth | PBKDF2-SHA256 password hash in D1 · HMAC-signed JWT (24 hr expiry) |
| PDF generation | jsPDF (browser-side) — invoked in EstimatesView and InvoicesView |
| Mobile layout | Position-fixed sidebar drawer + hamburger toggle — mobile-first CSS |
Database Schema
| Table | Key Columns |
|---|---|
admin_users | id (UUID), email, password_hash, password_changed (bool) |
clients | id, name, email, phone, service_address, city, zip, status, notes, created_at |
employees | id, name, email, phone, role, created_at |
jobs | id, client_id, employee_id, service_type, scheduled_date, scheduled_time, duration_minutes, address, price_cents, status, recurrence, recurrence_days, notes, created_at |
estimates | id, estimate_number, client_id, services, total_cents, status, valid_until, notes, created_at |
invoices | id, invoice_number, client_id, job_id, amount_cents, description, status, issue_date, paid_date, created_at |
invoice_items | id, invoice_id, description, quantity, unit_price_cents, total_cents |
payments | id, invoice_id, amount_cents, method, paid_date, notes |
service_requests | id, name, email, phone, services, message, status, created_at |
blocked_days | id, type (single/range/weekday), date, start_date, end_date, weekdays (JSON), reason, created_at |
settings | key, value — stores all business config as key-value pairs |
login_attempts | id, ip, attempted_at — used for rate limiting (5 attempts / 15 min) |
Settings Keys
| Key | Default Value |
|---|---|
business_name | Bernie's Backyard Manicured Lawncare |
business_phone | (555) 555-5555 |
business_email | info@example.com |
business_address | 123 Main St, Anytown USA |
business_logo | (empty — base64 data URL when set) |
service_types | ["Lawn Mowing","Edging & Trimming","Fertilisation","Aeration","Leaf Removal","Hedge Trimming","Snow Removal","Irrigation Check"] |
invoice_footer | Thank you for choosing Bernie's Backyard! |
invoice_next_number | 1001 |
estimate_next_number | 2001 |
Auth Flow
Login POST → Worker verifies PBKDF2 hash → issues HMAC-signed JWT (24 hr) → frontend stores in sessionStorage under key lawncare_token. Every authenticated API call sends Authorization: Bearer <token>. Rate limiting: 5 failed attempts per IP per 15 minutes, tracked in login_attempts. The password_changed flag on admin_users drives the default-password banner in the UI.
File Structure
lawncare-portal/
src/
App.tsx ← Root: token check → PublicSite or AdminApp
api.ts ← All fetch() wrappers — reads VITE_API_URL
index.css ← Mobile-first CSS (drawer, hamburger, badges, cards)
main.tsx ← Vite entry point
context/
AppContext.tsx ← Global state — auth, all data, config, document.title
components/
PublicSite.tsx ← Config-driven public landing page + estimate request form
AdminApp.tsx ← Sidebar drawer · hamburger · login screen · user-guide iframe
admin/
Dashboard.tsx ← Stat cards + recent jobs panel
JobsView.tsx ← Job CRUD + modal form + status flow
CalendarView.tsx ← Month calendar + click-to-add-job
ClientsView.tsx ← Client CRUD + job history tab
EmployeesView.tsx ← Employee CRUD
EstimatesView.tsx ← Estimate CRUD + PDF (jsPDF) + email + convert to invoice
InvoicesView.tsx ← Invoice CRUD + line items + PDF (jsPDF) + email + print
ServiceRequestsView.tsx ← Inbound requests from public site
ReportsView.tsx ← Monthly revenue chart + KPI panels
SettingsView.tsx ← Business settings + logo + blocked days + password change
public/
user-guide.html ← Embedded admin user guide (iframe target)
worker/
index.ts ← All API routes (single file)
migrations/
0001_init.sql ← Full schema — all tables + indices
0002_settings_seed.sql ← Default settings + admin user (Admin@2026 hash)
0003_extras.sql ← login_attempts · blocked_days · password_changed · recurrence columns · logo setting
wrangler.toml ← [env.bernies] — D1 binding + custom domain
vite.config.ts
tsconfig.json
package.json
API Routes
| Method | Route | Auth | Notes |
|---|---|---|---|
| GET | /api/config | None | Public settings for landing page (business_name, service_types, etc.) |
| POST | /api/requests | None | Submit estimate request from public form → inserts into service_requests |
| POST | /api/auth/login | None | Verify password → return JWT; rate-limited by IP |
| GET/POST | /api/jobs | JWT | List all jobs or create new |
| PUT/DELETE | /api/jobs/:id | JWT | Update or delete a job |
| GET/POST | /api/clients | JWT | List all clients or create new |
| PUT/DELETE | /api/clients/:id | JWT | Update or delete a client |
| GET/POST | /api/employees | JWT | List all employees or create new |
| PUT/DELETE | /api/employees/:id | JWT | Update or delete an employee |
| GET/POST | /api/estimates | JWT | List all estimates or create new |
| PUT/DELETE | /api/estimates/:id | JWT | Update or delete an estimate |
| GET/POST | /api/invoices | JWT | List all invoices or create new (auto-increments invoice_next_number) |
| PUT/DELETE | /api/invoices/:id | JWT | Update or delete (delete blocked if status = Paid) |
| POST | /api/invoices/:id/items | JWT | Add line item to invoice |
| DELETE | /api/invoices/:id/items/:itemId | JWT | Remove line item |
| GET/POST | /api/service-requests | JWT | List all requests or create manually |
| PUT | /api/service-requests/:id | JWT | Update status / notes |
| GET | /api/settings | JWT | Return all settings as key-value map |
| PUT | /api/settings | JWT | Upsert one or more setting keys |
| PUT | /api/auth/change-password | JWT | Verify current pw, update hash, set password_changed = 1 |
| GET/POST | /api/blocked-days | JWT | List or create blocked day entries |
| DELETE | /api/blocked-days/:id | JWT | Remove a blocked day entry |
CORS & Security
Worker sets permissive CORS headers (Access-Control-Allow-Origin: *) to allow the Pages frontend to call the Worker API. In production, this is safe because the JWT protects all mutation endpoints and the Worker is not accessible to anonymous write operations except the public /api/requests and /api/config routes. Login rate limiting (5 attempts / 15 min per IP) is enforced in the Worker before password comparison.
Frontend State
All data lives in AppContext (React context). On login, the context fetches all resources in parallel (jobs, clients, employees, estimates, invoices, service requests, settings, blocked days) and stores them in state. Mutations call the API then re-fetch or optimistically update local state. The admin view is a single-page app — AdminApp.tsx renders the active view by adminView string with no router library. The user-guide view renders an <iframe src="/user-guide.html"> filling the content area.