2026-09-10 19:28:34 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-10 19:28:34 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30
2026-09-07 20:36:18 +05:30

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.

# 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:

-- 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';
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

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.

Description
No description provided
Readme 836 KiB
Languages
JavaScript 99%
CSS 0.9%
Dockerfile 0.1%