npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aiquants/enrichment-react-router

v0.8.0

Published

React Router (v7 / v8) adapter for storage AI enrichment & knowledge governance admin console.

Readme

@aiquants/enrichment-react-router

React Router (v7 / v8) adapter for the Storage AI Enrichment & Knowledge Governance operations console: a set of nested routes — the console's shell and one route per tab — with their loaders and actions.

Overview

@aiquants/enrichment-react-router puts the console's administration of knowledge indexing, RAG pipelines, graph stores, vector indices and the peripheral services' health under one base path.

  • The shell is the layout route at the base path: it draws the console foundation's root, the page heading 運用管理, the breadcrumb, the tab row of links, the current tab's heading and the document history panel, and renders the tab's route in its outlet.
  • Each tab is a route nested under the shell (<basePath>/<tab>; the base path itself shows the ingest tab). Each route's loader reads only what its screen draws, and each route that sends commands has an action that accepts only its own intents.
  • The server side is the main entry (.); the screens are the subpath entries ./console/shell and ./console/<tab>, so a host's route module per tab loads that tab's screen alone.

Prerequisites

  • Node.js >= 18.0.0 (engines); the react-router peer sets the effective floor: >= 20.0.0 for React Router 7, higher for some 8.x releases (8.3.0 requires >= 22.22.0)
  • React >= 19.0.0 and React DOM >= 19.0.0 (peer dependencies)
  • React Router 7 or 8 (react-router ^7.0.0 || ^8.0.0, peer dependency)
  • zod ^3.25.0 (peer dependency; the route action validates form payloads with it)
  • @aiquants/authz-core, @aiquants/console-ui, @aiquants/enrichment-core, @aiquants/enrichment-react, @aiquants/enrichment-governance-core, @aiquants/enrichment-governance-react and @aiquants/sse (peer dependencies; the audit card follows its runs on an EventSource through @aiquants/sse)

Installation

pnpm add @aiquants/enrichment-react-router @aiquants/authz-core @aiquants/console-ui @aiquants/enrichment-core @aiquants/enrichment-react @aiquants/enrichment-governance-core @aiquants/enrichment-governance-react @aiquants/sse 'zod@^3.25.0'

React, React DOM and React Router come from the host application. zod carries the peer range because a bare zod installs the registry's latest tag, which is outside ^3.25.0.

Usage

Build the server side once, in a server-only module:

// storage-admin.server.ts
import { createStorageAdminConsole } from "@aiquants/enrichment-react-router"

export const storageAdminConsole = createStorageAdminConsole({ store, requireUser, requirePermission, resolveTenantId, authorizeIngest, authorizeHistoryCapture, canReadIntegrityFindings, canUpdateKnowledge, reportFault })

Declare the shell as a layout route at the base path and every tab as a child route, the ingest tab also as the index route:

// routes.ts
route("storage/admin", "routes/storage-admin.tsx", { id: "storage-admin" }, [
    index("routes/storage-admin/ingest.tsx", { id: "storage-admin-index" }),
    route("ingest", "routes/storage-admin/ingest.tsx", { id: "storage-admin-ingest" }),
    route("search", "routes/storage-admin/search.tsx", { id: "storage-admin-search" }),
    // objects, pipeline, knowledge-graph, parameters, prompts, knowledge-authority, data-retention
])

The shell's route module re-exports the shell's loader and action and draws the shell:

// routes/storage-admin.tsx
import { createStorageAdminShouldRevalidate, StorageAdminConsoleShell } from "@aiquants/enrichment-react-router/console/shell"
import { storageAdminConsole } from "./storage-admin.server"

export const loader = (args: LoaderFunctionArgs) => storageAdminConsole.shell.loader(args)
export const action = (args: ActionFunctionArgs) => storageAdminConsole.shell.action(args)
export const shouldRevalidate = createStorageAdminShouldRevalidate(["resolve_entity"])

export default function StorageAdminShellRoute() {
    return <StorageAdminConsoleShell basePath="/storage/admin" documentHistory={DOCUMENT_HISTORY_HOST} />
}

Each tab's route module re-exports its tab's loader (and action, when the tab sends commands) and draws the tab with the host functions it uses:

// routes/storage-admin/ingest.tsx
import { createStorageAdminShouldRevalidate, StorageAdminTabErrorBoundary } from "@aiquants/enrichment-react-router/console/shell"
import { StorageAdminIngestTab } from "@aiquants/enrichment-react-router/console/ingest"
import { storageAdminConsole } from "../storage-admin.server"

export const loader = (args: LoaderFunctionArgs) => storageAdminConsole.tabs.ingest.loader(args)
export const action = (args: ActionFunctionArgs) => storageAdminConsole.tabs.ingest.action(args)
export const shouldRevalidate = createStorageAdminShouldRevalidate(["resolve_entity"])
export const ErrorBoundary = StorageAdminTabErrorBoundary

export default function IngestTabRoute() {
    return <StorageAdminIngestTab onRefreshIngestQueue={refreshIngestQueue} />
}

Pass the host functions and host objects as module constants. A new identity re-reads nothing on the audit, policy conflict and ledger cards: they read the functions through refs and key their reads on what they read (a query, a pair's id), never on the host. graphTraversal is the exception: its relationship types are re-read when it changes (below).

createStorageAdminShouldRevalidate(hostIntents) is the shouldRevalidate of every console route: a submission whose intent changes nothing a console loader reads skips the revalidation — inspect_uri, match_file_passwords and capture_document_state always, and the intents the host's store answers without a write (hostIntents, e.g. resolve_entity for a store that refuses it) — and everything else keeps React Router's default. A write revalidates the shell's loader and the shown tab's, and a move to another tab runs the new tab's loader alone, since the shell's parameters do not change. It is built with @aiquants/route-revalidation's createShouldRevalidate (skipIntents on the intent field, every other case left to React Router), and its type StorageAdminShouldRevalidate is that package's RouteShouldRevalidate, which takes currentUrl and nextUrl as React Router always passes them.

The host ports (CreateStorageAdminConsoleOptions)

Every port below except the two optional settings of the last row is required, so a host that forgets one fails to compile instead of serving the console unguarded. A judgment port (canReadIntegrityFindings, canUpdateKnowledge) answers a refusal with false; a throw fails the loader that asks it (and the action that asks it), as a throw of requirePermission other than AuthzDeniedError does.

| Port | Returns | Notes | | --- | --- | --- | | store | StorageAdminStore | The reads and writes below. | | requireUser(request) | Promise<StorageAdminOperator \| null> | The operator (below); null without a session. | | requirePermission(request, { resourceKey, action }) | Promise<unknown> | Throws AuthzDeniedError to refuse. Every loader asks read and every action asks update, for resourceKey (default storage_admin). The shell's loader and the tab's run in parallel on one request and share one admission: requireUser, requirePermission and resolveTenantId run once per request. | | resolveTenantId(request) | string \| Promise<string> | The tenant every read and write acts in. There is no default tenant: the tenant is the isolation axis. | | canReadIntegrityFindings(request) | Promise<boolean> | Whether the operator may read every document (a file read scoped to no rows and no columns). The integrity audit's findings name documents, so the findings cells and the repair are offered only when it is true; when it is false the action refuses, before calling the store, a REPAIR start with FORBIDDEN, and resync_findings and rebuild_child_trees with 403 (the data-retention tab's rebuild panel then offers its start as unavailable, with the reason). The host's findings route should use the same judgment. | | canUpdateKnowledge(request) | Promise<boolean> | Whether the operator may write the document-authority ledger and decide policy conflicts: true exactly when the host would answer this operator's policy conflict summary with viewer.canDecide true (compute both from one function, so the ledger and the conflict queue never disagree). It is a permission, not a deployment state: a deployment that disables knowledge writes still answers true, and its write routes refuse with DEPLOYMENT_DISABLED, which the cards state. The loader answers it, so the ledger's first render (and a server render) already knows it. | | authorizeIngest(request, payload) | Promise<string \| null> | The ingest refusal in the operator's words, or null to allow. | | authorizeHistoryCapture(request, metadataId) | Promise<HistoryCaptureRefusal \| null> | Whether the operator may record the named document's current state (capture_document_state), decided before the store is called, in this order: the deployment's recording switch (503 when off or misconfigured), the operator's document reading (403 without file:read or with a column-scoped one), the named document's current row (404 when missing or unreadable). A refusal { status, error } is the action's answer and the store is not called. error is shown by the history panel after its own sentence: a 403's is the cause only (文書の状態を記録する権限がありません。<error>。), any other a phrase without a final 。 (文書の状態を記録できませんでした (<error>)。); "" leaves the panel's sentence alone. A throw propagates. | | reportFault(request, fault) | string | Called once for every fault of a store call the console answers at its boundary (see "Store refusals and faults"), with fault = { operation, error } (StoreFault; operation is a StoreOperation, error the thrown value itself). Log it at error level with the host's own request id and the error object, and return that id (non-blank): it is the reference the operator quotes. A throw of this port is dropped and the answer carries no reference. | | healthProbeOptions? / resourceKey? | | Optional probe settings and the permission resource key. |

