RP UI Axis — 02 UI/API Axis Surface Patch Packet
02 — UI/API Axis Surface Patch Packet (Workstream A)
Access status
UI/API source lives at web/server/api/registries-pivot/{summary,rows,node}.get.ts inside the incomex-nuxt container. The MCP read_file allowlist is /opt/incomex/docs, /opt/incomex/dot/specs, /var/log/nginx only — Nuxt source is NOT readable from this session. Therefore this is a PATCH-READY operator packet, not a live patch. Redeploy is operator-gated (Nuxt rebuild). No Nuxt math anywhere — every count/flag is already computed in PG.
Backend is fully live and read-only
The four data sources the UI needs already exist in PG and were verified live (doc 01):
v_registries_pivot_axis_surface(DAG-aware nodes)v_axis_topic_pivots(axis pivots)v_axis_topic_decision_queue(NEW this macro — decision queue)fn_topic_node_substrate(text)(substrate resolver)
Routes to add (mirror existing registries-pivot pattern)
Create sibling group web/server/api/axes/:
| Method/Route | File | PG source |
|---|---|---|
GET /api/axes |
axes/index.get.ts |
SELECT axis_code, axis_name, domain, status, owner_governance_ref FROM axis_registry |
GET /api/axes/:axis_code |
axes/[axis_code]/index.get.ts |
axis_registry row + v_axis_topic_pivots |
GET /api/axes/:axis_code/nodes |
axes/[axis_code]/nodes.get.ts |
v_registries_pivot_axis_surface WHERE axis_code=$1 |
GET /api/axes/:axis_code/node/:node_code |
axes/[axis_code]/node/[node_code]/index.get.ts |
one surface row |
GET /api/axes/:axis_code/node/:node_code/substrate |
.../substrate.get.ts |
SELECT fn_topic_node_substrate($node_code) |
GET /api/axes/:axis_code/pivots |
axes/[axis_code]/pivots.get.ts |
v_axis_topic_pivots |
GET /api/axes/:axis_code/decision-queue |
axes/[axis_code]/decision-queue.get.ts |
v_axis_topic_decision_queue WHERE axis_code=$1 |
Reference handler (thin SELECT passthrough — copy the registries-pivot style)
// web/server/api/axes/[axis_code]/nodes.get.ts
import { defineEventHandler, getRouterParam, createError } from 'h3'
import { pgQuery } from '~/server/utils/pg' // same helper the registries-pivot routes use
export default defineEventHandler(async (event) => {
const axisCode = getRouterParam(event, 'axis_code')
if (!axisCode) throw createError({ statusCode: 400, statusMessage: 'axis_code required' })
// NO math here. The view already computed every count, flag, and status.
const rows = await pgQuery(
`SELECT node_code, node_label, lifecycle_status, governance_status,
parent_codes, has_multiple_parents, count_value, child_count,
assignment_count, warning_flags, grouping_status, pin_state
FROM v_registries_pivot_axis_surface
WHERE axis_code = $1
ORDER BY count_value DESC, node_code`,
[axisCode]
)
return { axis_code: axisCode, count: rows.length, nodes: rows }
})
// web/server/api/axes/[axis_code]/decision-queue.get.ts
export default defineEventHandler(async (event) => {
const axisCode = getRouterParam(event, 'axis_code')
const rows = await pgQuery(
`SELECT node_code, node_label, evidence_strength, review_bucket,
proposed_classification, proposed_lifecycle_action,
duplicate_synonym_suspicion, iu_count, document_count,
evidence_tag_count, source_tag_keys, gov_council_decision_needed,
approval_ref, governance_note
FROM v_axis_topic_decision_queue
WHERE axis_code = $1`,
[axisCode]
)
return { axis_code: axisCode, count: rows.length, queue: rows }
})
// web/server/api/axes/[axis_code]/node/[node_code]/substrate.get.ts
export default defineEventHandler(async (event) => {
const nodeCode = getRouterParam(event, 'node_code')
const [row] = await pgQuery(`SELECT fn_topic_node_substrate($1) AS substrate`, [nodeCode])
return row?.substrate ?? null
})
Live response samples (from prod 2026-06-04)
GET /api/axes →
[{"axis_code":"AX-TOPIC","axis_name":"Chu de noi dung (Topic Axis)","domain":"content_topic","status":"CANDIDATE","owner_governance_ref":"GOV-COUNCIL (pending ratification)"}]
GET /api/axes/AX-TOPIC/nodes → 7 rows; first:
{"node_code":"TOPIC-CAND:knowledge_graph","node_label":"knowledge_graph","lifecycle_status":"candidate","governance_status":"UNGOVERNED_CANDIDATE","parent_codes":[],"has_multiple_parents":false,"count_value":10,"child_count":0,"assignment_count":10,"warning_flags":["CANDIDATE_NODE","ORPHAN_TOPIC_NODE"],"grouping_status":"OK","pin_state":"UNPINNED"}
GET /api/axes/AX-TOPIC/decision-queue → 7 rows; first:
{"node_code":"TOPIC-CAND:knowledge_graph","evidence_strength":"STRONG","review_bucket":"LIKELY_ROOT","proposed_classification":"ROOT_CANDIDATE","proposed_lifecycle_action":"PROPOSE_PROMOTE_TO_ROOT","duplicate_synonym_suspicion":null,"iu_count":10,"gov_council_decision_needed":true,"approval_ref":null}
GET /api/axes/AX-TOPIC/node/TOPIC-CAND:knowledge_graph/substrate → resolver jsonb with 10 information_units, 10 evidence_tags, null taxonomy_node/birth_record/governance_owner, empty parents/documents/workflows.
Frontend component set (additive)
- Axis selector dropdown over
/api/axes(today: "Topic Axis [CANDIDATE]" badge). - Decision queue table over
/api/axes/AX-TOPIC/decision-queue: columns evidence_strength badge, review_bucket chip, proposed action, IU count, sibling-suspicion warning, "Send to GOV-COUNCIL" action (files an approval_requests row — see doc 04, owner-gated; UI only drafts). - Node tree over
/api/axes/AX-TOPIC/nodes: root layer = empty parent_codes; child layer expanded by parent_code; multi-parent → render each path (never collapse). - Lifecycle + governance badges from lifecycle_status / governance_status.
- Substrate panel rendering resolver jsonb.
- Grouping warning when grouping_status=GROUPING_REQUIRED.
- Pin bound to existing registry_pin mechanism (axis pins = future).
Redeploy checklist (operator)
- Add the 7
axes/*.get.tshandlers (copy registries-pivot handlers; swap view names/columns above). - Add the axis selector + decision-queue table + node tree + substrate panel components.
- Link from
/knowledge/registries-pivot→ an "Axes" tab (legacy tree untouched). docker build+ redeployincomex-nuxt(operator-gated).- Smoke test:
/api/axesreturns AX-TOPIC;/api/axes/AX-TOPIC/nodesreturns 7;/api/axes/AX-TOPIC/decision-queuereturns 7; substrate for knowledge_graph returns 10 IUs.
Forbidden-action compliance
No Nuxt-side count math; legacy tree path untouched; DAG never forced into one tree; no redeploy executed; no hardcoded topic levels (depth comes from parent_codes arrays).