31 KiB
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.jsproxies/apiand/healthtoVITE_API_PROXY_TARGET, so the browser only ever talks to one origin. - Production.
Dockerfile:33bakesARG VITE_API_BASE_URL=https://mcp.krowforce.com/api/v1at build time —.dockerignoreexcludes.env*, so no env file reaches the image, andnginx.confhas nolocation /api/. A relative base URL in production would answer a login POST with 405. - Session cookie. HttpOnly, SameSite=Lax.
httpClient.jssendscredentials: 'include'and warns in DEV if the base URL is cross-origin (httpClient.js:72-84). - Envelope.
{ data, meta }in, bare records out —request()returnspayload.dataand dropsmetaentirely (httpClient.js:226). - Errors.
{ error: { code, message, details } }becomes aKrowApiErrorcarrying.status,.code,.details. A transport failure becomesstatus: 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 fromworker_profiles.earned_badges.GET /api/v1/badgeswould 404 if this hook were ever wired. Fix in this repo by deleting the hook, or accept it as unreachable. - User.
'User'is inENTITY_NAMESandRESOURCE_PATHS, socreateEntity('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 /meis fired once at module load and shared, so the elevenme()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 —AssistantPanelContextdecides whether Owliver starts open in auseStateinitialiser. That is why the last user is mirrored tolocalStorageunderkrow_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
hiredand 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 on every host. 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 (base44.owliver.suggestions) → src/lib/krowHooks.js (fetchOwliverSuggestions, useOwliverSuggestions) → src/lib/skills/serverSuggestions.js → KrowAssistant.jsx |
| Backend source registers it | go-api/internal/httpserver/owliver.go:22 |
https://mcp.krowforce.com serves it |
No — 404 as of this writing |
The endpoint now also builds real context: with no query, the backend counts
the organization's positions, applications, interviews, staff, profiles,
courses and activity in one statement and ranks the page's readings against
them, so a suggestion changes when the data does. See the backend contract's
§2A.
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/v1answers401when the session cookie is absent — a registered route and a nonexistent one alike.curlagainst the host proves nothing on its own. The404only appears once a request is authenticated and reaches the router, which is why it shows up in the browser and not in a terminal. 401versus404is the whole diagnosis. Through the dev proxy,401means the session is not signed in;404means the session is fine and the route is not on that backend.
The panel treats the two differently, in fetchOwliverSuggestions
(src/lib/krowHooks.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. Against a
host that does not serve the route the chip row is simply empty — the panel, the
composer and every answer are unaffected.
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.
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'sui: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 toNO_OWLIVER— disabled, and behaviourally identical to before the feature existed. enabled: falseswitches it off without deleting what was written.steps:≡periods:— the same list under two names.- Inheritance. A response with no
sourcetakes the first declaredui:section'ssource,periods,limitandeditable, 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.
editableneeds both halves. The source must bewritableand 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:
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 blockdata: {"delta": "..."}— appended to the trailing text blockdata: [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'smd, 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 collapsesmainto nothing. Deliberatelymdand notlg: tablets lay the inline column out acceptably, so only phones change presentation.useIsPhone()usesmatchMedia, notinnerWidth. When anything on the page overflows, mobile browsers widen the layout viewport to fit it, andinnerWidthreports 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.100dvhfollows the address bar but knows nothing about the keyboard, so a100dvhsheet puts its own composer underneath the keys. Returnsnullwhere the API is missing, and the caller falls back to the100dvhclass. No fixed pixel heights anywhere.- Width is clamped against
useViewportWidth()and persisted tokrow_assistant_widthon 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 |
Fixed. The file is deleted. It was 173 lines and imported by nothing. |
krow-demo/src/api/seed.js |
Fixed. The default user shape moved to src/api/demoUser.js, so no production module imports the fixture and its 1,928 lines are out of the bundle. seed.js remains, serving scripts/skill-check.mjs and scripts/owliver-capture.mjs as the test fixture it now is. |
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_candidateorassign_employeeaudit 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.