The store contract (StorageAdminStore)

The host implements one object and injects it through createStorageAdminConsole. Beyond the configuration, governance and health reads, the store owns:

| Method | Returns | Notes | | --- | --- | --- | | getPipelineStepMetrics(tenantId) | readonly PipelineStepMetric[] | One aggregate per (documentKind, stepKey) over the newest pipelineStepMetricWindow records of that pair (@aiquants/enrichment-core); documentKind is the kind of the run the records belong to. Read from the step table, never measured in process. The loader checks every entry against PipelineStepMetricSchema and throws (naming the method and each issue) when one is not a metric — one without documentKind included — or when a pair appears twice, because the flow figure shows the first match and would silently drop the second. | | pipelineStepMetricWindow (property) | number | How many of the newest records per (documentKind, stepKey) each metric aggregates: the same constant the aggregation query uses. The loader throws, before it queries anything, when it is not a positive safe integer. | | getRecentPipelineRuns(tenantId, request) | readonly RecentPipelineRunEntry[] | The tenant's most recent runs, newest first, without their steps, for the operator request acts for: each entry is a readable run ({ kind: "readable", item: PipelineRunSummary }) or the restricted mark of a run the quarantine (RESTRICTED) withholds (RestrictedRunMark: startedAt, finishedAt, status only). A run the operator's row scope alone hides is left out; the readability and mark predicates are applied inside the query, and its row limit counts runs and marks together after them. The loader checks every entry against RecentPipelineRunEntrySchema and throws (naming the method and each issue) when one is neither — a steps key included, because a full PipelineRunRecord is assignable to the summary type and would otherwise ship every step to the browser on each load, and any key a mark does not carry. | | getRecentIngestItems(tenantId, request) | readonly RecentIngestEntry[] | The ingest list for the operator request acts for: readable rows ({ kind: "readable", item: RecentIngestItem }, whose lastRun is the latest readable run, that run's RestrictedRunMark, or null, and whose unlock is how the row's revision lost its password, or null) and the RestrictedIngestMark of each row the quarantine withholds (the document's current row RESTRICTED, or a readable document's RESTRICTED revision row, an older one included; a mark carries no latest run). The host's ingest-queue route answers the same list through the same judgment (IngestQueueRefresher). | | listFilePasswords(tenantId, request) | FilePasswordListing | The tenant's file passwords for the operator request acts for, in the order they are tried and without their passwords ({ kind: "listed", passwords }, checked against FilePasswordListSchema), { kind: "forbidden" } for an operator the upstream refuses, or { kind: "unavailable", status } for an upstream that answers nothing usable, so the parameters tab stays drawn either way. | | saveFilePassword(tenantId, draft, accessToken) | FilePasswordCommandReply | Saves a new row (passwordId null) or rewrites one (a null password keeps the sealed one) upstream in the operator's name: { ok: true } or { ok: false, status, errorCode }. The password travels to the upstream only; the store never logs or keeps it. | | deleteFilePassword(tenantId, passwordId, accessToken) | FilePasswordCommandReply | Deletes a row upstream in the operator's name. | | matchFilePasswords(tenantId, facts, accessToken) | FilePasswordMatchReply | The rows a file with the facts would be tried with, and the rows a regular expression ran past its time on ({ ok: true, match }, checked against FilePasswordMatchSchema); it writes nothing. | | promoteUnlockedCopy(tenantId, metadataId, accessToken) | UnlockPromotionReply | Overwrites a document's source with its unlocked copy upstream in the operator's name: { ok: true, promotion } ({ promoted: true, copyRemoved }, or { promoted: false, refusedReason } when the source still cannot be written; nothing changed then) or { ok: false, status, errorCode }. | | getFailedSyncItems(tenantId, request) | readonly FailedSyncEntry[] | The failed rows for the operator request acts for: readable rows ({ kind: "readable", item: FailedSyncItem }) and the RestrictedFailureMark (failedAt, syncStatus only) of each row the quarantine withholds. | | getDocumentReadRefusal(tenantId, request) | string \| null | Why the operator may read no document of the three lists (no file:read, or a column-scoped one), as a full sentence, or null. The loader reads it in the same wave as the lists and ships it as documentReadRefusal; such an operator's lists are empty (no mark either). | | getSourceCatalog(tenantId, request) | SourceCatalog | The deployment's source catalog (GET /ai/sources, @aiquants/enrichment-core's SourceCatalogSchema) for the operator request acts for. The ingest loader reads it in the same wave as the lists and ships it as sourceCatalog; a failure fails the loader rather than an empty catalog standing in for it, because an empty catalog says that the deployment reads no source. | | ingestDocument(tenantId, payload, actor, accessToken?) | DocumentIngestOutcome | { kind: "ingested", result }, or { kind: "refused", status: 403 \| 500 \| 503, error } for a refusal decided inside the host's own transaction after authorizeIngest passed (the revision writer's scope refusal, the pre-write history capture's lock timeout, any other failure of that capture); error is shown as given, never a driver message. { kind: "grant", href, error } asks the operator to grant a permission first (a Google scope the operator's token lacks, for one) at href, a root-relative address of the host such as its consent sign-in. A throw is answered at the boundary (a StoreRefusal with 400 and its sentence, anything else as a fault). | | captureDocumentState(tenantId, metadataId, actor) | DocumentStateCaptureOutcome | Records the named document's current state after authorizeHistoryCapture admitted it: { kind: "recorded" }, { kind: "unchanged" }, or { kind: "failed", status: 404 \| 500 \| 503, error } (error a phrase without a final 。, or ""). A throw is answered as a 500 without a reason and its cause is discarded, so the store logs a failure before it throws for it. | | getStrategyVariants(tenantId) | readonly StrategyVariant[] | { strategyId, variantKey, label, embeddingModel, domainKey }, one per tm_enrichment_strategy row the tenant can see (its own rows and the SYSTEM rows). strategyId is the identity: every domain has its own default row, so a variant key repeats across domains. | | getEnrichmentStrategies(tenantId) | EnrichmentStrategyRow[] | The per-domain strategies. Also the one source of the domain list the ingest console and the classification-rule editor offer ({ key: domainKey, label: domainLabel }, distinct, in board order). | | retryAllFailedSyncItems(tenantId, actor?, accessToken?) | RetryAllFailedResult | { requeued, skipped: { metadataId, reason }[] } — every row the retry left where it was is named, so the console never reports an all-clear over skipped rows. | | readIntegrityAuditOverview(tenantId) | IntegrityAuditOverview | The integrity audit's overview read from the recorded runs (@aiquants/enrichment-core): the database's clock (readAt), the last completed run with its quantities, the active run, a newer unsuccessful run. No store is measured. The loader checks the answer against IntegrityAuditOverviewSchema and catches this read alone: a throw or a contract violation becomes the overview's own failure (integrityAudit), and the rest of the console is still drawn. A StoreRefusal's sentence is that failure's reason, shipped in the loader data and shown to the operator as given, so state as a refusal only what an operator may read (the core's range sentences, assertSystemParameterInRange, as they are); anything else thrown is a fault (reportFault) whose reason is the fixed sentence with the reference, never the error's message. | | startIntegrityRun(tenantId, mode, basisRunUid, accessToken) | IntegrityCommandReply | Starts a SCAN (basisRunUid null) or a REPAIR (the basis scan's UID) upstream in the operator's name; the token is required. Answers { ok: true, runUid } or { ok: false, status, errorCode } (the upstream status and business code; no upstream wording). A throw is answered UPSTREAM_FAILED without a status and its cause is discarded (the package has no logger and shows no upstream wording), so the store logs a failure it throws for. | | cancelIntegrityRun(tenantId, runUid, accessToken) | IntegrityCommandReply | Cancels a run (or closes one whose heartbeat stopped), answered like the start. | | resyncIntegrityCell(tenantId, request, actor, accessToken) | IntegrityCellResyncResult | One chunk of the findings inspector's bulk resync: requeues the documents recorded in one UNRECORDED cell of the tenant's latest completed run, in recorded order after request.after, at most INTEGRITY_FINDINGS_PAGE_LIMIT_MAX (100), each through the host's one requeue rule (the rule retrySyncItem and retryAllFailedSyncItems apply), and answers { requeued, skipped: { metadataId, reason }[], nextAfter } (@aiquants/enrichment-core's IntegrityCellResyncResultSchema; nextAfter is the last sequence read when the chunk read exactly the limit, else null). The action calls it only for an operator canReadIntegrityFindings admits, with a request IntegrityCellResyncRequestSchema accepted, and checks the answer before the console reads it. Throw a StoreRefusal to stop (e.g. the run is no longer the tenant's latest completed run, so the findings the operator approved are no longer shown); the action answers its sentence as the inspector's refusal, and anything else thrown is a fault. accessToken is undefined when the operator holds none. | | rebuildChildTrees(tenantId, request, actor, accessToken) | ChildTreeRebuildResult | One chunk of the data-retention tab's bulk rebuild of the child trees: reads one keyset page of candidates from the API server in the operator's name (the tenant's current, undeleted, non-derived roots holding at least one live node of a kind that carries parts, in ascending identifier order after request.after, at most CHILD_TREE_REBUILD_CHUNK (100)), passes each through the host's one requeue rule of a resync (the rule retrySyncItem, retryAllFailedSyncItems and resyncIntegrityCell apply) and answers { requeued, skipped: { metadataId, reason }[], nextAfter } (@aiquants/enrichment-core's ChildTreeRebuildResultSchema; nextAfter is the last root read when the chunk read exactly the limit, else null; after and nextAfter are at most Number.MAX_SAFE_INTEGER). The requeue rule does not apply the operator's row or column scope of the file read, so the action calls it only for an operator canReadIntegrityFindings admits, with a request ChildTreeRebuildRequestSchema accepted, and checks the answer before the console reads it. Throw a StoreRefusal to stop (e.g. the candidates cannot be read); the action answers its sentence as the panel's refusal, and anything else thrown is a fault. Each requeued root runs every step of its flow again, the models' costs included. accessToken is undefined when the operator holds none. | | getSystemParameters(tenantId) | SystemParameter[] | The value arrives already in the type data_type names: boolean for bool, number for int / float, string for string / json. The store owns the conversion from stored text. |

