210 lines
8.9 KiB
Markdown
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.
|