updates on the ui changes and changed into doormile
This commit is contained in:
285
README.md
285
README.md
@@ -1,19 +1,20 @@
|
||||
# KROW Demo
|
||||
# Doormile Express Console
|
||||
|
||||
AI-powered hiring for event and hospitality staffing — a standalone demo build of KROW Forge.
|
||||
|
||||
Runs with no backend, no API keys and no real login. `npm install && npm run dev` and the app is fully
|
||||
populated.
|
||||
The operator console for the Doormile Express dispatch platform — orders, deliveries, riders,
|
||||
dispatch optimisation and reporting, talking to the live `api.doormile.com` admin API.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:5173 — opens on /admin/login
|
||||
cp .env.example .env.local # point it at the API you want
|
||||
npm run dev # http://localhost:5173 — opens on /login
|
||||
```
|
||||
|
||||
Production users are warehouse and dispatch staff, not end customers.
|
||||
|
||||
## Stack
|
||||
|
||||
Vite 6 · React 19 · React Router 6 · React Query 5 · Tailwind + shadcn/ui · Framer Motion ·
|
||||
Recharts + MUI X Charts · react-hot-toast.
|
||||
Vite 6 · React 19 · React Router 6 · React Query 5 · axios · Tailwind + shadcn/ui ·
|
||||
Framer Motion · Leaflet · dayjs · SheetJS · react-hot-toast.
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -23,129 +24,165 @@ src/
|
||||
App.jsx providers + the entire route table
|
||||
index.css design tokens (HSL CSS variables) + utility layers
|
||||
|
||||
api/ the backend seam — see below
|
||||
base44Client.js the contract the production app talks to
|
||||
store.js in-memory entity store, mirrored to localStorage
|
||||
aiEngine.js local, deterministic InvokeLLM
|
||||
seed.js demo dataset
|
||||
attendanceSeed.js shift/attendance dataset
|
||||
api/doormile/ the backend seam — see below
|
||||
client.js axios instance, bearer token, 401 handling
|
||||
endpoints.js one function per /admin/* route
|
||||
queries.js the page-facing layer: joins, status mapping, solver calls
|
||||
notify.js the data layer's toast seam
|
||||
index.js the single import surface
|
||||
|
||||
lib/ domain logic and data hooks
|
||||
krowHooks.js React Query hooks over the entity store
|
||||
krowAi.js AI workflows
|
||||
krowScore.js score engine
|
||||
AuthContext.jsx auth provider
|
||||
admin/ admin session, permissions, position insights
|
||||
skills/ the Owliver skill system (registry, resolver, flows)
|
||||
agents/ the agent system (registry, runtime, lifecycle)
|
||||
|
||||
skills/ *.md skill definitions — loaded by import.meta.glob
|
||||
agents/ *.md agent definitions — loaded by import.meta.glob
|
||||
doormileHooks.js React Query hooks over the API — every page reads through here
|
||||
doormileFormat.js currency/number/distance formatting, search, xlsx export
|
||||
doormileTimestamp.js the IST wall-clock parse every date must go through
|
||||
batchBucket.js the Morning/Afternoon/Evening wave windows
|
||||
orderStatusGroups.js which raw booking enums make up each Orders tab
|
||||
dispatchPreview.js the optimiser preview's tree/flat-list model
|
||||
profitability.js revenue, cost and margin for a delivery day
|
||||
distance.js OSRM driving distance, Haversine fallback
|
||||
bulkOrderColumns.js the bulk-upload sheet's column map
|
||||
preferences.js per-browser display settings
|
||||
AuthContext.jsx the Doormile session
|
||||
|
||||
components/
|
||||
ui/ 23 shadcn primitives
|
||||
ds/ 30 design-system components (the app's real vocabulary)
|
||||
admin/ admin page shell + role glyphs
|
||||
krow/ domain components (+ talent/, proving/)
|
||||
charts/ MUI X Charts wrappers and theme
|
||||
agents/ agent configure / canvas / test panel
|
||||
forge/ KROW Forge skill + challenge components
|
||||
skills/ skill authoring surfaces
|
||||
ai-assistant/ Owliver — panel, routing, capabilities, history
|
||||
ui/ shadcn primitives
|
||||
ds/ the design system — the vocabulary pages are written in
|
||||
doormile/ console-specific pieces (ListToolbar, DispatchMap)
|
||||
|
||||
layouts/ AdminLayout — header nav, ⌘K search, assistant panel
|
||||
pages/ route components (admin/ holds the live product)
|
||||
hooks/ shared hooks
|
||||
assets/ brand asset references
|
||||
lib/assistant/ Doormile AI — the deterministic operations copilot
|
||||
intents.js the intent catalog: match → run → answer
|
||||
scan.js booking scans with honest truncation reporting
|
||||
vocab.js dates, statuses, batches, typo tolerance
|
||||
flows.js the three conversational writes + the confirm gate
|
||||
pageContext.js route → context label + suggested questions
|
||||
|
||||
layouts/ AdminLayout — header nav, ⌘K search, exception notifications, AI trigger
|
||||
pages/ Login + doormile/ (the console)
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
The app opens on **`/admin/login`**. Everything else lives under `/admin/*` behind two guards:
|
||||
`ProtectedRoute` (auth) and `AdminRoute` (admin session), rendered inside `AdminLayout`.
|
||||
The app opens on **`/login`**. Everything else lives under `/doormile/*` behind `ProtectedRoute`,
|
||||
rendered inside `AdminLayout`.
|
||||
|
||||
| Path | Page |
|
||||
| --- | --- |
|
||||
| `/admin/login` | Admin sign-in — outside the protected boundary |
|
||||
| `/admin` | Control Center |
|
||||
| `/admin/positions`, `positions/new`, `positions/:id` | Positions, Create, Detail |
|
||||
| `/admin/candidates`, `candidates/:id` | Candidates, Candidate Intelligence profile |
|
||||
| `/admin/hired` · `/admin/talent-pool` | Hired History, Talent Pool |
|
||||
| `/admin/university`, `university/:id` | KROW Forge, challenge detail |
|
||||
| `/admin/analytics` · `/admin/activity` | Analytics, Activity |
|
||||
| `/admin/profile` · `/admin/settings` | Profile, Settings |
|
||||
| `/admin/workspace` | Workspace hub |
|
||||
| `/admin/workspace/agents`, `agents/new`, `agents/:id` | Agent list, agent editor |
|
||||
| `/admin/workspace/skills`, `skills/new`, `skills/:id` | UI skill list and editor |
|
||||
| `/admin/workspace/skills/owliver/new`, `owliver/:id` | Owliver skill editor |
|
||||
| `/admin/workspace/skill-development` | Skill Development |
|
||||
| `/login` | Sign-in — outside the protected boundary |
|
||||
| `/doormile/dispatch` | The dispatch board — live map, rider detail, wave economics |
|
||||
| `/doormile/dispatch/preview` | The optimiser's plan — reconcile, then commit |
|
||||
| `/doormile/orders`, `orders/create`, `orders/createorders`, `orders/preview` | Orders, create one, bulk upload, assignment preview |
|
||||
| `/doormile/deliveries` | Deliveries |
|
||||
| `/doormile/riders`, `riders/create`, `riders/edit` | Riders, create, edit |
|
||||
| `/doormile/tenants` · `clients/create` | Clients, create |
|
||||
| `/doormile/customers` · `customer/create` | Customers, create |
|
||||
| `/doormile/pricing` | Per-client rate cards + quote simulator |
|
||||
| `/doormile/hubs` · `vehicles` · `tripsheets` · `exceptions` · `competitive-intel` · `app-users` | Fleet Ops |
|
||||
| `/doormile/reports/orderssummary`, `ordersdetails`, `riderssummary`, `profitability` | Reports |
|
||||
| `/doormile/profile` | Account and password |
|
||||
| `*` | `lib/PageNotFound.jsx` |
|
||||
|
||||
Legacy top-level paths (`/overview`, `/positions`, `/candidates`, `/hired`, `/talent-pool`,
|
||||
`/university`, `/analytics`, `/activity`, `/tracking`) all `Navigate` into their `/admin/*`
|
||||
equivalent, id preserved. Static route segments are declared before dynamic ones so
|
||||
`workspace/agents/new` cannot be read as an agent whose id is `"new"`.
|
||||
|
||||
> **Note.** `App.jsx` still imports a set of pre-redesign Employer/Talent pages
|
||||
> (`Overview`, `Apply`, `WorkerProfile`, `KrowIdentity`, `EmployeeDashboard`, `DesignSystem`, …)
|
||||
> and `layouts/Layout.jsx`. None of them are mounted on a route — they are unreachable at runtime
|
||||
> and retained pending a decision on whether to re-route or remove them.
|
||||
Static route segments are declared before dynamic ones, so `orders/create` cannot be read as an
|
||||
order whose id is `"create"`.
|
||||
|
||||
## The backend seam
|
||||
|
||||
The production app talks to a Base44 backend through `src/api/base44Client.js`. This demo keeps that
|
||||
module path, export name and full method contract, and swaps only the transport:
|
||||
Everything goes through `src/api/doormile/`, which talks to `api.doormile.com/api/v1/admin/*`.
|
||||
|
||||
| Contract | Demo implementation |
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `base44.entities.<Name>.list/filter/get/create/update/delete` | `api/store.js` — in-memory records, mirrored to `localStorage` |
|
||||
| `base44.integrations.Core.InvokeLLM` | `api/aiEngine.js` — local, deterministic |
|
||||
| `base44.integrations.Core.UploadFile` | blob URLs, session-scoped |
|
||||
| `base44.auth.*` | seeded employer/admin user, always signed in |
|
||||
| `client.js` | One axios instance. Attaches `Bearer localStorage.doormileToken` to every request; a 401 clears the session and returns to `/login`. Rejects with the server's own body, keeping the status on `err.httpStatus`. |
|
||||
| `endpoints.js` | One named export per route. Thin: build the URL, call, unwrap the envelope. GET/list return `response.data.data`; mutations return the whole `{ success, data \| message }`. |
|
||||
| `queries.js` | The page-facing layer — the joins, the status mapping, the AI-solver calls, and safe empty defaults for the resources this API has no equivalent for. |
|
||||
|
||||
Because the seam did not move, everything above it — `lib/krowHooks.js`, `lib/krowAi.js`,
|
||||
`lib/krowScore.js`, every page and component — is unchanged from the reference implementation.
|
||||
Pages do not import these directly. They use the React Query hooks in `lib/doormileHooks.js`, so a
|
||||
resource has exactly one cache key across the app and a write on one page invalidates the list
|
||||
another page is showing.
|
||||
|
||||
## The AI engine
|
||||
### What this API cannot do
|
||||
|
||||
Every AI workflow funnels through one call, `InvokeLLM({ prompt, response_json_schema })`.
|
||||
`api/aiEngine.js` reimplements it locally: it recognises each workflow by the phrase its prompt opens
|
||||
with, reads the structured fields the prompt already carries (`Years Experience: 6`,
|
||||
`Required Certifications: …`, the talent-pool JSON block) and scores them deterministically.
|
||||
Several things the previous backend served have no equivalent here. The functions covering them
|
||||
return safe empty defaults rather than throwing, so a page renders an empty state instead of
|
||||
crashing:
|
||||
|
||||
Nine workflows are implemented: candidate screening, job-description generation, résumé building,
|
||||
interview questions, interview evaluation, talent matching, Owliver's conversation, Career DNA
|
||||
building and Proving Ground evaluation. Results respond to the data rather than being canned — the
|
||||
same input always returns the same output.
|
||||
- **Zones** are not a resource. `applocationid` exists only as a field on a hub, so every zone
|
||||
picker is derived from the distinct cities across `GET /admin/hubs`.
|
||||
- **Rider substitution** has no endpoint at all, so the console has no substitution surface.
|
||||
- **Invoices and expense approvals** have no endpoint.
|
||||
- **Payment-mode types, item subcategories, rider shifts and vehicle types** are not enumerable.
|
||||
|
||||
## Skills and agents
|
||||
The AI dispatch optimiser (`routes.workolik.com` / `routemate.workolik.com`) is a separate solver
|
||||
service with no equivalent in the admin API, and is called directly.
|
||||
|
||||
Two registries discover their definitions from markdown at build time:
|
||||
## Doormile AI
|
||||
|
||||
| Registry | Glob | Files |
|
||||
| --- | --- | --- |
|
||||
| `lib/skills/registry.js` | `import.meta.glob('/src/skills/**/*.md')` | 18 Owliver skills, 5 workforce training skills |
|
||||
| `lib/agents/registry.js` | `import.meta.glob('/src/agents/**/*.md')` | 9 agent definitions |
|
||||
An in-console copilot, opened from the header — not a route. It answers questions about live data
|
||||
by calling **the same endpoints the pages call**, so its numbers and the screen's cannot disagree.
|
||||
|
||||
**These globs are absolute paths.** Moving or renaming `src/skills/` or `src/agents/` makes the
|
||||
registry silently return nothing — no build error, no import failure, just an empty registry. Add
|
||||
definitions by dropping a new `.md` file into the right folder; nothing else needs to change.
|
||||
**It is deterministic, not an LLM.** This app has no backend of its own, so there is nowhere to hold
|
||||
a model key that would not ship to the browser. The trade was taken deliberately: a rich matcher
|
||||
over typed API calls, rather than a generated answer nobody can verify.
|
||||
|
||||
## Demo data
|
||||
- **Never fabricate a number.** A question no API function covers is left unanswered. A wrong number
|
||||
from this bot is worse than no answer.
|
||||
- **Every answer carries its sources** behind a disclosure — which endpoint ran, and what came back.
|
||||
That is the verifiability contract; do not remove it.
|
||||
- **A truncated scan is a floor, not a total.** `/admin/bookings` caps `pagesize` at 1000, so scans
|
||||
drain pages up to a budget and report `truncated`. Counts built on one say "at least", and the
|
||||
source entry reports an error rather than a green tick.
|
||||
- **Writes are gated.** The three creates — customer, order, bulk — are conversations, one question
|
||||
per turn. The bot gathers, shows exactly what will be sent, and the mutation fires only when the
|
||||
operator presses the button. `executeFlow` is the only mutating function and no `match` calls it.
|
||||
- **A flow reply never reaches the router.** The panel intercepts it first: the router matches text,
|
||||
and a bare answer like a phone number matches no intent.
|
||||
- **Ordering is load-bearing.** Domain intents (rider, tenant, fleet) sit ahead of the generic
|
||||
order/status ones, and the generic ones additionally guard against another domain's keywords. A
|
||||
bug shipped once where "how many riders are active" was swallowed by the order-status intent,
|
||||
because "active" is a valid order status.
|
||||
|
||||
Seeded in `src/api/seed.js` — positions, applicants, AI-scored candidates, interviews, hires,
|
||||
talent-pool profiles, Proving Ground courses and an activity log; shift data in
|
||||
`src/api/attendanceSeed.js`.
|
||||
## Dispatch is a monitoring board
|
||||
|
||||
Edits persist to `localStorage`. To restore the shipped dataset:
|
||||
It reads the deliveries feed, which by construction returns only bookings that **already carry a
|
||||
rider or a consignment** — so an unassigned order cannot appear on it. Assignment therefore lives on
|
||||
Orders, which reads raw bookings and can see the pending pile. Running the optimiser from Dispatch
|
||||
would re-send work that is already assigned; the source console never did either.
|
||||
|
||||
```js
|
||||
// in the browser console
|
||||
localStorage.removeItem('krow_demo_db'); location.reload();
|
||||
```
|
||||
Selecting a rider opens their day: live position with a reverse-geocoded area and ping history, the
|
||||
plan against their GPS trail, and what the day earned. Per-stop actual distance is apportioned from
|
||||
the trail by each stop's share of the plan — the feed carries no per-stop odometer, so it is an
|
||||
estimate and the panel says so.
|
||||
|
||||
Bumping `STORAGE_VERSION` in `src/api/store.js` (currently `8`) invalidates saved snapshots
|
||||
automatically, so changing the seed does not require a manual clear.
|
||||
## Things that look wrong but are not
|
||||
|
||||
- **A delivery can read "Active" while the booking says `Converted_To_Consignment`.** A booking's
|
||||
status freezes the moment it is picked up; the lifecycle continues on the consignment. The
|
||||
Deliveries table lets the consignment's status win and labels the row so you can tell where the
|
||||
value came from.
|
||||
- **The Picked tab is empty on hyperlocal traffic.** A matching 3-digit pickup/delivery pincode
|
||||
prefix goes straight to `Out_for_Delivery` at pickup, so those parcels never rest in "picked".
|
||||
- **Update Status refuses half its own options.** The write is consignment-scoped, and Pending,
|
||||
Accepted, Arrived, Picked and Skipped describe states a consignment cannot be in. Those are
|
||||
refused with the reason rather than guessed at.
|
||||
- **Assigned means different things on Orders and Deliveries.** Orders tracks the *operator's*
|
||||
workflow, so assigning a rider counts as Assigned. Deliveries tracks the *rider's* engagement, so
|
||||
it waits for the rider to accept. This is deliberate, and the two taxonomies live in
|
||||
`orderStatusGroups.js` and `queries.js` respectively.
|
||||
|
||||
## Batches, dates and the one rule about time
|
||||
|
||||
Waves bucket on a booking's **creation time** (`orderdate`), never on `assigntime` — which is mapped
|
||||
to the backend's last-modified column and is re-stamped by any status change — and never on
|
||||
`expecteddeliverytime`, which is the promised slot rather than the wave the order was placed in.
|
||||
|
||||
`GET /admin/bookings` takes no date parameter, so the range is applied client-side on that same
|
||||
field. **Filtering on one timestamp while bucketing on another cannot produce an honest count**, and
|
||||
both have been tried.
|
||||
|
||||
The windows in `lib/batchBucket.js` cover the whole clock, and that is load-bearing: earlier windows
|
||||
left 40% of the day in a gap where an order belonged to no wave and vanished from every filter.
|
||||
|
||||
Every timestamp goes through `parseDoormileTimestamp`. Doormile stores IST wall-clock in naive
|
||||
columns and some responses carry a false trailing `Z`; parsing that as UTC shifts a row by five and
|
||||
a half hours, and therefore into the wrong day and the wrong wave.
|
||||
|
||||
## Design system
|
||||
|
||||
@@ -153,56 +190,40 @@ Tokens live in `src/index.css` as HSL CSS variables consumed by `tailwind.config
|
||||
|
||||
| Token | Value |
|
||||
| --- | --- |
|
||||
| Primary — KROW Blue | `#0A39DF` |
|
||||
| Accent — KROW Yellow | `#F9E547` |
|
||||
| Pale Yellow | `#F8E08E` |
|
||||
| Primary — Doormile red | `#C8102E` |
|
||||
| Accent | `#F9E547` |
|
||||
| Dark Navy | `#333F48` |
|
||||
| Mint | `#D1E0D7` |
|
||||
| Radius | `0.75rem` |
|
||||
| Headings | Sora |
|
||||
| Body | Inter |
|
||||
|
||||
`src/components/ds/` is the vocabulary the app actually writes in — `Surface`, `KpiCard`,
|
||||
`DataTable`, `PageHeader`, `Modal`, `toast` and the rest, exported through `ds/index.js`.
|
||||
`src/components/ui/` holds the shadcn primitives those are built on. Surfaces use `glass` and
|
||||
`glass-card`; the page background is the `gradient-mesh` utility.
|
||||
`src/components/ds/` is the vocabulary the app writes in — `Surface`, `KpiCard`, `DataTable`,
|
||||
`PageHeader`, `Modal`, `StatusBadge`, `toast` and the rest, exported through `ds/index.js`.
|
||||
`src/components/ui/` holds the shadcn primitives those are built on.
|
||||
|
||||
Toasts go through `ds/toast` (a wrapper over `react-hot-toast`), rendered by `<HotToaster>` in
|
||||
`App.jsx`.
|
||||
`StatusBadge`'s `STATUS_MAP` is the only place a lifecycle status maps to a colour and a label.
|
||||
Statuses are looked up case-insensitively, and an unmapped value renders as a neutral badge showing
|
||||
the humanised raw string — so a missing entry is visible rather than silent. Add states there, never
|
||||
in a page-local table.
|
||||
|
||||
## Environment
|
||||
|
||||
| Variable | Purpose |
|
||||
| --- | --- |
|
||||
| `VITE_DOORMILE_URL` | The admin API. Defaults to `https://api.doormile.com/api/v1`. |
|
||||
| `VITE_OSRM_URL` | Routing for order pricing. Defaults to the public OSRM demo server, which is rate-limited and not for production. |
|
||||
|
||||
## Scripts
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `npm run dev` | Dev server (honors `PORT`, default 5173) |
|
||||
| `npm run dev` | Dev server (honours `PORT`, default 5173) |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run preview` | Serve the build |
|
||||
| `npm run lint` | ESLint — currently clean |
|
||||
| `npm run lint:fix` | ESLint with `--fix` |
|
||||
| `npm test` | `scripts/skill-check.mjs` — 835 assertions over the skill and agent systems |
|
||||
| `npm run typecheck` | `tsc -p ./jsconfig.json` with `checkJs` |
|
||||
|
||||
### The test harness
|
||||
|
||||
`npm test` boots a real Vite dev server in middleware mode and `ssrLoadModule`s the actual
|
||||
application modules, so it exercises the real `@/` alias and `import.meta.glob` pipeline rather than
|
||||
mocks. It also diffs Owliver's behaviour across eleven page contexts against a committed snapshot,
|
||||
`scripts/__baseline__/owliver-baseline.json`; regenerate that deliberately with
|
||||
`node scripts/owliver-baseline.mjs --write`, never to turn a red check green.
|
||||
|
||||
Because it loads modules by absolute path, the harness pins the locations of `src/api/`, `src/lib/`,
|
||||
`src/lib/skills/`, `src/lib/agents/`, `src/components/ai-assistant/`, `src/App.jsx`,
|
||||
`src/layouts/AdminLayout.jsx` and a handful of agent files. Move any of them and update
|
||||
`scripts/owliver-capture.mjs` and `scripts/skill-check.mjs` in the same change.
|
||||
|
||||
### Known-failing checks
|
||||
|
||||
Two checks fail on a clean checkout and are tracked as separate work, not regressions:
|
||||
|
||||
- `npm test` — 834/835 pass; `the seeded overtime climb is found` fails.
|
||||
- `npm run typecheck` — 59 errors, all pre-existing JSDoc/inference gaps in `lib/krowAi.js`,
|
||||
`lib/skills/*`, `lib/positionModel.js` and a few components.
|
||||
|
||||
## Deployment
|
||||
|
||||
`Dockerfile` builds with `node:22-alpine` (capped at 1 GB heap for small hosts) and serves the
|
||||
|
||||
Reference in New Issue
Block a user