@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-agentWhat 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 carryingdata-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 })— mountshtmlintoeland 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.