The store reads no single run and no document history: the history panel and the recent changes card read through the injected documentHistory host (below), whose routes and authorization belong to the host.

The operator (StorageAdminOperator)

requireUser(request) is required and resolves Promise<StorageAdminOperator | null>: null when the request carries no session, otherwise the operator the request acts for.

| Field | Type | Meaning | | --- | --- | --- | | email | string | The address every write of the action is attributed to (updated_by, and the actor argument of the store's writes). An id or a display name is not an identity the audit trail can be read back through, so there is no other field to fall back on. | | accessToken | string \| null | The operator's OAuth access token, forwarded unchanged to the store calls that take one (retrySyncItem, retryAllFailedSyncItems, resyncIntegrityCell, rebuildChildTrees, startIntegrityRun, cancelIntegrityRun, ingestDocument, inspectFileUri, saveFilePassword, deleteFilePassword, matchFilePasswords, promoteUnlockedCopy). null when the session holds no token the upstream services accept — a mock or test session included; the package knows no host's mock values, so the host answers null for them, and a start or cancel is then refused with FORBIDDEN without calling the store. "" is not a token: the action throws. |

The host maps its own session profile onto these two fields (for a Google session profile, email is emails[0].value). The package reads nothing else from the session and never probes the host's object, so a profile that does not carry email and accessToken does not type-check as an operator.

import type { StorageAdminOperator } from "@aiquants/enrichment-react-router"

const requireUser = async (request: Request): Promise<StorageAdminOperator | null> => {
    const session = await readSession(request)
    if (session === null) return null
    return { email: session.emails[0].value, accessToken: isUsableAccessToken(session.accessToken) ? session.accessToken : null }
}

Route data

The shell's loader answers StorageAdminShellData and each tab's loader answers StorageAdminTabData[<tab>], both picked from one vocabulary of documented fields (StorageAdminConsoleFields). Each tab reads only what its screen draws, so opening a tab costs that tab's reads alone:

| Route | Fields | Store reads | | --- | --- | --- | | shell | tenantId, displayTimeZone | none | | search | strategyVariants | getStrategyVariants | | ingest (and the index route) | confidentialityPolicies, enrichmentStrategies, recentIngestItems, documentReadRefusal, sourceCatalog | getConfidentialityPolicies, getEnrichmentStrategies, getRecentIngestItems, getDocumentReadRefusal, getSourceCatalog | | objects | documentReadRefusal | getDocumentReadRefusal (the list itself is read a page at a time through the tab's host) | | pipeline | featureFlags, pipelineStepMetrics, pipelineStepMetricWindow, recentPipelineRuns, documentReadRefusal, enrichmentStrategies, systemHealth | getFeatureFlags, getPipelineStepMetrics, getRecentPipelineRuns, getDocumentReadRefusal, getEnrichmentStrategies, and checkAllSystemHealth | | knowledge-graph | ontologyStatus, graphSchema, unresolvedEntities | getFeatureFlags, getSystemParameters and getSyncCounts once (the snapshot getOntologyStatus reads), then getOntologyStatus, getGraphSchema, getUnresolvedEntities | | parameters | featureFlags, classificationRules, classificationRuleConsumer, enrichmentStrategies, confidentialityPolicies, policiesMissingDestination, promptTemplates, transcriptionVocabulary, transcriptionReferences, unresolvedReferenceUris, systemParameters, filePasswords | the matching get… reads, and the store's classificationRuleConsumer | | prompts | promptTemplates | getPromptTemplates | | knowledge-authority | canUpdateKnowledge | none (the host judgment canUpdateKnowledge) | | data-retention | integrityAudit, canReadIntegrityFindings, syncCounts, totalFiles, failedSyncItems, documentReadRefusal, qdrantCollections, qdrantHealth | readIntegrityAuditOverview, getSyncCounts, getFailedSyncItems, getDocumentReadRefusal, getQdrantCollections, and checkQdrantHealth |

The fields:

  • tenantId — the shell's tenant; every tab reads it through the shell's context.
  • displayTimeZone — the IANA time zone every recorded instant is written in, resolved on the server (Intl.DateTimeFormat().resolvedOptions().timeZone). The shell sets the console root's time zone from it (ConsoleUiRoot of @aiquants/console-ui, locale="ja", formatLocale="ja-JP", test id storage-admin-console-ui, its announcer storage-admin-console-ui-announcer), and every card reads the zone from the root — no card takes it as a prop — so the server-rendered page and the hydrated one print the same string. SyncStatusMonitor's failedSyncItems[].item.failed_at (and a mark's failedAt) must therefore be ISO 8601 instants with an explicit offset, or null.
  • pipelineStepMetrics — the step aggregates above, one per (documentKind, stepKey); the pipeline flow viewer draws its per-step figures from these and from nothing else, showing only the selected kind's own entry (a kind without one reads 未測定, never another kind's numbers).
  • pipelineStepMetricWindow — the store's pipelineStepMetricWindow, passed to PipelineFlowViewer as metricSampleWindow so the figure states the window its numbers cover. The pipeline loader refuses a window that is not a positive safe integer before it queries anything.
  • recentPipelineRuns — the readable runs and run marks above, listed under the flow viewer.
  • recentIngestItems / failedSyncItems — the readable rows and marks of the ingest list and the failed rows above.
  • documentReadRefusal — the store's getDocumentReadRefusal: null, or the sentence shown in place of each of the three lists (no row, no empty-list sentence).
  • integrityAudit — ResourceOutcome<IntegrityAuditOverview> (@aiquants/console-ui/core): the store's overview, or { ok: false, status: null, reason } when that one read failed; the rest of the tab is still drawn.
  • canReadIntegrityFindings / canUpdateKnowledge — the two host judgments above, answered per request.
  • qdrantHealth — the Qdrant probe alone (checkQdrantHealth): the Qdrant card's badge and response time.
  • strategyVariants — the variants above, offered by the model-comparison workbench.
  • enrichmentStrategies — the strategies above; the domain options are derived from them in the ingest and parameters tabs.
  • systemParameters — the typed parameters above.

Every timestamp the host ships in route data or through documentHistory (startedAt, finishedAt, createdAt, settledAt, lastStartedAt, checkedAt, and a failed row's failed_at or a mark's failedAt when not null) must be an ISO 8601 instant with an explicit offset (Z or ±hh:mm), as Date.prototype.toISOString() writes it: @aiquants/enrichment-react's RecordedInstant throws RangeError for anything else rather than guess the zone. The integrity audit's instants are checked by the core's contract where the loader reads them.

Route action validation

Each route's action accepts only the intents its route carries (STORAGE_ADMIN_ACTION_INTENTS):

| Route | Intents | | --- | --- | | shell | capture_document_state | | ingest | inspect_uri, ingest_document, retry_job, promote_unlocked_copy | | pipeline | update_enrichment_strategy, refresh_health | | knowledge-graph | resolve_entity | | parameters | toggle_flag, reorder_rules, update_confidentiality_policy, save_transcription_term, save_transcription_reference, update_parameter, save_file_password, delete_file_password, match_file_passwords | | prompts | update_prompt | | data-retention | start_integrity_run, cancel_integrity_run, retry_job, retry_all_failed, resync_findings, rebuild_child_trees |

The search, objects and knowledge-authority tabs send no command through their routes, so their routes have no action. Before any intent is acted on, every action refuses the request as a whole:

| Condition | Answer | | --- | --- | | requireUser resolves null | 401 { error: "認証が必要です。" } | | The operator's email is empty or only whitespace | 403 { error: "操作者を特定できないため設定を変更できません。" } | | The operator's accessToken is "" | Throws Error (a host error: answer null for a session without a usable token); it propagates to the route's error boundary and nothing is written. | | requirePermission throws AuthzDeniedError for update | 403 { error: "権限がありません。" }, except that a start_integrity_run / cancel_integrity_run is refused on the audit card: 403 { failure } with FORBIDDEN and no status (below), and a capture_document_state in the history panel: 403 { captureFailure: { status: 403, reason: "" } }. Only this refusal reads the form, to choose the answer; a request the gate admits is read after the gate. A refused body that is not a form (JSON, or none) names no intent and gets the same 403 { error }. Any other error requirePermission throws propagates. |

Every loader throws Response 401 (認証が必要です。) when requireUser resolves null, and 403 (権限がありません。) when requirePermission throws AuthzDeniedError for read, before it reads the store.

Each intent then answers { success: true, ... } or refuses with { error }:

| Intent | Refused with | Refused when | | --- | --- | --- | | refresh_health | — | Never; answers { success: true, refreshed: true } (the revalidated loader re-probes). | | update_enrichment_strategy | 400 / 500 | stage is not a canonical enrichment stage key; strategyId or domainId is not a positive safe integer; the store throws a StoreRefusal (its sentence is the error); 500 for a fault. | | toggle_flag | 400 / 500 | flagKey is empty or is not one of the tenant's flags reported by getFeatureFlags; the store throws a StoreRefusal while reading the flags or writing one (its sentence is the error); 500 for a fault. | | update_confidentiality_policy | 400 / 500 | confidentialityLevel or capability is not a canonical value; allowCloudProvider is 'true' for a capability that sends nothing outside the deployment; the store throws a StoreRefusal (its sentence is the error); 500 for a fault. | | save_transcription_term | 400 / 500 | term is blank; boost is present but not a finite number greater than zero (absent defers to the service); id is present but not a positive safe integer (absent means a new row); the store throws a StoreRefusal (e.g. the row belongs to another tenant or no longer exists; its sentence is the error); 500 for a fault. | | save_transcription_reference | 400 / 500 | fileUri is blank; id is present but not a positive safe integer; the store throws a StoreRefusal (e.g. a URI the host's document-URI rule refuses; its sentence is the error); 500 for a fault. | | update_parameter | 400 / 500 | paramKey is empty or is not one of the tenant's parameters, or paramValue is not readable as the parameter's data_type (bool: 'true' / 'false'; int: an integer literal within the safe range; float: a finite number; json: valid JSON), or the core's range table (assertSystemParameterInRange of @aiquants/enrichment-core) bounds the key and the value lies outside it (<key> は <min>〜<max> の整数で指定してください。). The raw string is passed to the store on success. A StoreRefusal the store throws while reading the parameters or writing one is 400 with its sentence, a fault 500. | | update_prompt | 400 / 500 | payload is not valid JSON; templateKey is empty; payload is not { templateText: string; modelName: string; temperature: number; maxOutputTokens: number; responseSchema?: object \| string \| null; endpointBaseUrl: string \| null } (unknown keys refused); the store throws a StoreRefusal; 500 for a fault. | | reorder_rules | 400 / 500 | orderedRuleIds is not valid JSON, or is not a non-empty array of distinct positive safe integers; the store throws a StoreRefusal (its sentence is the error); 500 for a fault. | | retry_job | 400 / 500 | 400 when fileId is empty or the store throws a StoreRefusal; 500 for a fault. | | retry_all_failed | 400 / 500 | 400 when the store throws a StoreRefusal; 500 for a fault. Success answers { success, requeued, skipped }. | | resync_findings | 403 / 400 / 500 / 502 | 403 { error } when canReadIntegrityFindings answers false (checked before the form; the store is not called; a throw of the judgment is thrown, never read as false). 400 { error } naming the issues unless the form (every field but intent) passes IntegrityCellResyncRequestSchema: runUid (a UUID), store, scopeKey, itemClass UNRECORDED, and an optional after written as a positive decimal integer (any other spelling, an unknown field such as limit, or another class is refused; the store is not called). 400 { error } with the sentence of a StoreRefusal the store throws, 500 { error } for a fault. 502 { error } when the store's answer breaks IntegrityCellResyncResultSchema (not shown as a result). Success answers { success, cellResync }. | | rebuild_child_trees | 403 / 400 / 500 / 502 | 403 { error } when canReadIntegrityFindings answers false, the gate of resync_findings (checked before the form; the store is not called; a throw of the judgment is thrown, never read as false): the rebuild requeues every candidate in the operator's name and the host's requeue rule does not apply the operator's row or column scope. 400 { error } naming the issues unless the form (every field but intent) passes ChildTreeRebuildRequestSchema: only an optional after written as a positive decimal integer up to Number.MAX_SAFE_INTEGER (any other spelling, a larger number, or another field such as limit, is refused; the chunk's size is the contract's CHILD_TREE_REBUILD_CHUNK; the store is not called). 400 { error } with the sentence of a StoreRefusal the store throws, 500 { error } for a fault. 502 { error } when the store's answer breaks ChildTreeRebuildResultSchema (not shown as a result). Success answers { success, childTreeRebuild }. | | start_integrity_run | 400 / failure | 400 { error } unless the form is mode=SCAN without basisRunUid or mode=REPAIR with a UUID basisRunUid. Success answers { success, runUid }. A failure answers { failure } (below). | | cancel_integrity_run | 400 / failure | 400 { error } unless runUid is a UUID. Success answers { success, runUid }. A failure answers { failure } (below). | | resolve_entity | 400 / 500 | 400 when entityId is empty, action is neither merge nor promote, or the store throws a StoreRefusal; 500 for a fault. | | ingest_document | 400 / 403 / 500 / 503 | 400 when payload is absent or empty, is not valid JSON, or is refused (not an object; fileUri absent, not a string, or blank; confidentialityLevel absent or not one of PUBLIC, INTERNAL, DEPARTMENT, CONFIDENTIAL, RESTRICTED; departmentId present but neither an integer nor null — an absent departmentId is accepted), and when the store throws a StoreRefusal; 500 for a fault. 403 when the host's authorizeIngest(request, payload) returns a refusal (the refusal is the error). The store's { kind: "refused", status, error } is answered with its status (403, 500, 503) and { error }, and its { kind: "grant", href, error } with 403 { error, grant } (an href that is not root-relative — another origin, //host, a scheme or a backslash — is thrown instead of answered). Success answers { success, result }. | | capture_document_state | 400 / 403 / 404 / 500 / 503 | 400 { error } unless metadataId is written as a positive decimal integer within the safe range (neither the judgment nor the store is called). A refusal of authorizeHistoryCapture (403 / 404 / 503) is answered with its status and { captureFailure: { status, reason: error } }, and the store is not called. The store's failed is answered the same way with its status (404 / 500 / 503); a throw, or an outcome of no known kind (only a store breaking its type can answer one; the mapping is an exhaustive switch, so a new kind fails to compile), is 500 with reason "". Success answers { outcome: "RECORDED" } or { outcome: "UNCHANGED" } and nothing else (no state id, sequence or facet). | | inspect_uri | 400 / 500 / 501 | 400 when fileUri is blank, the store throws a StoreRefusal (500 for a fault), or the inspection's fileSize is not a non-negative safe integer (the count rule the ingest console draws it with; the error names fileSize, so a broken size never reaches the console as a value); 501 when the store implements no inspectFileUri. Success answers { success, inspection }. | | save_file_password | 400 / 403 / 404 / 422 / 502 | 400 when the board's fields are malformed (passwordId not empty and not a positive integer; a blank label or one past its width in characters; a password past FILE_PASSWORD_MAX_BYTES UTF-8 bytes; a priority that is not ASCII digits up to FILE_PASSWORD_PRIORITY_MAX; an unknown source kind or syntax; a condition past its width; isActive neither 'true' nor 'false'; a field missing) or a new row has no password; 403 for an operator without an access token (before the store is called) or one the upstream refuses; 404 for a row that is gone; 422 for an input the upstream refuses (a regular expression it cannot compile); 502 with the status otherwise, and when the store throws (the outcome is unknown). No answer carries the password. | | delete_file_password | 400 / 403 / 404 / 502 | 400 unless passwordId is a positive integer; otherwise as save_file_password. | | match_file_passwords | 400 / 403 / 422 / 502 | 400 for a blank uri (fileName, sender and subject are null when empty); otherwise as save_file_password. Success answers { success, filePasswordMatch }. | | promote_unlocked_copy | 400 / 403 / 404 / 409 / 502 | 400 unless metadataId is a positive integer; 403 for an operator without an access token or one the upstream refuses; 404 when the copy is gone; 409 when the source still cannot be written (its reason, nothing changed) or changed since the copy was made (UNLOCK_ERROR_CODES.sourceChanged); 502 with the status otherwise, and when the store throws. | | any other value, and an intent the route does not carry | 400 | Unknown intent: <intent>, before the store is read. |

A start or cancel failure is { failure }, an IntegrityCommandFailure of @aiquants/enrichment-core that the core's contract admits: FORBIDDEN without a status when requirePermission refuses update, when the operator holds no access token, or for a REPAIR when canReadIntegrityFindings answers false (the store is not called); the store's { ok: false, status, errorCode } mapped to the command's own vocabulary by integrityCommandFailureCodeOf(command, status, errorCode); UPSTREAM_FAILED without a status when the store throws (its cause is discarded; the store logs it) or answers something that is not a reply. The response's HTTP status states the failure: the status the core's code table (INTEGRITY_COMMAND_CODES) gives the code, and 502 for UPSTREAM_FAILED, the one code the table has no row for; not being a success, it keeps a router from re-reading its loader by default, and the data-retention tab re-reads exactly after the failures that outdate the overview (integrityFailureNeedsOverviewReread of @aiquants/enrichment-governance-react).

Store refusals and faults

Every intent that calls the store, and the data-retention loader's read of the integrity audit's overview, answers what a store call threw at one boundary, so a store never decides what reaches the browser by the wording of an error. Every other loader read propagates: a failed loader is the page's error.

| What the store throws | The answer | | --- | --- | | StoreRefusal (exported from the server entry; checked by isStoreRefusal, which reads only its brand, so an instance built by another copy of the module is still a refusal and an error that merely names itself StoreRefusal is not) | 400 { error } with its sentence, shown as it is; reportFault is not called. In the overview's read, the sentence is the overview's reason. | | Anything else (a database, driver or transport error, an upstream's contract violation, a broken invariant, a value that is not an Error) | A fault: reportFault(request, { operation, error }) is called once. Every intent answers 500 { error } with the same sentence, サーバーで障害が起きました。時間をおいてもう一度実行し、続くようなら開発担当に連絡してください (要求 ID: <reference>)。, the reference being what reportFault returned; each screen names the operation in its own sentence and states this one as the reason. In the overview's read, the reason is the phrase サーバーの障害、要求 ID: <reference>, since the audit card puts the reason inside its own parentheses. The fault's message, name and cause never reach the browser. When reportFault throws, the sentence ends at …連絡してください。 and the phrase is サーバーの障害, both without a reference. |

The sentence and the phrase are exported from the server entry as storeFaultSentenceOf(reference: string | null): string and storeFaultReasonOf(reference: string | null): string, the builders this boundary answers with, so a host's own HTTP routes answer a fault with exactly the console's texts: log the fault under an id of the request and pass that id as the reference (a route serving the integrity audit's overview answers storeFaultReasonOf, which the audit card shows as it shows the loader's).

So throw a StoreRefusal with a sentence composed for the operator (keep a cause with { cause }: it is never answered), and let every other failure propagate as it is. reportFault receives the operation (StoreOperation) for the host's log: read_integrity_audit for the overview's read, and one per intent that calls the store, named as the intent and carried by each of its store calls: resolve_entity, update_enrichment_strategy, toggle_flag, update_confidentiality_policy, save_transcription_term, save_transcription_reference, update_parameter, update_prompt, reorder_rules, retry_job, retry_all_failed, resync_findings, rebuild_child_trees, ingest_document and inspect_uri.

The screens read these answers as data (answer.error), the same way for 400 and 500. capture_document_state, start_integrity_run, cancel_integrity_run, promote_unlocked_copy and the file password commands answer their store's throws with their own fixed failures (above). A failure of resolveTenantId, of authorizeIngest, or of reading the form propagates out of the action to the route's error boundary.

Styling contract

  • Borders name their style. Every Tailwind border-width utility in this package's sources (border, border-b, …) sits in the same class list as border-solid, and every divide-x / divide-y next to divide-solid, so the console draws its borders under any host — including one whose base layer sets border-style: none on every element (e.g. * { @apply border-none }). tests/borderStyleContract.spec.ts scans every string literal in src/ and fails on a width without its style; @aiquants/enrichment-react holds the same rule, and @aiquants/enrichment-governance-react draws its borders with inline border: 1px solid ….
  • One card-title scale. Every card title (h2) on every tab — this package's 直近の実行 and the panels of @aiquants/enrichment-react and @aiquants/enrichment-governance-react alike — is 16 px bold; tests/consoleTabs.spec.tsx checks each tab.
  • The shell and the tabs paint nothing themselves (G14). The modules under src/console and src/components hold no className, no inline style, no colour literal and no token variable read in code: every colour comes from a console foundation component's colour role (AAA in both schemes). The 直近の実行 table is the one source not yet moved onto the foundation; it styles with Tailwind and marks its root data-cui-migration="pending", which the console's browser checks leave out. tests/migratedSources.spec.ts enumerates the sources and fails on a paint outside that list.

Shell and tab props

A host read answers every failure it can describe as an outcome. The reads whose failure a screen itself shows (loadIntegrityOverview, the four reads of documentHistory, onBrowseGraphNodes) settle even when the function breaks that contract: a rejection or a synchronous throw is shown as a failed read whose reason is the error's message, so no control stays busy and no panel stays loading. The ingest queue's refresh shows no failure: a failed round keeps the rows and the next round reads again.

StorageAdminConsoleShell (./console/shell) is the layout route's component:

| Prop | Required | Purpose | | --- | --- | --- | | basePath: string | yes | Where the console is mounted: an absolute path without a trailing slash (/storage/admin); every tab's route is <basePath>/<tab>. The shell throws RangeError for another form, and when it is drawn at a location that is neither the base path nor one of its tabs. | | documentHistory?: DocumentHistoryHost | no | { loadHistory(metadataId, signal), compare(query, signal), readLines(query, signal), loadChanges(cursor, signal) } (@aiquants/enrichment-react's host type, re-exported here), each resolving to a ResourceOutcome; pass a module constant. Without it the history panel, the recent changes card and every entry point (詳細, 履歴, 開く) are absent. A failed read's reason is a short phrase without a final 。 and a 403's names only the cause the route declared ("" when none). Every read settles even when the function rejects or throws (a failure without a status whose reason is the error's message), and a history whose run step carries a report string with nothing to read (empty, or only white space of JavaScript's and Python's \s) is answered to the panel as a failed read naming the run, the step and the field, since the panel's step timeline would refuse it while rendering. | | onGrant?: (href: string) => Promise<boolean> | no | The host's way of letting the operator grant, at href, the permission a refused action needs (a consent popup, for one); it resolves whether the grant completed. The ingest tab offers it under the refusal of an ingest_document answered { error, grant } (storage-admin-error-alert-grant), then states what the grant came to (storage-admin-error-alert-grant-result); the operator runs the action again. Without it the refusal is stated with no button. | | renderHeader?: (props: { title, tenantId }) => ReactNode | no | The host's page head (its title band, say). Without it the shell draws the h1 運用管理 itself; with it the host's head carries the page's one h1. | | data? / captureFetcher? | no | Overrides for tests and special mounts: the shell's data in place of the route loader's, and the fetcher that carries the history panel's capture_document_state only. |

Each tab's component (./console/<tab>) is drawn by the tab's route module under the shell, and throws when it is drawn outside it. Every tab takes data? (the tab's data in place of the route loader's) and every tab that submits takes fetcher? (the data-retention tab also integrityFetcher?, which carries the audit card's start and cancel only, inspectorFetcher?, which carries the findings inspector's retry_job and resync_findings only, and rebuildFetcher?, which carries the chunks of the child trees' rebuild, rebuild_child_trees, only), overrides for tests and special mounts; the components are mounted under a data router in every case. The host functions each tab takes:

| Prop | Tab | Required | Purpose | | --- | --- | --- | --- | | host: ProcessingObjectHost | StorageAdminObjectsTab | yes | Reads one page of the processing-objects list ({ loadPage(filter, cursor, signal) }, @aiquants/enrichment-react's host type, re-exported here), resolving to a ResourceOutcome<ProcessingObjectPage>; the first page carries the total. The list is the tab itself, so the host must supply it; this package names no route. | | onSearch: MetadataSearchRunner | StorageAdminSearchTab | yes | Runs one document search ((params: DocumentSearchParams, signal: AbortSignal) => Promise<DocumentSearchAnswer>, the public API's GET /ai/search/documents) and rejects with an Error whose message the workbench shows. The workbench is the search tab itself, so the host must supply the runner; this package names no route. Every answer carries the tiers that ran, the answering worker's breakers and the scopes, so the tab's loader reads none of them. | | onCompareVariants?: ModelComparisonRunner | StorageAdminSearchTab | no | Runs a side-by-side search over two strategy rows ((input: VariantComparisonInput, signal: AbortSignal) => Promise<VariantComparisonAnswer>, the public API's POST /ai/search/compare-variants, strategy_ids answered per strategy_id); the model-comparison workbench (below the search workbench) is absent without it. A comparison that could not run rejects with an Error, whose message is shown as given and which clears the previous results. | | onRefreshIngestQueue?: IngestQueueRefresher | StorageAdminIngestTab | no | () => Promise<IngestQueueRefresh> — refetches the ingest queue while an entry is being processed (a readable row by its sync status or by a RUNNING run mark standing in for its latest run, a mark by its own sync status; a mark whose syncStatus is null, a document with no enrichment record, is not in progress). { kind: "rows", rows } replaces the rows shown (readable rows and marks, the loader's list's shape); { kind: "refused", reason } (the host route's 403 and its { error }) withdraws the rows shown, states reason where the loader's refusal stands (the ingest console's listRefusal) and stops refreshing until the loader delivers again; a rejection keeps the rows and tries again on the next round. Rows the loader delivers restart the rounds and drop the answer of a refresh still in flight, so a refresh that began before a revalidation never replaces its newer rows. Never started for an operator the loader refused. | | graphTraversal?: GraphTraversalHost | StorageAdminKnowledgeGraphTab | no | { run: GraphTraversalRunner; listRelationshipTypes: GraphRelationshipTypesReader } — graph traversal and the relationship types it may follow, supplied together; the traversal form is absent without it. listRelationshipTypes answers the deployment's offer ({ ok: true, relationshipTypes }, e.g. from the governance backend's POST /ai/governance/admin/kg/relationship-types, or { ok: false, reason }); the form offers exactly those types, says why nothing can be traversed when the offer is empty, and re-reads when the function changes — keep the object stable (a module constant). A traversal that names no type follows the whole offer. run hands the backend's rows over unchanged (label, from_label, to_label included): the result map names each node by label and identifier, so an Entity and a File with the same identifier stay two nodes, and offers only Entity nodes as the next start. | | onBrowseGraphNodes? | StorageAdminKnowledgeGraphTab | no | Reads one page of a label's nodes; the index is absent without it. | | onSimulateGovernance?: SimulationRunner | StorageAdminPromptsTab | no | Governance dry run; the dry-run console is absent without it. | | policyConflicts: PolicyConflictHost | StorageAdminKnowledgeAuthorityTab, StorageAdminDataRetentionTab | yes | The summary, the list's pages, a pair's detail, the decision and the detection (@aiquants/enrichment-governance-react); reads answer ResourceOutcome, the decision and the detection CommandOutcome with the core's rejection body. | | documentAuthority: DocumentAuthorityHost | StorageAdminKnowledgeAuthorityTab | yes | The ledger's pages, the unregistered candidates, the replaceable rows and the command (CommandOutcome with the core's rejection body). | | loadIntegrityOverview: IntegrityOverviewLoader | StorageAdminDataRetentionTab | yes | (signal) => Promise<ResourceOutcome<IntegrityAuditOverview>> — the same overview the store reads, through the host's own route. The tab re-reads it on the ingestion stream's signals and once at the active run's staleAfter, reads it once whenever the document becomes visible again while an overview is shown, and calls it from the reload of a failed first read. A 403 keeps its status, and its reason names only the cause the route declared (which permission or scope to fix), or is "" when it declared none (@aiquants/console-ui's ResourceOutcome contract): the tab states the refusal itself and follows it with that cause. | | ingestionEvents: IngestionEventsStream | StorageAdminDataRetentionTab | yes | The host's stream of ingestion events, which the audit card follows its runs on instead of polling: url (an absolute path on the console's origin), cursorParam (the query parameter the host reads the cursor from when the tab reopens the stream), terminalEvents (the host's own terminal events), heartbeatStaleAfterMs (the silence after which the tab reopens it) and connection (IngestionEventsConnection: the reopen policy of the host's stream contract, the connection fields of @aiquants/sse's useReopeningEventSource). The stream relays the API's integrity_run events and its terminal resync-required as they are (INGESTION_EVENT_NAMES of @aiquants/enrichment-core). Pass a module constant: a new url opens a new stream, and the terminal events are fixed while it is open. | | loadIntegrityFindings: IntegrityFindingsLoader | StorageAdminDataRetentionTab | yes | (query, signal) => Promise<ResourceOutcome<IntegrityFindingPage>> — a page of one cell's findings, for the audit card's findings inspector. |

The pipeline and parameters tabs take no host function.

Routes and tabs

The shell (<basePath>) is the layout route; each tab is a child route at <basePath>/<tab>, and the base path itself is the index route, which shows the ingest tab:

| Tab | Route | Name | | --- | --- | --- | | search | <basePath>/search | メタデータ検索ワークベンチ | | ingest | <basePath>/ingest and <basePath> | ドキュメント取り込み & メタ生成 | | objects | <basePath>/objects | 処理オブジェクト一覧 | | pipeline | <basePath>/pipeline | パイプライン & 接続状態 | | knowledge-graph | <basePath>/knowledge-graph | ナレッジグラフ探索 | | parameters | <basePath>/parameters | モデル & パラメータ設定 | | prompts | <basePath>/prompts | プロンプト & ドライラン | | knowledge-authority | <basePath>/knowledge-authority | 権威台帳 & 規程衝突 | | data-retention | <basePath>/data-retention | データ保持 & 整合性監査 |

STORAGE_ADMIN_TABS (./console/shell) is this list in its order, STORAGE_ADMIN_INDEX_TAB the index tab and storageAdminTabHref(basePath, tab) a tab's route.

  • The shell draws, in order, the page heading (h1 運用管理, storage-admin-title, or the host's renderHeader), the breadcrumb 運用管理 › <tab> (storage-admin-breadcrumb; its first item links to the base path), the tab row (storage-admin-tabs), the current tab's heading (h2, storage-admin-tab-heading), the document history panel when it is open, and the tab's outlet.
    • The tab row is @aiquants/console-ui's Tabs: a nav named 運用管理の画面 of real links (storage-admin-tabs-<tab>), the current one aria-current="page". A plain click moves through the router, so the shell stays mounted and only the new tab's loader runs; a click with a modifier key or another button opens the real href.
    • After a move to another tab (a link, the breadcrumb, the browser's back and forward), the shell moves focus to the tab's heading and announces <tab> を表示しました through the console root. The first render moves no focus and announces nothing.
    • A tab reads the shell's context (useOutletContext): the base path, the tenant, the document history and the move to another tab (openTab(key, search?), search being the query of the tab moved to, which the processing-objects tab reads its filters from).
  • Only the shown tab is mounted, so leaving a tab discards what was typed there and not sent — the ledger's and the conflict queue's drafts included — and a tab states the answers of its own submissions only while it is shown: an answer that settles after the operator moved to another tab is not stated (a write still revalidates the loaders).
  • A tab's refusals ({ error } of its last settled submission) are stated in the tab's own alert (storage-admin-error-alert, role="alert", read again for a repeated refusal).
  • A tab whose loader or render fails is replaced by its route's error boundary (StorageAdminTabErrorBoundary, ./console/shell): the alert <tab> を表示できませんでした (<reason>)。 (storage-admin-tab-error-alert) and 再読み込み, which runs the loaders again. The reason is a thrown response's text body or status text, or an error's message, which React Router replaces with a generic one in production. The shell, the tab row and the history panel stay, so the operator can move to another tab. A loader that refuses the request (401, 403) fails the shell's loader too, so that refusal reaches the host's own boundary.
  • parameters (モデル & パラメータ設定) draws FeatureFlagBoard (a flag being toggled drawn in its requested state while the submission is in flight), ClassificationRuleEditor, the alert naming the policy rows that will fail for a missing destination (policies-missing-destination), ConfidentialityPolicyBoard (its prompt choices are the tab's prompt rows), TranscriptionAdaptationBoard and SystemParameterTable.
  • prompts (プロンプト & ドライラン) draws PromptTemplateConsole (a save sends every editable field) and, with onSimulateGovernance, GovernanceDryRunConsole.
  • knowledge-graph (ナレッジグラフ探索) draws OntologyStatusCard and KnowledgeGraphExplorer (its node index read one page at a time through onBrowseGraphNodes, a failed read stated as such rather than as an empty index).
  • pipeline (パイプライン & 接続状態) draws, in order, PipelineFlowViewer, the 直近の実行 table, EnrichmentStrategyBoard and InfrastructureHealthMonitor.
    • PipelineFlowViewer draws the document-kind flows with the tenant's feature flags laid over each step's effective gate policy for that flow, and, for the selected step, the selected kind's own metric with its window (metricSampleWindow = pipelineStepMetricWindow).
    • It draws no circuit breaker: the breakers belong to the search path, which the pipeline never consults, and only a search answer reports them, so a breaker opened by failed searches is never drawn on the projection step.
    • The 直近の実行 table lists recentPipelineRuns: document, kind, trigger, attempt, status, failed step, started / finished, requester.
    • It writes the status badge (PIPELINE_RUN_STATUS_PRESENTATION[pipelineRunPresentationKey(run)], so a failed run with a scheduled retry reads 失敗 (再試行予定)), the trigger (PIPELINE_TRIGGER_LABELS, the raw value kept in data-trigger) and the times (@aiquants/enrichment-react's RecordedInstant / FinishedInstant, written in the console root's time zone, which the shell sets from displayTimeZone) exactly as the history panel and the ingest queue do.
    • It names the kind and the failed step with @aiquants/enrichment-react's documentKindLabel and ledgerStepLabel, so a kind or a step the current vocabulary does not know reads 知らない種類 (<kind>) or 知らない段 (<key>) instead of breaking the row.
    • Its horizontal scroller is a containing block (relative), so the visually hidden header of the button column cannot widen the page at narrow widths.
    • Headers, run / document ids, kind, trigger, attempt, times and the 詳細 button never wrap (whitespace-nowrap); only the failed step and the requester (wrap-anywhere) wrap, so a squeezed table breaks long values instead of splitting 詳細 or きっかけ mid-word.
    • A restricted run mark is one row of the same table (pipeline-run-mark-<position>, data-restricted-mark="run"): its start and finish times, 隔離 (RESTRICTED) の文書 in the document column and its status, the other cells empty — no run id, kind, trigger, attempt, failed step, requester or 詳細. Its React key is its position in the answer (restricted:<position>), a readable run's its id (readable:<id>).
    • With a non-null documentReadRefusal the table states it in place of its body (document-read-refusal-pipeline, a warning Notice): no run and no empty-table sentence, since the operator cannot read the runs, which is not the same as there being none.
  • The document history panel (@aiquants/enrichment-react's DocumentHistoryInspector) opens in the shell above the tab and stays open across tabs. The panel owns the selection; the shell passes the document, what the entry point selects (DocumentHistoryTarget) and an openRequest that advances on every open from outside, a press naming what is already shown included (the panel then re-reads the history and moves focus to its heading).
    • A recent run's 詳細 opens that run's document selecting that run ({ kind: "run", runId }); an ingest row's or an audit finding's 履歴 opens the document selecting its newest readable state ({ kind: "latest_state" }); the recent changes card's 開く opens the change's document selecting its arriving state ({ kind: "state", stateId }).
    • Re-targeting keeps the one panel mounted. On 閉じる or Escape focus returns to the entry point that opened the panel most recently; when that element has left the page (the operator moved to another tab), returnFocusFallback hands focus to the current tab's heading.
    • The panel reads through documentHistory as described under Shell and tab props; its failures, its 403 withdrawal and its restricted-mark group are the panel's own.
    • Its 今の状態を記録 submits capture_document_state with the document's metadata id to the shell's action on a fetcher of its own (captureFetcher), so no other submission aborts it; the answer settles the panel's command. { outcome } is announced (記録しました / 前の記録から変化はありません) and the history read again; { captureFailure } is stated by the panel from its status and reason; any other answer (the action's refusals before it reads the intent: no session, a blank email, a malformed metadataId) is a failure without a status whose reason is that error with exactly one final 。 removed, so the panel's sentence does