Admin cockpit

Operator panels that write through an onAction callback you own.

@ascent/seo-agent/admin is a small set of zero-dependency, pure-ESM browser widgets for an operator cockpit: proposals, keywords, rankings, competitors, an autonomy dial, and a status readout. Same honesty conventions as widgets — measured data is labeled as measured, nothing is claimed that wasn't observed.

Install

npm i @ascent/seo-agent

What ships

Six widgets — a representative set, not the full catalog. Long-tail widgets (e.g. content diff review, budget controls) are deferred.

import {
  table, actionButton, handleActionClick, mountPanel,
  buildProposals, mountProposals,
  buildKeywords, mountKeywords,
  buildRankings, mountRankings,
  buildCompetitors, mountCompetitors,
  buildAutonomyDial, mountAutonomyDial,
  buildStatusPanel, mountStatusPanel,
} from '@ascent/seo-agent/admin'

The write model: onAction, not a write endpoint

SP1 (the deployed service) has no operator-facing write route — no proposal-approve, no settings-write. Its routes (analyze, generate, rankings, competitors, content, run, status) run the engine pipeline, not admin CRUD. So every admin widget that has an action button renders it as data-action/data-payload and dispatches clicks through handleActionClick to a consumer-supplied onAction(action, payload) callback — it never calls a write endpoint that doesn't exist. You wire onAction to whatever backend you use to persist the decision (your own API, a database write, a queue, etc.). The kit's job stops at "the operator clicked approve with this payload."

Panel framework

Shared by every widget below (from admin/panel.mjs).

  • table(headers, rows) — escapes and renders an HTML table.
  • actionButton(label, action, payload) — a button carrying data-action + data-payload

(JSON-stringified).

  • handleActionClick(event, onAction) — reads the nearest [data-action] ancestor of the

click target and calls onAction(action, payload).

  • mountPanel(el, { html, onAction }) — mounts html into el and wires a single click

listener that dispatches through handleActionClick.

Proposals

mountProposals(el, proposals, { onAction })

Renders each proposal's type + rationale with Approve/Reject buttons. Actions: approve, reject — payload { id }.

Keywords

mountKeywords(el, keywords, { onAction })

One row per keyword with a track/untrack toggle. Action: toggle-target — payload { keyword }.

Rankings — measured

mountRankings(el, rows)

No onAction — read-only. Positions are measured on Google; a keyword with no observed position renders "not yet measured" rather than a guess. No actions.

Competitors

mountCompetitors(el, list, { onAction })

One row per discovered domain with a "Mark as peer" button. Action: mark-peer — payload { domain }.

Autonomy dial

mountAutonomyDial(el, settings, { onAction })

Per-category off/human/auto control, settings a map of category -> level. Action: set-autonomy — payload { category, level }.

Status panel — reads SP1 status

mountStatusPanel(el, {}, { client })

Calls client.status() (the same makeAscentClient used by widgets) and renders which capabilities are configured. Fails soft — an unconfigured or errored client renders "Ascent is not configured" instead of throwing. Read-only, no onAction.

Not shipped

No Web Components, no framework wrappers, no build step, no write endpoint on SP1 — writes are entirely the consumer's responsibility via onAction. No admin widgets beyond the six above exist yet.