# krow-employee The KROW Employer console — the hiring surface for `platform.krowforce.com`. A standalone React frontend over the existing KROW Go backend. It has no backend and no database of its own: every figure on every page is read from `/api/v1`, and every write goes back through the same API. --- ## Running it `npm run dev` starts the API for you. It checks the host that `vite.config.js` proxies `/api` to, and if nothing is listening there it launches `../krow-backend`, waits for `/health`, and only then starts Vite — so the dev server never comes up pointing at an API that does not exist. ```bash # One-time backend setup (separate repo, ../krow-backend) cd ../krow-backend make migrate-up && make seed make import-agents ORG=krow-dev # required, or Owliver 404s make ingest ORG=krow-dev # required, or retrieval finds nothing # This app — starts the API too cd ../Krow-employee npm install cp .env.example .env npm run dev # http://localhost:5173 ``` PostgreSQL still has to be running; the API needs it and will not start without it. `pg_isready` should say `accepting connections`. | Variable | Meaning | |---|---| | `KROW_BACKEND_DIR` | the API repo, if it is not `../krow-backend` | | `KROW_SKIP_API_CHECK=1` | skip the preflight | `npm run dev:vite` runs Vite alone, for frontend work with no API. ### `502 Bad Gateway` on `/api/v1/...` This means nothing is listening on the proxy target — the API is a *separate process*, and a frontend that looks perfectly healthy will still 502 on every call without it. It is not a backend bug and not a CORS problem. `npm run dev` now prevents it at startup. If you see it anyway, the API exited mid-session; the terminal running `npm run dev` prints the target and the fix, and the response body carries `code: "api_not_running"` rather than a generic status-502 failure. Restart with `npm run dev`, or `cd ../krow-backend && make run`. `GET /api/v1/version` reports the endpoint count. Owliver's agent routes are registered only when `ANTHROPIC_API_KEY` is set on the backend; without it the assistant panel says so plainly instead of failing at request time. ### Signing in The seed ships one user, `demo@krow.app`, with role `admin` and **no password** — passwords never travel in the seed. Set one, and add an employer to test the role gate against: ```sql -- only if employer@krow.app does not already exist INSERT INTO users (org_id, email, full_name, role, account_type, status) SELECT id, 'employer@krow.app', 'Employer Demo', 'employer', 'employer', 'active' FROM organizations WHERE slug = 'krow-dev'; ``` ```bash cd ../krow-backend/go-api go run ./cmd/setpassword -email employer@krow.app ``` Without an `employer` user every local session runs as `admin`, and the role gate in `routes/EmployerRoute.jsx` is never actually exercised. --- ## Why the dev server proxies the API `VITE_API_BASE_URL` must stay a **same-origin path** (`/api/v1`) in development. The session is an HttpOnly, `SameSite=Lax` cookie. A Lax cookie is not sent on a cross-site request, and a browser treats `localhost:5173` and `127.0.0.1:8080` as different sites — so an absolute URL there means every call after login arrives without the cookie and is answered `401`. `vite.config.js` proxies `/api` and `/health` instead, which makes the cookie first-party. Nothing in client code names a backend host. --- ## Architecture ``` Employer UI → lib/krowHooks.js → api/krowClient.js → api/httpClient.js (TanStack Query) (entities, auth, (fetch, credentials: workflows) 'include') ↓ Existing KROW Go API → PostgreSQL ``` | Directory | What lives there | |---|---| | `src/api/` | `httpClient.js` (fetch, `{data, meta}` unwrapping, `KrowApiError`), `krowClient.js` (entities, auth, workflows, Owliver suggestions), `aiEngine.js` | | `src/components/ds/` | The KROW design system. One import surface: `@/components/ds` | | `src/components/ui/` | Radix primitives, restyled to the tokens | | `src/components/ai-assistant/` | Owliver: the panel, the SSE transport, the block vocabulary | | `src/components/krow/` | Domain components — candidate cards, talent modals, analytics blocks | | `src/lib/` | Hooks, scoring, the hiring ladder, the skill and agent registries | | `src/agents/`, `src/skills/` | Markdown definitions, loaded by `import.meta.glob` at build time | | `src/pages/hiring/` | The hiring pages | ### Routes `/login` · `/dashboard` · `/positions` · `/positions/new` · `/positions/:id` · `/candidates` · `/candidates/:id` · `/hired` · `/talent-pool` · `/analytics` · `/activity` · `/profile` · `/settings` ### Canonical paths — read this before editing a route Route literals in the source are written in **canonical** form (`/admin/positions`), not the addresses this app serves (`/positions`). That is deliberate. This console was lifted out of `krow-demo`, where the same hiring pages are mounted twice — once under `/admin` and once under `/employer`. Well over a hundred route literals are written against `/admin`, and not only in components: `lib/skills/registry.js`, `lib/skills/actions.js` and `components/ai-assistant/` emit and match canonical routes too, and `pageKeyForRoute` derives from them the page id the backend's `GET /owliver/suggestions?page=` accepts — a closed vocabulary this app does not own (`control-center`, `positions`, `hired-history`…). So `lib/product.js` translates in one place instead: - `rebase('/admin/positions')` → `/positions` — on the way out, at every link - `canonicalize('/positions')` → `/admin/positions` — on the way in, for the placement, skill and agent tables `useProduct.js` is the hook layer. A page still says `navigate('/admin/positions/123')`; the address bar says `/positions/123`. --- ## What this console deliberately does not have The backend has no endpoint for any of these, so none of them are built. A control that looks live and does nothing is worse than an absent one. - **Interview scheduling and candidate messaging.** No table, no endpoint. The card offers a real `tel:` dialler and real stage transitions instead. - **Notifications.** Nothing in the backend sends any. The Activity page and Owliver's suggestions carry "what needs attention". - **Password change, 2FA, session management.** Passwords are set by the `setpassword` CLI on the server. - **Organisation settings, team members, billing.** No `/organizations` or `/users` route exists. - **File and avatar upload.** `selfie_url` and friends are plain text columns. - **Deleting a position.** `DELETE /job-postings/{id}` answers 405 by design. Analytics is computed in the browser from raw list endpoints, because the backend has no aggregate routes and no date-range filters. The SQL for those aggregations already exists in `krow-backend/go-api/internal/tools/*.go` and would be the natural extension when data volume outgrows the 1000-row cap. AI screening, job-description generation and talent matching run through `api/aiEngine.js`, a deterministic local stand-in, and persist to the real score columns. It sits behind `krow.integrations.Core.InvokeLLM`, so replacing it with a server endpoint is a one-file change. --- ## Data traps These come from the backend's own handover notes and have produced wrong numbers before. The ladder lives once, in `src/lib/hiringRecords.js`, and everything counts through it. - **`screened_at` is vestigial.** Nothing writes it. Screened means the application is at or beyond `ai_screened` — use `atOrBeyond`. - **A score of `0` means "not rated", never "rated zero"** — `ai_score`, `client_rating`, `krow_score`, `reliability_score`, `attendance_score`, `performance_score`, `experience_years`. Average with a `> 0` filter and state the basis count. - **Genuine zeros, do not filter:** `overtime_hours`, `minutes_late`, `xp`, `profile_completion`, `actual_hours`. - **`rejected` still counts as screened.** It is terminal from any stage and overwrites the stage it was reached from; `rankOf` puts it back at the screened rung. - **`assigned` counts as hired** — a hire who has been rostered. `HIRED_STATUSES`. - **`attendance_score` defaults to 100** and must never be a scoring input. - **Array columns are not server-filterable** — `skills`, `certifications`, `availability`. Filter them client-side. - **Most resources have no `GET /{id}`.** Only `job-postings` and `courses` do; everything else answers 405. `createEntity` uses `?id=` on the list route, so pages do not have to know. --- ## Checks ```bash npm run build # production bundle npm run lint # eslint, must be clean npm run typecheck # tsc over JSDoc; see the note below ``` `typecheck` reports pre-existing errors inherited from `krow-demo` (mostly `InvokeLLM` call shapes and `import.meta.glob`, which `tsc` does not model). It is a signal to read, not a gate that currently passes.