Files
krow_employer/README.md
2026-09-07 20:36:18 +05:30

210 lines
8.9 KiB
Markdown

# 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=<uuid>` 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.