first commit
This commit is contained in:
209
README.md
Normal file
209
README.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user