662 lines
30 KiB
Markdown
662 lines
30 KiB
Markdown
# Krow frontend — API audit
|
|
|
|
Every API this repository needs, and where each one stands against the running
|
|
Go backend at `../krow-backend`.
|
|
|
|
**The backend's `docs/api-contract.md` is normative.** This document does not
|
|
restate its semantics — it inventories what *this* repo calls, and marks each
|
|
against what the server actually registers. Nothing here proposes a route,
|
|
request field, response field or table that is not traced to a frontend call
|
|
site or an existing backend requirement.
|
|
|
|
## Status labels
|
|
|
|
| Label | Meaning |
|
|
| --- | --- |
|
|
| **Served** | The frontend calls it, and the backend registers it. Working. |
|
|
| **Adopt** | The backend registers it; the frontend does not use it yet, and should. No new work on the server. |
|
|
| **Local stub** | Implemented in-browser. No network, no server, no key. |
|
|
| **Browser-only** | State lives in `localStorage` / `sessionStorage` and nowhere else. |
|
|
| **External** | Third-party host, hotlinked, no auth, no contract. |
|
|
| **Dead code** | Frontend code exists with no consumer and no route. Not a backend gap. |
|
|
|
|
---
|
|
|
|
## 1. Route counts, derived from source
|
|
|
|
The figure "38 endpoints" is the backend contract's **§2 table** (34 Live +
|
|
4 "unreachable today"). It is not the count of what the server registers.
|
|
|
|
Counted from source rather than from any README:
|
|
|
|
| Group | Routes | Where counted |
|
|
| --- | --- | --- |
|
|
| Entity CRUD | **34** | `Ops` bits across 14 resources in `internal/domain/resources_gen.go` |
|
|
| `/me` | 4 | `internal/httpserver/me.go:83-86` |
|
|
| `/auth` | 2 | `internal/httpserver/auth.go:129-130` |
|
|
| Agent + skill definitions | 10 | `internal/httpserver/definitions.go:11-21` |
|
|
| Workflows | 2 | `internal/httpserver/workflows.go:87-88` |
|
|
| Owliver suggestions | 1 | `internal/httpserver/owliver.go` |
|
|
| Health | 1 | `internal/httpserver/server.go:146` |
|
|
| **Total registered** | **54** | |
|
|
|
|
`Server.Endpoints()` returns 53 — the sum of the six `route*` helpers,
|
|
excluding `GET /health`, which is registered before the sum.
|
|
|
|
The backend README used to say "51 registered routes", omitting the two
|
|
workflow routes; it now enumerates all 54 and is back in step with its router.
|
|
|
|
---
|
|
|
|
## 2. Configuration and transport
|
|
|
|
| Variable | Default | Read by |
|
|
| --- | --- | --- |
|
|
| `VITE_API_BASE_URL` | `/api/v1` | `src/api/httpClient.js:50` — client code |
|
|
| `VITE_API_PROXY_TARGET` | `https://mcp.krowforce.com` | `vite.config.js:9` — dev server only |
|
|
| `VITE_ASSISTANT_ENDPOINT` | *(unset)* | `src/components/ai-assistant/provider.js:205` |
|
|
|
|
- **Development.** `vite.config.js` proxies `/api` and `/health` to
|
|
`VITE_API_PROXY_TARGET`, so the browser only ever talks to one origin.
|
|
- **Production.** `Dockerfile:33` bakes
|
|
`ARG VITE_API_BASE_URL=https://mcp.krowforce.com/api/v1` at build time —
|
|
`.dockerignore` excludes `.env*`, so no env file reaches the image, and
|
|
`nginx.conf` has **no** `location /api/`. A relative base URL in production
|
|
would answer a login POST with 405.
|
|
- **Session cookie.** HttpOnly, SameSite=Lax. `httpClient.js` sends
|
|
`credentials: 'include'` and warns in DEV if the base URL is cross-origin
|
|
(`httpClient.js:72-84`).
|
|
- **Envelope.** `{ data, meta }` in, bare records out — `request()` returns
|
|
`payload.data` and drops `meta` entirely (`httpClient.js:226`).
|
|
- **Errors.** `{ error: { code, message, details } }` becomes a `KrowApiError`
|
|
carrying `.status`, `.code`, `.details`. A transport failure becomes
|
|
`status: 0, code: 'unreachable'` with a message naming the URL.
|
|
|
|
---
|
|
|
|
## 3. Entity CRUD — Served
|
|
|
|
`RESOURCE_PATHS` (`src/api/httpClient.js:96`) against `Ops`
|
|
(`internal/domain/resources_gen.go`). **Every method the frontend actually
|
|
calls has a route. Zero new endpoints are needed here.**
|
|
|
|
| Entity | Path | Frontend calls | Backend `Ops` | |
|
|
| --- | --- | --- | --- | --- |
|
|
| JobPosting | `job-postings` | list, get, create, update | List·Get·Create·Update | Served |
|
|
| JobApplication | `job-applications` | list, filter, create, update, delete | List·Create·Update·Delete | Served |
|
|
| AIInterview | `ai-interviews` | list, create | List·Create | Served |
|
|
| Staff | `staff` | list, create, update | List·Create·Update | Served |
|
|
| WorkerProfile | `worker-profiles` | list, filter, create, update | List·Create·Update | Served |
|
|
| Course | `courses` | list, get, create, update | List·Get·Create·Update | Served |
|
|
| LearningPath | `learning-paths` | list | List | Served |
|
|
| RoleCategory | `role-categories` | list, create | List·Create | Served |
|
|
| Certification | `certifications` | list, create, delete | List·Create·Delete | Served |
|
|
| UserActivity | `user-activity` | list, filter, create | List·Create | Served |
|
|
| Evidence | `evidence` | list, filter, create, update | List·Create·Update | Served |
|
|
| Assignment | `assignments` | list, create | List·Create | Served |
|
|
| ShiftRecord | `shift-records` | list | List | Served |
|
|
| Badge | `badges` | `Badge.list` via `useBadges` | **`Ops: 0` — no routes** | Dead code |
|
|
| User | `users` | *(none)* | **no resource** | Dead code |
|
|
|
|
`filter()` is not a separate endpoint — it is `GET /{resource}` with field
|
|
params. Arrays become repeated params (`?status=applied&status=hired`);
|
|
`undefined` is omitted; `sort` is always sent, because `?sort=` means "no
|
|
ordering" while omitting it applies the endpoint default.
|
|
|
|
### The two dead-code rows
|
|
|
|
Neither is a backend gap, and neither warrants a new endpoint:
|
|
|
|
- **Badge.** `useBadges` (`src/lib/krowHooks.js:537`) has **zero consumers**.
|
|
The backend contract §2 records why: every badge the UI renders comes from
|
|
`worker_profiles.earned_badges`. `GET /api/v1/badges` would 404 if this hook
|
|
were ever wired. Fix in this repo by deleting the hook, or accept it as
|
|
unreachable.
|
|
- **User.** `'User'` is in `ENTITY_NAMES` and `RESOURCE_PATHS`, so
|
|
`createEntity('User')` builds a client for `/api/v1/users` — a path with no
|
|
backend resource. **No call site exists.** Authentication goes through `/me`.
|
|
|
|
### Defaults line up
|
|
|
|
The backend's `DefaultSort` / `DefaultLimit` were derived from these call
|
|
sites, and match them:
|
|
|
|
| Resource | Frontend call | Backend default |
|
|
| --- | --- | --- |
|
|
| job-applications | `list('-ai_score', 200)` | `-ai_score` / 200 |
|
|
| worker-profiles | `list('-krow_score', 500)` | `-krow_score` / 500 |
|
|
| user-activity, assignments, shift-records | `list('-created_date', 500)` | `-created_date` / 500 |
|
|
| courses, certifications, evidence, badges | `list('-created_date', 200)` | `-created_date` / 200 |
|
|
| job-postings, ai-interviews, staff, learning-paths, role-categories | `list('-created_date', 100)` | `-created_date` / 100 |
|
|
|
|
Filters actually sent: `job_posting_id` (applications),
|
|
`email` (worker profiles, `limit 1`), `worker_email` (evidence),
|
|
`user_email` (activity, `limit 20` — `src/pages/Profile.jsx:28`).
|
|
|
|
---
|
|
|
|
## 4. Auth and preferences — Served
|
|
|
|
| Method | Path | Frontend |
|
|
| --- | --- | --- |
|
|
| `GET` | `/api/v1/me` | `auth.me()`, and a module-load hydration promise |
|
|
| `PATCH` | `/api/v1/me` | `auth.updateMe(patch)` |
|
|
| `PATCH` | `/api/v1/me/preferences` | `auth.updatePreferences(patch)` |
|
|
| `POST` | `/api/v1/auth/login` | `{ email, password, remember_me }` |
|
|
| `POST` | `/api/v1/auth/logout` | Redirects locally even if the call fails |
|
|
|
|
- `GET /me` is fired once at module load and shared, so the eleven `me()` call
|
|
sites in the first render make one request between them
|
|
(`src/api/base44Client.js:130-143`).
|
|
- A 401 is the ordinary signed-out state, not a failure. Every credential
|
|
failure returns the same 401 with the same message, by design.
|
|
- `auth.preferences()` is **synchronous** and must stay so —
|
|
`AssistantPanelContext` decides whether Owliver starts open in a `useState`
|
|
initialiser. That is why the last user is mirrored to `localStorage` under
|
|
`krow_demo_user`.
|
|
|
|
### Preference keys in use
|
|
|
|
| Key | Written by | Note |
|
|
| --- | --- | --- |
|
|
| `owliverDefault` | Settings | Seeded (`src/api/seed.js:1891`) |
|
|
| `compactDensity` | Settings | Seeded; read by `ds/DataTable.jsx:67` |
|
|
| `emailDigest` | Settings | Seeded |
|
|
| `customSkills` | Skill editors, `WorkspaceSkills.jsx` | Array of `{ path: 'custom/<id>.md', raw: <markdown> }` |
|
|
| `customAgents` | `lib/agents/useAgents.js:68,78` | Same shape, for agents |
|
|
| `disabledSkills` | `WorkspaceSkills.jsx`, `AgentDetail.jsx` | Array of skill ids |
|
|
| `removedSkills` | `WorkspaceSkills.jsx:331,353` | Array of skill ids |
|
|
|
|
> **Drift:** the backend contract §1 still says *"Auth: None in v1. Every
|
|
> endpoint is unauthenticated."* That is stale — sessions, argon2id and cookie
|
|
> middleware are implemented (`migrations/000004_auth_sessions.up.sql`,
|
|
> `internal/auth/`, `internal/httpserver/auth.go`).
|
|
|
|
---
|
|
|
|
## 5. Adopt — 12 endpoints the backend already has and the frontend ignores
|
|
|
|
None of these needs server work. All exist and are registered today.
|
|
|
|
### 5.1 Workflow endpoints (2) — **Served**
|
|
|
|
The backend built these specifically to replace frontend call sequences, and
|
|
says so in the source. Both are now adopted.
|
|
|
|
| Endpoint | Frontend call site | Replaced |
|
|
| --- | --- | --- |
|
|
| `POST /api/v1/job-applications/{id}/hire` | `base44.workflows.hire` ← `useHireCandidate` | `PATCH /job-applications/{id}` + `POST /staff` + `POST /user-activity` |
|
|
| `POST /api/v1/job-postings/{id}/assignments` | `base44.workflows.assign` ← `useAssignWorkers` | per worker: `POST /assignments` + `POST`/`PATCH` `/job-applications` + `POST /user-activity` |
|
|
|
|
`internal/httpserver/workflows.go:85` reads verbatim:
|
|
|
|
> *"handleHire moves an application to `hired` and creates the staff record in
|
|
> one transaction. Replaces the two-call sequence at krowHooks.js:302-303."*
|
|
|
|
That sequence is gone. `useHireCandidate` now issues one request and sends only
|
|
what a hiring decision chooses — `role`, `profile_tier`, `hire_date`, `status`;
|
|
name, email, phone, score, posting and application id are carried across from
|
|
the application by the server. `useAssignWorkers` sends the whole batch in one
|
|
body, naming each worker's application by `application_id` where the page
|
|
already holds it and by an `application` payload where it does not, so a worker
|
|
with no application is found-or-filed inside the same transaction rather than in
|
|
a second, untransacted request.
|
|
|
|
Both handlers also write their own `user-activity` entry inside the transaction
|
|
— `hire_candidate` and `assign_employee` — so the frontend no longer logs
|
|
either. It invalidates `['userActivity']` instead.
|
|
|
|
The server-side handlers authorize on the operations they perform — hire needs
|
|
UPDATE on job-applications, CREATE on staff, CREATE on user-activity — checked
|
|
all-or-nothing before any transaction opens.
|
|
|
|
### 5.1a Interview completion — **Served**
|
|
|
|
`POST /api/v1/ai-interviews` is routed through the same workflow service
|
|
(`httpserver/api.go:156`): creating an interview also moves its application to
|
|
`status: 'interview'`, sets `interview_id`, and copies
|
|
`overall_interview_score` onto `ai_score` when the request supplied one — all in
|
|
one transaction. `AIInterviewModal.finishInterview` no longer patches the
|
|
application afterwards. That second call was not merely redundant: for a talent
|
|
user sitting their own interview it was a 403, because `ai-interviews:Create` is
|
|
open to everyone and `job-applications:Update` is operators only — the interview
|
|
existed, the application still read `applied`, and nothing that counts
|
|
`status === 'interview' || interview_id` could see it.
|
|
|
|
### 5.2 Definition endpoints (10)
|
|
|
|
`agent-definitions` and `skill-definitions`, five methods each: `GET`, `POST`
|
|
on the collection; `GET`, `PATCH`, `DELETE` on `/{id}`.
|
|
|
|
**This is where account-authored Owliver skills are supposed to live.**
|
|
`migrations/000005_agent_skill_definitions.up.sql` states the intent verbatim:
|
|
|
|
> *"Before this migration the last two lived in `user_preferences.extra`, a
|
|
> jsonb blob with no owner, no tenancy, no size bound, no server-side
|
|
> validation and no query surface — and returned in full by GET /api/v1/me on
|
|
> every page load. This migration is that move."*
|
|
|
|
**The frontend has not moved.** It still writes `customSkills` (121 references
|
|
across the repo) and `customAgents` through `PATCH /me/preferences`.
|
|
|
|
What the tables give that the preferences blob does not:
|
|
|
|
| Column | Purpose |
|
|
| --- | --- |
|
|
| `markdown` | The definition verbatim — the authoritative artefact |
|
|
| `definition_id` | Author-facing id, `^[a-z0-9][a-z0-9-]*$`, unique per owner or per org |
|
|
| `visibility` | `personal` \| `organization` — shadow-by-id is deliberate |
|
|
| `owner_user_id` / `org_id` | Tenancy and ownership; personal definitions cascade with their owner |
|
|
| `status` | agents: `draft`/`published`/`archived`; skills: `active`/`inactive` |
|
|
| `version` | Agents only, monotonic. **Skills have no version** — none is invented |
|
|
| `name`, `description`, `pages` | Server-parsed projections, never accepted from a request body |
|
|
|
|
Plus a `length(markdown) BETWEEN 1 AND 65536` bound the jsonb blob has no
|
|
equivalent of.
|
|
|
|
**`disabledSkills` and `removedSkills` stay in preferences** — the same
|
|
migration says so. They are per-account arrays of skill *ids*, mostly shipped
|
|
ids, so they are suppression preferences over a namespace, not definitions.
|
|
|
|
### 5.3 Owliver suggestions (1)
|
|
|
|
`GET /api/v1/owliver/suggestions?page={surface}&query={typed}` — specified in
|
|
the backend contract's §2A.
|
|
|
|
**Called, registered in backend source, and NOT deployed.** This is the one row
|
|
in this document where those three come apart, so it is worth stating plainly:
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| Frontend calls it | `src/api/base44Client.js:300`, via `useSuggestions.js` |
|
|
| Backend source registers it | `go-api/internal/httpserver/owliver.go:22`, commit `b6f8655`, pushed to `origin/main` |
|
|
| `https://mcp.krowforce.com` serves it | **No — 404** |
|
|
|
|
So a signed-in dev server pointed at `mcp.krowforce.com` answers this route
|
|
`404 Not Found`, and no prompt chips appear. **That is a deployment lag, not a
|
|
frontend bug, and no change in this repository fixes it.** The two ways out are
|
|
to deploy `b6f8655` to that host, or to point `VITE_API_PROXY_TARGET` at a
|
|
backend built from current source.
|
|
|
|
Two things make this expensive to diagnose, and both are worth knowing before
|
|
reaching for the frontend code again:
|
|
|
|
- **An unauthenticated probe cannot tell you anything.** Authentication wraps the
|
|
whole mux (`server.go:153`), so *every* path under `/api/v1` answers `401`
|
|
when the session cookie is absent — a registered route and a nonexistent one
|
|
alike. `curl` against the host proves nothing on its own. The `404` only
|
|
appears once a request is authenticated and reaches the router, which is why
|
|
it shows up in the browser and not in a terminal.
|
|
- **`401` versus `404` is the whole diagnosis.** Through the dev proxy, `401`
|
|
means the session is not signed in; `404` means the session is fine and the
|
|
route is not on that backend.
|
|
|
|
The panel treats the two differently as of `useSuggestions.js`: a `404` closes a
|
|
circuit breaker and it stops asking for the rest of the page load, because no
|
|
further keystroke can change the answer. A `401`, a `500` and an unreachable API
|
|
are all still retried.
|
|
|
|
The endpoint replaces the *selection*, not
|
|
the answering: it returns at most three `{ text, intent }` pairs, where `intent`
|
|
is the frontend capability id verbatim — `position-strength`,
|
|
`hiring-operations` — so a chosen suggestion dispatches through the capability
|
|
manifests that already exist. An optional `capability` names the section type
|
|
when the query asked for one ("as a flow"), from `OWLIVER_CAPABILITIES`.
|
|
|
|
`page` is a `SKILL_SURFACES` id, not a route and not a context id, so the call
|
|
site is `surfaceForRoute()` / the page key the skill registry already derives —
|
|
not `ASSISTANT_CONTEXTS`. Aliases resolve, so `hired` and `hired-history` both
|
|
work.
|
|
|
|
What the server adds over computing this in the browser is the permission
|
|
filter: each intent declares what it reads, checked against the policy table, so
|
|
a caller is never offered a reading their role cannot perform. That check cannot
|
|
be done client-side, which is the reason this one is worth adopting rather than
|
|
keeping local.
|
|
|
|
No table and no migration — the catalogue is derived from the capability
|
|
manifests and the closed Owliver vocabulary. §6's `owliver:` suggestions are a
|
|
different thing and unaffected: those are what an *authored skill* declares, and
|
|
the backend still treats the block as opaque.
|
|
|
|
### 5.4 Registered, deliberately not adopted (1)
|
|
|
|
`GET /api/v1/me/preferences` exists. The frontend reads preferences off
|
|
`GET /me` plus the `krow_demo_user` mirror instead, because
|
|
`auth.preferences()` must answer synchronously on first paint. Not a gap.
|
|
|
|
---
|
|
|
|
## 6. Owliver skill response API — Local stub
|
|
|
|
What a skill definition may declare about what Owliver can be *asked for*. The
|
|
`owliver:` block, normalized by `src/lib/skills/owliverConfig.js` against the
|
|
closed vocabulary in `src/lib/skills/surfaces.js`.
|
|
|
|
```yaml
|
|
owliver:
|
|
enabled: true
|
|
suggestions:
|
|
- Show hiring activity
|
|
- label: Summarize it
|
|
prompt: Summarize hiring activity
|
|
capability: summary
|
|
capabilities:
|
|
- summary
|
|
- flow
|
|
responses:
|
|
flow:
|
|
title: Hiring Activity Flow
|
|
source: position.activity
|
|
steps: [today, yesterday, last-week]
|
|
```
|
|
|
|
### 6.1 Resolution rules
|
|
|
|
- **A response is a section.** It normalizes through the same
|
|
`normalizeSection()` the page's `ui:` sections go through, so a capability
|
|
resolves to the same record, the same data source and the same renderer.
|
|
There is no second shape for "the chat version".
|
|
- **The block is optional.** No `owliver:` normalizes to `NO_OWLIVER` —
|
|
disabled, and behaviourally identical to before the feature existed.
|
|
- `enabled: false` switches it off without deleting what was written.
|
|
- **`steps:` ≡ `periods:`** — the same list under two names.
|
|
- **Inheritance.** A response with no `source` takes the first declared `ui:`
|
|
section's `source`, `periods`, `limit` and `editable`, in declaration order.
|
|
- **A capability that resolves no source is dropped**, with a named error —
|
|
otherwise it would register as something Owliver offers and then have nothing
|
|
to answer with.
|
|
- **Nothing unknown survives.** Capabilities, sources, periods and shapes are
|
|
checked against the tables below; an unrecognised value is a named error, not
|
|
a dropped key.
|
|
- **`editable` needs both halves.** The source must be `writable` *and* the
|
|
page must publish a handler (`usePublishPageActions`). Neither side can
|
|
enable editing alone.
|
|
|
|
### 6.2 Capabilities (10)
|
|
|
|
`shape` is the section type the answer is drawn with. `terms` are how a
|
|
question is recognised as asking for that *shape* — never for a subject; a
|
|
subject belongs in the definition's `triggers`.
|
|
|
|
| id | shape | Reads as |
|
|
| --- | --- | --- |
|
|
| `summary` | *(none — prose)* | The figures read back as sentences |
|
|
| `flow` | `flow` | Stages or periods as a sequence |
|
|
| `stats` | `stats` | A row of counted figures |
|
|
| `list` | `list` | A ranked or plain list of records |
|
|
| `table` | `table` | Rows and columns |
|
|
| `timeline` | `timeline` | Dated events, most recent first |
|
|
| `progress` | `progress` | Bars against a total |
|
|
| `weights` | `weights` | Weighted criteria, adjustable where the page accepts the write |
|
|
| `insight` | `insight` | One finding, stated plainly |
|
|
| `card` | `card` | A titled panel of figures |
|
|
|
|
`summary` is the one capability with no component, so it is compatible with
|
|
every source.
|
|
|
|
### 6.3 Data sources (25)
|
|
|
|
`context` is what the page must supply for the reading to be possible.
|
|
`options` are the knobs a definition may set. Only `position.vetting` is
|
|
writable.
|
|
|
|
| id | context | options |
|
|
| --- | --- | --- |
|
|
| `position.activity` | positionId | periods |
|
|
| `position.pipeline` | positionId | — |
|
|
| `position.candidates` | positionId | limit |
|
|
| `position.matches` | positionId | limit |
|
|
| `position.requirements` | positionId | — |
|
|
| `position.vetting` | positionId | — · **writable** |
|
|
| `candidate.readiness` | candidateId | — |
|
|
| `candidate.activity` | candidateId | — |
|
|
| `candidates.pipeline` | — | — |
|
|
| `candidates.activity` | — | periods |
|
|
| `candidates.quality` | — | periods, limit |
|
|
| `positions.demand` | — | limit |
|
|
| `positions.risk` | — | limit |
|
|
| `talent.pool` | — | limit |
|
|
| `hires.recent` | — | limit |
|
|
| `hires.performance` | — | — |
|
|
| `workforce.training` | — | limit |
|
|
| `workforce.coverage` | — | limit |
|
|
| `workforce.attendance` | — | periods, limit |
|
|
| `workforce.overtime` | — | periods, limit |
|
|
| `activity.signals` | — | limit |
|
|
| `activity.breakdown` | — | periods, limit |
|
|
| `activity.events` | — | limit |
|
|
| `operations.risk` | — | — |
|
|
| `workspace.summary` | — | — |
|
|
|
|
Each source declares which `shapes` it can be drawn as; `sourceSupportsShape()`
|
|
is the single resolver all three consumers use (the normalizer, the Board
|
|
editor's picker, the Owliver editor's per-capability picker).
|
|
|
|
### 6.4 Section types (9) and periods (6)
|
|
|
|
`card`, `stats`, `list`, `timeline`, `flow`, `table`, `progress`, `insight`,
|
|
`weights`.
|
|
|
|
`today`, `yesterday`, `last-7-days`, `last-week`, `this-month`,
|
|
`previous-month` — each a window over `created_date`, resolved at read time.
|
|
Never a stored figure, never a hard-coded date.
|
|
|
|
### 6.5 Surfaces (18)
|
|
|
|
`control-center`, `positions`, `create-position` *(alias `new-position`)*,
|
|
`candidates`, `hired-history` *(alias `hired`)*, `talent-pool`, `krow-forge`
|
|
*(aliases `university`, `forge`)*, `analytics`, `activity`,
|
|
`workspace-agent-configure`, `settings`, `workspace`, `workspace-agents`,
|
|
`workspace-skills`, `workspace-skill-configure`, `skill-development`,
|
|
`profile`, `candidates-analysis`.
|
|
|
|
The nine with placements are domain surfaces; the rest declare
|
|
`placements: []` — configuration screens holding no workforce records, where a
|
|
`ui:` skill is refused at validation rather than validating and drawing
|
|
nothing. `placementProvides()` says which placement supplies a `positionId` or
|
|
a `candidateId`, so a section reading `position.activity` cannot be attached
|
|
where no position is in context.
|
|
|
|
### 6.6 Where this is enforced
|
|
|
|
**Client-side only.** The backend mirrors `SKILL_SURFACES` exactly — all 18
|
|
ids and the same aliases, in `internal/definition/vocabulary.go:18-40` — and
|
|
validates a definition's `pages:` against it. It treats `ui:` and `owliver:`
|
|
as **opaque** (`internal/definition/skill.go:237`).
|
|
|
|
So if the definition endpoints in §5.2 are adopted, the server will store and
|
|
round-trip an `owliver:` block verbatim and check its `pages:`, but will not
|
|
reject an unknown capability or data source. That validation stays in
|
|
`owliverConfig.js`.
|
|
|
|
---
|
|
|
|
## 7. Assistant streaming API — Local stub (wired, unused)
|
|
|
|
`src/components/ai-assistant/provider.js`. The UI never generates an answer and
|
|
never knows where one came from; it calls `provider.stream(request)` and renders
|
|
snapshots.
|
|
|
|
```
|
|
provider.id: string
|
|
provider.stream(request): AsyncIterable<Block[]>
|
|
|
|
request = { contextId, capability, question, facts, signal }
|
|
```
|
|
|
|
Each yielded value is the response **so far** as a full block array — snapshots,
|
|
not deltas, because a table or a KPI row has no meaningful half-state.
|
|
|
|
`createHttpProvider({ endpoint })` sends:
|
|
|
|
```http
|
|
POST <endpoint>
|
|
Content-Type: application/json
|
|
|
|
{ "contextId": "admin.positions", "capability": "flow",
|
|
"question": "...", "agent": {...}, "owliverContext": {...} }
|
|
```
|
|
|
|
and reads SSE-style lines:
|
|
|
|
- `data: {"block": {...}}` — appended as a whole block
|
|
- `data: {"delta": "..."}` — appended to the trailing text block
|
|
- `data: [DONE]` — ignored
|
|
- unparseable payload — appended as text
|
|
|
|
Requires a streaming `response.body`; a non-2xx throws
|
|
`Assistant request failed: <status>`.
|
|
|
|
**The fact sheet is deliberately not sent.** Dashboard data is to be read
|
|
server-side from the caller's own session, so the client cannot ask about
|
|
records it is not entitled to see — and an agent cannot widen that by being
|
|
named in the body.
|
|
|
|
**Status.** `createAssistantProvider()` returns `createHttpProvider` only when
|
|
`VITE_ASSISTANT_ENDPOINT` is set. It is unset, so the app runs
|
|
`createLocalProvider()` — deterministic answers computed from the dashboard's
|
|
own data. **No backend route exists for this.**
|
|
|
|
Related, and honest about itself: an agent with `webSearch` set gets a note
|
|
saying no search provider is configured in this deployment, rather than an
|
|
answer that quietly came from nowhere.
|
|
|
|
---
|
|
|
|
## 8. AI integrations — Local stub
|
|
|
|
`src/api/aiEngine.js`. No network, no key, no server route anywhere in the
|
|
backend.
|
|
|
|
### `integrations.Core.InvokeLLM({ prompt, response_json_schema, model })`
|
|
|
|
Recognises each workflow by a stable phrase its prompt opens with, reads the
|
|
structured fields the prompt already carries, and scores deterministically —
|
|
same input, same output. An unrecognised prompt logs a warning and returns `{}`
|
|
(or `''` with no schema) rather than a shape the caller cannot use.
|
|
|
|
Consumers — `src/lib/krowAi.js`: `generateJobDescription`,
|
|
`buildResumeFromText`, `screenCandidate`, `generateInterviewQuestion`,
|
|
`matchTalentForJob`, `evaluateInterview`, `owliverQuestion`,
|
|
`buildProfileFromConversation`. And `src/lib/provingGround.js`:
|
|
`challengeFollowUp`, `evaluateChallenge`.
|
|
|
|
### `integrations.Core.UploadFile({ file })`
|
|
|
|
Returns `{ file_url, file_name, file_size }` where `file_url` is a `blob:` URL
|
|
from `URL.createObjectURL`. **There is no file storage backend.** The URL is
|
|
valid for the rest of the session and dies on reload — anything persisted
|
|
through it (a selfie on a worker profile, challenge media) is a dangling
|
|
reference afterwards.
|
|
|
|
Consumers: `src/pages/KrowIdentity.jsx`,
|
|
`src/components/krow/proving/MediaChallenge.jsx`,
|
|
`src/components/krow/proving/VideoRecorder.jsx`.
|
|
|
|
### `analytics.track({ eventName, properties })`
|
|
|
|
A DEV-only `console.debug` (`src/api/base44Client.js:287-293`). No telemetry
|
|
endpoint exists. Note that real activity logging is separate and *is* served —
|
|
`logActivity` writes through `POST /api/v1/user-activity`.
|
|
|
|
### `GET /health`
|
|
|
|
Registered on the backend and public. `vite.config.js` proxies it. **No
|
|
frontend code calls it.**
|
|
|
|
---
|
|
|
|
## 9. Browser-only state
|
|
|
|
| Key | Store | Holds |
|
|
| --- | --- | --- |
|
|
| `krow_demo_user` | localStorage | Mirror of the last `GET /me`, so `auth.preferences()` can be synchronous |
|
|
| `krow_assistant:history` | localStorage | Owliver conversation history across sessions |
|
|
| `krow_assistant_open` | sessionStorage | Whether the panel is open |
|
|
| `krow_assistant_expanded` | sessionStorage | Expanded state |
|
|
| `krow_assistant_width` | sessionStorage | Panel width, throttled on drag |
|
|
| `krow_assistant:agent` | sessionStorage | Selected agent |
|
|
| `<contextKey>` / `:flow` / `:id` | sessionStorage | Current thread, in-flight flow and conversation id, per page context |
|
|
|
|
Other platform APIs: `window.matchMedia` and `window.visualViewport`
|
|
(§11), `URL.createObjectURL` (§8), `navigator.mediaDevices.getUserMedia` and
|
|
`MediaRecorder` (`components/krow/proving/VideoRecorder.jsx:21,37`), and
|
|
`html2canvas` (`components/krow/talent/IdentityCareerCard.jsx`).
|
|
|
|
---
|
|
|
|
## 10. External hotlinks — External
|
|
|
|
No auth, no versioning, no contract. Each is a live dependency on a third party.
|
|
|
|
| Host | Used for | Where |
|
|
| --- | --- | --- |
|
|
| `fonts.googleapis.com` | Sora webfont | `src/index.css:1` |
|
|
| `api.qrserver.com` | vCard QR for the KROW ID | `KrowIdPreview.jsx:52`, `talent/IdentityCareerCard.jsx:132` |
|
|
| `images.unsplash.com` | Portrait fallback when `selfie_url` is empty | `SuggestedTalent.jsx`, `CandidateCard.jsx`, `TalentPoolCard.jsx` |
|
|
| `i.pravatar.cc` | Seed avatars | `src/api/seed.js` (15 records) |
|
|
| `static.wixstatic.com` | Image host allowlist | `src/components/ui/image.jsx:6` |
|
|
|
|
---
|
|
|
|
## 11. Owliver panel responsiveness
|
|
|
|
**This section is UI layout, not an API contract.** It is here because it was
|
|
asked for alongside the API surface, not because anything below crosses a
|
|
network boundary.
|
|
|
|
`src/components/ai-assistant/viewport.js`:
|
|
|
|
- **`STACK_BREAKPOINT = 768`**, i.e. Tailwind's `md`, exported so the panel and
|
|
the provider switch on the same number. Below it Owliver stops being a column
|
|
beside the dashboard and becomes an overlay — a 380px track on a 375px phone
|
|
collapses `main` to nothing. Deliberately `md` and not `lg`: tablets lay the
|
|
inline column out acceptably, so only phones change presentation.
|
|
- **`useIsPhone()` uses `matchMedia`, not `innerWidth`.** When anything on the
|
|
page overflows, mobile browsers widen the layout viewport to fit it, and
|
|
`innerWidth` reports the widened value — so a width comparison reads the
|
|
viewport as a tablet and hands it a sidebar, defeating the very layout that
|
|
exists to rescue the page. A media query is evaluated against the media, not
|
|
the content.
|
|
- **`useVisualViewport()`** returns the region left over with the on-screen
|
|
keyboard up. `100dvh` follows the address bar but knows nothing about the
|
|
keyboard, so a `100dvh` sheet puts its own composer underneath the keys.
|
|
Returns `null` where the API is missing, and the caller falls back to the
|
|
`100dvh` class. No fixed pixel heights anywhere.
|
|
- **Width** is clamped against `useViewportWidth()` and persisted to
|
|
`krow_assistant_width` on a throttle rather than sixty times a second.
|
|
|
|
---
|
|
|
|
## 12. Drift found
|
|
|
|
Recorded, not fixed — each belongs in its own repo.
|
|
|
|
| Where | Issue |
|
|
| --- | --- |
|
|
| `krow-backend/docs/api-contract.md` §1 | Says "Auth: None in v1. Every endpoint is unauthenticated." Sessions and cookie middleware have been implemented since. |
|
|
| `krow-demo/src/api/store.js` | 173 lines, **imported by nothing**. Superseded by `httpClient.js`. Dead. |
|
|
| `krow-demo/src/api/seed.js` | 1,928 lines, retained for one export — `DEMO_USER.preferences`, the default shape the synchronous accessor needs. Entity data lives in PostgreSQL. |
|
|
| `krow-demo/src/lib/krowHooks.js:537` | `useBadges` has no consumers and no route. |
|
|
| `krow-demo/src/api/httpClient.js:96` | `User: 'users'` names a resource the backend does not have. No call site, so harmless today. |
|
|
|
|
## 13. Summary
|
|
|
|
- **Nothing the frontend calls is missing from the backend.** All 14 live
|
|
entities and all 5 auth/preference routes are served.
|
|
- **The two workflow endpoints are adopted.** Hire, assign and interview
|
|
completion each go over one request and one transaction; the frontend no
|
|
longer sequences the writes itself, and no longer writes the `hire_candidate`
|
|
or `assign_employee` audit entries the server now writes inside those
|
|
transactions.
|
|
- **12 registered endpoints still go unused**, of which 11 are worth adopting:
|
|
10 definition endpoints that are where Owliver's authored skills and agents
|
|
were meant to move, and the Owliver suggestion endpoint, which is the only one
|
|
that cannot be done correctly in the browser — it is permission filtered.
|
|
- **Four surfaces have no server at all** — `InvokeLLM`, `UploadFile`, the
|
|
assistant streaming endpoint, and web search. They are local stubs today, and
|
|
the app is honest about each.
|
|
- **The Owliver skill vocabulary is enforced in the browser only.** The backend
|
|
validates `pages:` and treats the rest of a definition as opaque.
|