Files
turnstone/turnstone/console/static/style.css
T
Patrick Buckley be165c1971 feat: MCP resource and prompt discovery with read_resource tool (#44)
* feat: MCP resource and prompt discovery with read_resource tool

Extends MCPClientManager with resource and prompt discovery alongside
existing tool support. Resources and prompts are discovered on connect,
cached per-server with copy-on-write rebuilds, and refreshed via push
notifications, periodic polling, or manual /mcp refresh.

New read_resource built-in tool reads MCP resources by URI. Requires
user approval (same as MCP tool calls) since resources are served by
external MCP servers. Resource catalog injected into system message
with XML delimiters. Error messages sanitized to prevent leaking
server internals to the model.

Prompt discovery stores prefixed names (mcp__server__prompt) and
exposes get_prompt_sync() for future use_prompt tool (Chunk D).

/mcp command now shows tools, resources, and prompts. Docs and
diagrams updated.

* feat: MCP prompt governance sync with origin tracking and readonly guards

Migration 009 adds origin, mcp_server, and readonly columns to
prompt_templates. MCP prompts discovered by MCPClientManager are
automatically synced into the governance table as read-only templates
with origin="mcp".

Sync engine handles: create on connect, update on prompt refresh,
delete when prompts are removed from server. Manual templates take
precedence on name collision (MCP prompt skipped with warning).

Admin API returns 403 on update/delete of readonly templates. Console
UI shows MCP origin badge and disables edit/delete buttons. Storage
backends gain get_prompt_template_by_name, list_prompt_templates_by_origin,
and delete_prompt_templates_by_server methods.

Also addresses PR #44 review feedback: concurrent.futures.TimeoutError
handling in sync dispatch, XML-escape resource catalog descriptions,
resource template entries excluded from _resource_map, URI collision
warnings, needs_periodic capability-aware computation, malformed JSON
primary key fallback for read_resource.

* feat: use_prompt tool, prompt catalog, and PR review hardening

New use_prompt built-in tool invokes MCP prompt templates by name,
expanding them into messages. Requires user approval (external MCP
servers). Prompt catalog injected into system message with XML
delimiters (up to 30 prompts, HTML-escaped).

Prompt listener registered in session for catalog rebuild on changes.

Addresses PR #44 review feedback:
- _init_system_messages() now uses copy-on-write (build locally,
  assign atomically) so background thread callbacks never see
  partial system messages
- sync_prompts_to_storage() serialized behind _sync_lock to prevent
  races between set_storage() (main thread) and MCP background thread
- shutdown() clears listener lists to release callback references

Docs and diagrams updated for 18 built-in tools.

* feat: granular tool policies for MCP resources, prompts, and tools

Policy evaluation now uses approval_label (falling back to func_name)
for fnmatch pattern matching, enabling fine-grained per-URI and
per-server policies:
- read_resource: mcp_resource__{normalized_uri}
- use_prompt: mcp__{server}__{prompt} (prefixed name)
- MCP tools: mcp__{server}__{tool} (was static "mcp_tool")

URI normalization resolves .. path segments to prevent traversal
bypasses in policy matching. Resource templates filtered from system
message catalog (not directly readable). use_prompt arguments
validated as dict with string coercion.

TypeScript SDK PromptTemplateInfo gains origin, mcp_server, readonly
fields. Governance docs updated with MCP policy patterns.

* feat: MCP visibility in server and console UIs

Server health endpoint includes mcp.servers, mcp.resources, mcp.prompts
counts. Server UI status bar shows magenta MCP indicator with tooltip.
Console cluster status bar shows MCP metrics with magenta LED dot.
Console node detail view shows per-node MCP summary. Console collector
aggregates MCP counts across nodes in overview.

Uses var(--magenta) design token with new --magenta-glow for theme
adaptation. ARIA roles on MCP status elements. Tooltips on console
MCP metric labels. Node MCP summary hidden on mobile (< 700px).

New diagram: 20-mcp-architecture.puml covering full MCP lifecycle
(connection, discovery, refresh, governance sync, policy, UI).

* fix: McpStatus in health schema, count properties, catalog name fidelity

Adds McpStatus model to HealthResponse (Python + TypeScript SDKs) so
typed clients see the mcp field from /health.

Addresses Copilot review feedback:
- resource_count/prompt_count properties avoid list allocation on
  /health and /metrics polls
- get_tools/resources/prompts return shallow-copied dicts to prevent
  callers from mutating internal cache
- Prompt names and arg names in system message catalog are NOT
  HTML-escaped (model must use exact strings in use_prompt calls);
  only descriptions are escaped

* fix: OpenAPI spec McpStatus + diagram approval column accuracy

Adds McpStatus schema and optional mcp field to HealthResponse in
openapi-server.json, matching the Python schema and TypeScript types.

Fixes tool pipeline diagram: math, web_fetch, web_search correctly
shown as auto-approve (not "Yes" for approval).
2026-03-12 14:49:58 -07:00

1347 lines
40 KiB
CSS

/* ==========================================================================
turnstone console — page-specific styles
Extends shared base.css with console-only layout and components
========================================================================== */
/* ==========================================================================
Header overrides — wider padding for console layout
========================================================================== */
#header { padding: 10px 20px; gap: 16px; }
.header-dim {
color: var(--fg-dim);
font-weight: 400;
}
#cluster-summary {
font-size: 11px;
color: var(--fg-dim);
letter-spacing: 0.03em;
}
#theme-toggle { color: var(--fg); }
/* ==========================================================================
Main content
========================================================================== */
#main {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 20px 24px;
padding-bottom: 60px;
max-width: 1140px;
margin: 0 auto;
width: 100%;
}
/* ==========================================================================
Breadcrumb
========================================================================== */
.breadcrumb {
padding: 8px 20px;
font-size: 11px;
color: var(--fg-dim);
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
font-family: var(--font-display);
letter-spacing: 0.02em;
}
.breadcrumb a {
color: var(--accent);
text-decoration: none;
font-weight: 500;
}
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb-sep { margin: 0 8px; color: var(--fg-dim); opacity: 0.5; }
/* ==========================================================================
Cluster Status Bar — the instrument panel
Backlit, slightly raised, precision readouts
========================================================================== */
#cluster-status-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 44px;
background: var(--bg-surface);
border-top: 1px solid var(--border-strong);
display: flex;
align-items: center;
padding: 0 20px;
gap: 12px;
z-index: 100;
font-size: 12px;
/* Subtle top glow — like backlit instruments */
box-shadow:
0 -1px 0 var(--border),
0 -8px 24px -4px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
#cluster-status-bar::before {
content: '';
position: absolute;
top: -1px;
left: 10%;
right: 10%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent-glow-strong), transparent);
pointer-events: none;
}
.csb-states { display: flex; gap: 2px; align-items: center; }
.csb-state {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
white-space: nowrap;
color: var(--fg-dim);
font-size: 12px;
background: none;
border: 1px solid transparent;
font: inherit;
font-family: var(--font-mono);
}
.csb-state:hover {
background: var(--bg-highlight);
border-color: var(--border-strong);
color: var(--fg-bright);
}
.csb-state:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.csb-state.active {
background: var(--accent-dim);
border-color: var(--accent);
color: var(--accent);
}
.csb-state.active .csb-state-count { color: var(--accent); }
.csb-state.active .csb-state-label { color: var(--accent); opacity: 0.8; }
/* State indicator dots — LED effect with glow */
.csb-state-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.csb-state-dot[data-state="running"] { background: var(--green); border-radius: 2px; box-shadow: 0 0 6px var(--green-glow); }
.csb-state-dot[data-state="thinking"] { background: var(--cyan); box-shadow: 0 0 6px var(--cyan-glow); }
.csb-state-dot[data-state="attention"] { background: var(--yellow); border-radius: 1px; transform: rotate(45deg); box-shadow: 0 0 6px var(--yellow-glow); }
.csb-state-dot[data-state="idle"] { background: var(--fg-dim); opacity: 0.5; }
.csb-state-dot[data-state="error"] { background: var(--red); border-radius: 0; box-shadow: 0 0 6px var(--red-glow); }
.csb-state-count { font-weight: 600; color: var(--fg-bright); font-variant-numeric: tabular-nums; }
.csb-state-count.zero { color: var(--fg-dim); font-weight: 400; opacity: 0.6; }
.csb-state-label {
color: var(--fg-dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-family: var(--font-display);
font-weight: 500;
}
.csb-divider { width: 1px; height: 22px; background: var(--border-strong); flex-shrink: 0; }
.csb-metrics {
display: flex;
gap: 16px;
align-items: center;
color: var(--fg-dim);
font-size: 11px;
margin-left: auto;
white-space: nowrap;
}
.csb-metric { display: flex; align-items: center; gap: 5px; }
.csb-metric-value { color: var(--fg-bright); font-weight: 500; font-variant-numeric: tabular-nums; }
.csb-metric-label {
color: var(--fg-dim);
font-size: 10px;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.04em;
opacity: 0.7;
}
/* MCP indicator dot — LED effect with magenta glow */
.csb-mcp-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--magenta);
box-shadow: 0 0 4px var(--magenta-glow);
display: inline-block;
flex-shrink: 0;
}
.csb-loading { color: var(--fg-dim); font-size: 11px; font-style: italic; opacity: 0.8; }
#cluster-status-bar.stale { border-top-color: var(--yellow); }
#cluster-status-bar.stale::after {
content: 'STALE';
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
font-size: 9px;
font-family: var(--font-display);
font-weight: 600;
color: var(--yellow);
letter-spacing: 0.1em;
background: var(--bg-surface);
padding: 0 8px;
line-height: 1;
}
/* ==========================================================================
Section headers
========================================================================== */
.section-header {
font-family: var(--font-display);
font-size: 11px;
font-weight: 600;
color: var(--accent);
letter-spacing: 0.1em;
margin-bottom: 12px;
text-transform: uppercase;
}
/* ==========================================================================
Node table — column headers
========================================================================== */
.node-colheaders {
display: grid;
grid-template-columns: 1fr 50px 50px 50px 70px 60px 140px;
padding: 6px 16px;
background: var(--bg-surface);
border-bottom: 1px solid var(--border-strong);
font-size: 10px;
font-family: var(--font-display);
font-weight: 600;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.08em;
position: sticky;
top: 0;
z-index: 10;
}
.ncol-ws, .ncol-run, .ncol-attn, .ncol-tokens { text-align: right; }
.ncol-health, .node-cell-health { padding-left: 10px; }
/* ==========================================================================
Node rows — individual instrument readouts
========================================================================== */
.node-row {
display: grid;
grid-template-columns: 1fr 50px 50px 50px 70px 60px 140px;
padding: 9px 16px;
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
border-left: 3px solid transparent;
position: relative;
}
.node-row:nth-child(odd) { background: var(--bg); }
.node-row:nth-child(even) { background: var(--row-alt); }
.node-row:hover {
background: var(--bg-highlight);
box-shadow: inset 0 0 0 1px var(--border);
}
.node-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.node-row.has-attention { border-left-color: var(--yellow); background: linear-gradient(90deg, var(--yellow-glow), transparent 30%); }
.node-row.has-running { border-left-color: var(--green); background: linear-gradient(90deg, var(--green-glow), transparent 30%); }
.node-row.has-thinking { border-left-color: var(--cyan); background: linear-gradient(90deg, var(--cyan-glow), transparent 30%); }
.node-row.has-error { border-left-color: var(--red); background: linear-gradient(90deg, var(--red-glow), transparent 30%); }
.node-row.has-attention:hover, .node-row.has-running:hover,
.node-row.has-thinking:hover, .node-row.has-error:hover {
background: var(--bg-highlight);
}
.node-cell { font-size: 12px; display: flex; align-items: center; }
.node-cell-name {
color: var(--fg-bright);
font-weight: 500;
gap: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-cell-name .node-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green);
flex-shrink: 0;
box-shadow: 0 0 4px var(--green-glow);
}
.node-cell-name .node-dot.unreachable {
background: var(--red);
box-shadow: 0 0 4px var(--red-glow);
}
.node-cell-name .node-dot.degraded {
background: var(--accent);
box-shadow: 0 0 4px var(--accent-glow-strong);
}
.node-degraded-badge {
font-size: 9px;
font-family: var(--font-display);
font-weight: 600;
color: var(--accent);
background: var(--accent-dim);
border: 1px solid var(--accent);
border-radius: var(--radius-sm);
padding: 0 5px;
margin-left: 6px;
text-transform: uppercase;
letter-spacing: 0.06em;
line-height: 1.6;
flex-shrink: 0;
}
/* Version drift */
.node-cell-version {
font-size: 11px;
color: var(--fg-dim);
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
font-variant-numeric: tabular-nums;
}
.node-cell-version.drift { color: var(--yellow); }
.ncol-version { text-align: right; }
.node-version-drift-badge {
font-size: 9px;
font-family: var(--font-display);
font-weight: 600;
color: var(--yellow);
background: var(--yellow-glow);
border: 1px solid var(--yellow);
border-radius: var(--radius-sm);
padding: 0 5px;
margin-left: 4px;
text-transform: uppercase;
letter-spacing: 0.06em;
line-height: 1.6;
flex-shrink: 0;
}
.csb-version-drift .drift-warn {
color: var(--yellow);
font-weight: 600;
font-size: 11px;
letter-spacing: 0.06em;
}
.node-cell-num {
color: var(--fg-dim);
font-size: 11px;
justify-content: flex-end;
font-variant-numeric: tabular-nums;
}
.node-cell-num.has-value { color: var(--fg-bright); }
.node-cell-health { gap: 8px; font-size: 11px; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
/* Health bar — precision gauge */
.health-bar {
width: 80px;
height: 4px;
background: var(--bg-highlight);
border-radius: 2px;
overflow: hidden;
position: relative;
}
.health-bar-fill {
display: block;
height: 100%;
min-width: 3px;
border-radius: 2px;
transition: width 0.3s ease;
}
.health-bar-fill.low { background: var(--green); box-shadow: 0 0 4px var(--green-glow); }
.health-bar-fill.mid { background: var(--yellow); box-shadow: 0 0 4px var(--yellow-glow); }
.health-bar-fill.high { background: var(--red); box-shadow: 0 0 4px var(--red-glow); }
/* ==========================================================================
Node groups — rack panel sections
========================================================================== */
.node-group { margin-bottom: 1px; }
.node-group-header {
display: grid;
grid-template-columns: 1fr 50px 50px 50px 70px 60px 140px;
padding: 10px 16px;
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
background: var(--bg-surface);
border-left: 3px solid transparent;
border-bottom: 1px solid var(--border);
position: relative;
}
.node-group-header::before {
content: '';
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.15s;
pointer-events: none;
}
.node-group-header:hover { background: var(--bg-highlight); }
.node-group-header:hover::before { opacity: 1; }
.node-group-header:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.node-group-header.has-attention { border-left-color: var(--yellow); }
.node-group-header.has-running { border-left-color: var(--green); }
.node-group-header.has-thinking { border-left-color: var(--cyan); }
.node-group-header.has-error { border-left-color: var(--red); }
.node-group-name {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--fg-bright);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-group-chevron {
font-size: 10px;
color: var(--fg);
transition: transform 0.2s ease;
flex-shrink: 0;
width: 14px;
display: inline-block;
text-align: center;
}
.node-group-chevron.expanded { transform: rotate(90deg); }
.node-group-badge {
font-size: 10px;
font-family: var(--font-display);
color: var(--fg-dim);
background: var(--bg-highlight);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 1px 6px;
font-weight: 500;
letter-spacing: 0.02em;
}
.node-group-cell { font-size: 12px; display: flex; align-items: center; }
.node-group-cell.num { color: var(--fg-dim); font-size: 11px; justify-content: flex-end; font-variant-numeric: tabular-nums; }
.node-group-cell.num.has-value { color: var(--fg-bright); }
.node-group-body { overflow: hidden; }
.node-group-body.collapsed { display: none; }
.node-group-body .node-colheaders { position: static; z-index: auto; opacity: 0.85; }
.node-group-body .node-row { padding-left: 32px; }
.node-group-single .node-row { padding-left: 16px; }
/* ==========================================================================
Dashboard table — console overrides
========================================================================== */
.dash-row.has-link { cursor: pointer; }
.dash-row.has-link::after {
content: "\2197";
position: absolute;
right: 12px;
top: 10px;
font-size: 10px;
color: var(--fg-dim);
opacity: 0;
transition: opacity 0.15s;
}
.dash-row.has-link:hover::after,
.dash-row.has-link:focus-visible::after { opacity: 0.6; }
.dash-cell-node {
color: var(--accent);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: color 0.1s;
}
.dash-cell-node:hover { text-decoration: underline; color: var(--fg-bright); }
/* ==========================================================================
MCP summary in node detail
========================================================================== */
#node-mcp-summary {
color: var(--magenta);
font-size: 11px;
font-family: var(--font-mono);
margin-left: 12px;
}
/* ==========================================================================
Node link
========================================================================== */
.node-link {
display: inline-block;
margin-top: 16px;
color: var(--accent);
font-size: 11px;
text-decoration: none;
font-family: var(--font-display);
font-weight: 500;
letter-spacing: 0.02em;
padding: 4px 0;
border-bottom: 1px solid transparent;
transition: border-color 0.15s;
}
.node-link:hover { border-bottom-color: var(--accent); }
/* ==========================================================================
Pagination
========================================================================== */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 16px 0;
font-size: 12px;
color: var(--fg-dim);
}
.pagination button {
background: var(--bg-surface);
border: 1px solid var(--border-strong);
color: var(--fg-bright);
border-radius: var(--radius-sm);
padding: 5px 14px;
font: inherit;
font-size: 11px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
font-family: var(--font-display);
font-weight: 500;
}
.pagination button:hover { background: var(--bg-highlight); border-color: var(--accent-dim); }
.pagination button:disabled { opacity: 0.25; cursor: not-allowed; }
/* ==========================================================================
Toast override — position above cluster status bar
========================================================================== */
#toast { bottom: 56px; z-index: 200; color: var(--fg-bright); border-color: var(--border-strong); }
/* ==========================================================================
Header accent button (+ new)
========================================================================== */
#header .header-btn-accent {
color: var(--accent);
border-color: var(--accent);
font-weight: 600;
}
#header .header-btn-accent:hover {
background: var(--accent-dim);
color: var(--accent);
}
#header .header-btn-accent:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ==========================================================================
New Workstream Modal
========================================================================== */
#new-ws-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 500;
}
#new-ws-box {
background: var(--bg-surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius);
padding: 32px;
width: 380px;
max-width: 90vw;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.03),
0 24px 48px -12px rgba(0, 0, 0, 0.5),
0 0 80px -20px var(--accent-dim);
position: relative;
}
#new-ws-box::before {
content: '';
position: absolute;
top: -1px;
left: 20%;
right: 20%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
border-radius: 1px;
}
#new-ws-box h2 {
font-family: var(--font-display);
color: var(--accent);
font-size: 15px;
font-weight: 700;
margin-bottom: 18px;
letter-spacing: 0.02em;
}
#new-ws-box label {
display: block;
font-family: var(--font-display);
font-size: 10px;
font-weight: 600;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-top: 14px;
margin-bottom: 5px;
}
.label-hint {
font-weight: 400;
text-transform: none;
letter-spacing: 0;
opacity: 0.6;
}
#new-ws-box select,
#new-ws-box input[type="text"],
#new-ws-box textarea {
width: 100%;
padding: 9px 12px;
background: var(--bg);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
color: var(--fg);
font: inherit;
font-size: 13px;
transition: border-color 0.15s, box-shadow 0.15s;
}
#new-ws-box textarea { resize: vertical; min-height: 40px; }
#new-ws-box textarea::placeholder { color: var(--fg-dim); opacity: 0.6; }
#new-ws-box select:focus,
#new-ws-box input:focus,
#new-ws-box textarea:focus {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 3px var(--accent-dim);
}
#new-ws-box input::placeholder { color: var(--fg-dim); opacity: 0.6; }
#new-ws-box select {
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%238a93ad' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
#new-ws-error {
display: none;
color: var(--red);
font-size: 12px;
margin-bottom: 6px;
}
#new-ws-buttons {
display: flex;
gap: 10px;
margin-top: 22px;
justify-content: flex-end;
}
#new-ws-buttons button {
padding: 9px 20px;
border-radius: var(--radius-sm);
font: inherit;
font-size: 12px;
cursor: pointer;
border: 1px solid var(--border-strong);
background: var(--bg-highlight);
color: var(--fg);
transition: background 0.15s, border-color 0.15s, color 0.15s;
font-family: var(--font-display);
font-weight: 500;
letter-spacing: 0.02em;
}
#new-ws-cancel:hover { background: var(--bg-elevated); border-color: var(--border-strong); }
#new-ws-submit {
background: var(--accent);
color: var(--bg);
border-color: var(--accent);
font-weight: 600;
}
#new-ws-submit:hover { filter: brightness(1.1); }
#new-ws-submit:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
#new-ws-submit:disabled { opacity: 0.4; cursor: not-allowed; filter: none; }
@media (max-width: 380px) { #new-ws-box { padding: 24px 18px; } }
/* ==========================================================================
Responsive
========================================================================== */
@media (max-width: 700px) {
:root { --dash-grid: 68px 110px 1fr 56px 44px; }
.dash-col-model, .dash-cell-model, .dash-col-node, .dash-cell-node { display: none; }
.node-colheaders, .node-row { grid-template-columns: 1fr 40px 40px 40px 60px; }
.node-group-header { grid-template-columns: 1fr 40px 40px 40px 60px; }
.node-group-header .node-group-cell:last-child { display: none; }
.ncol-version, .node-cell-version { display: none; }
.ncol-health, .node-cell-health { display: none; }
#node-mcp-summary { display: none; }
#main { padding: 16px; padding-bottom: 60px; }
}
@media (max-width: 480px) {
:root { --dash-grid: 50px 1fr 50px; }
.dash-col-model, .dash-cell-model, .dash-col-node, .dash-cell-node, .dash-col-task, .dash-cell-task, .dash-col-ctx, .dash-cell-ctx { display: none; }
#cluster-status-bar { height: auto; flex-wrap: wrap; padding: 8px 12px; gap: 6px; }
.csb-states { flex-wrap: wrap; gap: 2px; }
.csb-state { padding: 4px 6px; font-size: 11px; }
.csb-divider { display: none; }
.csb-metrics { width: 100%; justify-content: center; }
#main { padding-bottom: 80px; }
#header h1 { font-size: 13px; }
}
/* ==========================================================================
Admin panel
========================================================================== */
.admin-tabs {
display: flex;
gap: 2px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
padding-bottom: 0;
}
.admin-tab {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--fg-dim);
font-family: var(--font-display);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 8px 16px 10px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.admin-tab:hover { color: var(--fg); }
.admin-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.admin-tab:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
border-radius: var(--radius-sm);
}
.admin-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.admin-action-btn {
margin-left: auto;
background: var(--accent);
color: var(--bg);
border: none;
border-radius: var(--radius-sm);
font-family: var(--font-display);
font-size: 11px;
font-weight: 600;
padding: 6px 14px;
cursor: pointer;
letter-spacing: 0.02em;
transition: filter 0.15s;
}
.admin-action-btn:hover { filter: brightness(1.1); }
.admin-action-btn:focus-visible { outline: 2px solid var(--fg-bright); outline-offset: 2px; }
.admin-action-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.admin-toolbar select {
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
font: inherit;
font-size: 12px;
padding: 5px 30px 5px 10px;
min-width: 180px;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%238a93ad' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
}
.admin-toolbar select:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-dim); }
/* Admin table grid */
.admin-colheaders {
display: grid;
padding: 0 12px;
margin-bottom: 4px;
}
.admin-colheaders .admin-col {
font-family: var(--font-display);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-dim);
}
.admin-row {
display: grid;
padding: 8px 12px;
align-items: center;
border-radius: var(--radius-sm);
transition: background 0.1s;
}
.admin-row:nth-child(even) { background: var(--row-alt, rgba(255,255,255,0.015)); }
.admin-row:hover { background: var(--bg-highlight); }
.admin-col { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.admin-col code { font-family: var(--font-mono); font-size: 11px; color: var(--fg-dim); }
/* Users grid: USERNAME | NAME | CREATED | ACTIONS */
#admin-users .admin-colheaders,
#admin-users .admin-row {
grid-template-columns: 140px 1fr 100px 80px;
}
/* Tokens grid: PREFIX | NAME | SCOPES | CREATED | EXPIRES | ACTIONS */
#admin-tokens .admin-colheaders,
#admin-tokens .admin-row {
grid-template-columns: 100px 1fr 160px 100px 100px 80px;
}
/* Channels grid: CHANNEL | EXTERNAL ID | LINKED | ACTIONS */
#admin-channels .admin-colheaders,
#admin-channels .admin-row {
grid-template-columns: 100px 1fr 100px 80px;
}
/* Scope badges */
.scope-badge {
display: inline-block;
font-family: var(--font-display);
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 1px 6px;
border-radius: 2px;
margin-right: 3px;
background: var(--bg-highlight);
color: var(--fg-dim);
border: 1px solid var(--border);
}
.scope-write { color: var(--cyan); border-color: rgba(103, 232, 249, 0.2); }
.scope-approve { color: var(--accent); border-color: var(--accent-dim); }
.scope-channel { color: var(--magenta); border-color: rgba(192, 132, 252, 0.25); }
/* Action buttons */
.admin-btn-danger {
background: none;
border: 1px solid var(--red);
color: var(--red);
font-family: var(--font-display);
font-size: 10px;
font-weight: 500;
padding: 2px 8px;
border-radius: var(--radius-sm);
cursor: pointer;
opacity: 0.8;
transition: opacity 0.15s, background 0.15s;
}
.admin-btn-danger:hover { opacity: 1; background: rgba(248, 113, 113, 0.1); }
.admin-btn-danger:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.admin-btn-action {
background: none;
border: 1px solid var(--border-strong);
color: var(--fg-dim);
font-family: var(--font-display);
font-size: 10px;
font-weight: 500;
padding: 2px 8px;
border-radius: var(--radius-sm);
cursor: pointer;
opacity: 0.8;
transition: opacity 0.15s, background 0.15s;
margin-right: 4px;
}
.admin-btn-action:hover { opacity: 1; background: rgba(255, 255, 255, 0.05); color: var(--fg); }
.admin-btn-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Schedules grid: NAME | TYPE | SCHEDULE | TARGET | NEXT RUN | STATUS | ACTIONS */
#admin-schedules .admin-colheaders,
#admin-schedules .admin-row {
grid-template-columns: 1.5fr 60px 1.2fr 80px 130px 70px 170px;
}
/* Schedule runs grid: STARTED | NODE | STATUS | ERROR */
.sched-runs-grid { grid-template-columns: 2fr 1fr 1fr 2fr; }
/* Schedule status indicators */
.sched-active { color: var(--green); font-weight: 500; }
.sched-disabled { color: var(--fg-dim); }
.sched-expired { color: var(--accent); }
/* Watches grid: NAME | NODE | COMMAND | INTERVAL | POLL | CONDITION | STATUS | ACTIONS */
#admin-watches .admin-colheaders,
#admin-watches .admin-row {
grid-template-columns: 1.2fr 80px 1.5fr 60px 70px 1fr 70px 70px;
}
/* Watch status indicators */
.watch-active { color: var(--green); font-weight: 500; }
.watch-completed { color: var(--accent); }
/* Wide modal variant for schedule forms */
.admin-modal-wide { width: 480px; }
/* Checkbox labels inside admin modals */
.admin-modal label.admin-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: var(--fg);
cursor: pointer;
margin-top: 14px;
}
.admin-modal label.admin-checkbox input[type="checkbox"] {
width: auto;
margin: 0;
}
/* Admin modals (reuse new-ws-overlay pattern) */
.admin-modal {
background: var(--bg-surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius);
padding: 32px;
width: 380px;
max-width: 90vw;
box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.5), 0 0 80px -20px var(--accent-dim);
position: relative;
}
.admin-modal::before {
content: '';
position: absolute;
top: -1px; left: 20%; right: 20%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
border-radius: 1px;
}
.admin-modal h2 {
font-family: var(--font-display);
font-size: 15px;
font-weight: 700;
color: var(--accent);
margin-bottom: 16px;
letter-spacing: 0.02em;
}
.admin-modal label {
display: block;
font-family: var(--font-display);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-dim);
margin-bottom: 5px;
margin-top: 12px;
}
.admin-modal label:first-of-type { margin-top: 0; }
.admin-modal input:not([type="hidden"]), .admin-modal select, .admin-modal textarea {
width: 100%;
padding: 9px 12px;
background: var(--bg);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
color: var(--fg);
font: inherit;
font-size: 13px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.admin-modal input:focus, .admin-modal select:focus, .admin-modal textarea:focus {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 3px var(--accent-dim);
}
.admin-modal input::placeholder, .admin-modal textarea::placeholder { color: var(--fg-dim); opacity: 0.6; }
.admin-modal textarea { resize: vertical; min-height: 40px; }
.admin-modal [role="alert"] { color: var(--red); font-size: 12px; margin-bottom: 8px; display: none; }
.modal-buttons { display: flex; gap: 10px; margin-top: 20px; }
.modal-cancel {
flex: 1;
padding: 9px;
background: var(--bg-highlight);
color: var(--fg);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
font: inherit;
font-family: var(--font-display);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
.modal-cancel:hover { background: var(--bg-elevated); }
.modal-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.modal-submit {
flex: 1;
padding: 9px;
background: var(--accent);
color: var(--bg);
border: none;
border-radius: var(--radius-sm);
font: inherit;
font-family: var(--font-display);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: filter 0.15s;
}
.modal-submit:hover { filter: brightness(1.1); }
.modal-submit:focus-visible { outline: 2px solid var(--fg-bright); outline-offset: 2px; }
.modal-submit:disabled { opacity: 0.4; cursor: not-allowed; filter: none; }
#create-user-overlay, #create-token-overlay, #token-created-overlay, #create-channel-overlay, #confirm-overlay,
#create-schedule-overlay, #edit-schedule-overlay, #schedule-runs-overlay,
#create-role-overlay, #edit-role-overlay, #user-roles-overlay,
#create-policy-overlay, #edit-policy-overlay,
#create-template-overlay, #edit-template-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 500;
}
/* Token display (show-once) */
.token-created-warning {
font-family: var(--font-display);
font-size: 11px;
color: var(--yellow);
margin-bottom: 12px;
}
.token-display {
font-family: var(--font-mono);
font-size: 11px;
color: var(--fg-bright);
background: var(--bg);
padding: 12px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-strong);
word-break: break-all;
user-select: all;
}
@media (max-width: 700px) {
#admin-users .admin-colheaders, #admin-users .admin-row {
grid-template-columns: 100px 1fr 80px;
}
.admin-col-created { display: none; }
#admin-tokens .admin-colheaders, #admin-tokens .admin-row {
grid-template-columns: 80px 1fr 100px 80px;
}
.admin-col-created, .admin-col-expires { display: none; }
#admin-channels .admin-colheaders, #admin-channels .admin-row {
grid-template-columns: 80px 1fr 80px;
}
#admin-channels .admin-col-created { display: none; }
#admin-schedules .admin-colheaders, #admin-schedules .admin-row {
grid-template-columns: 1fr 60px 80px 130px;
}
.admin-col-sschedule, .admin-col-starget, .admin-col-snext { display: none; }
#admin-watches .admin-colheaders, #admin-watches .admin-row {
grid-template-columns: 1.2fr 80px 70px 70px 70px;
}
.admin-col-wcmd, .admin-col-wcond, .admin-col-winterval { display: none; }
}
/* ==========================================================================
Admin tabs — horizontal scroll for 10+ tabs
========================================================================== */
.admin-tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
flex-wrap: nowrap;
scrollbar-width: thin;
}
/* ==========================================================================
Governance: Roles grid
========================================================================== */
#admin-roles .admin-colheaders,
#admin-roles .admin-row {
grid-template-columns: 160px 1fr 110px;
}
/* ==========================================================================
Governance: Tool Policies grid
========================================================================== */
#admin-policies .admin-colheaders,
#admin-policies .admin-row {
grid-template-columns: 1.2fr 1fr 70px 50px 80px 140px;
}
/* Policy action badges */
.policy-badge {
display: inline-block;
font-family: var(--font-display);
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 8px;
border-radius: 2px;
}
.policy-allow {
color: var(--green);
background: var(--green-glow);
border: 1px solid var(--green-glow);
}
.policy-deny {
color: var(--red);
background: var(--red-glow);
border: 1px solid var(--red-glow);
}
.policy-ask {
color: var(--yellow);
background: var(--yellow-glow);
border: 1px solid var(--yellow-glow);
}
/* ==========================================================================
Governance: Prompt Templates grid
========================================================================== */
#admin-templates .admin-colheaders,
#admin-templates .admin-row {
grid-template-columns: 1.5fr 100px 1fr 140px;
}
/* ==========================================================================
Governance: Audit grid
========================================================================== */
#admin-audit .admin-colheaders,
#admin-audit .admin-row {
grid-template-columns: 80px 80px 1fr 120px 1.5fr;
}
/* Audit action badges */
.audit-badge {
display: inline-block;
font-family: var(--font-display);
font-size: 9px;
font-weight: 600;
text-transform: none;
letter-spacing: 0.02em;
padding: 1px 6px;
border-radius: 2px;
background: var(--bg-highlight);
color: var(--fg-dim);
border: 1px solid var(--border);
}
.audit-danger { color: var(--red); border-color: var(--red-glow); }
.audit-success { color: var(--green); border-color: var(--green-glow); }
/* ==========================================================================
Governance: Usage dashboard
========================================================================== */
.usage-summary {
display: flex;
gap: 24px;
padding: 16px 0 20px;
flex-wrap: wrap;
}
.usage-readout {
display: flex;
flex-direction: column;
gap: 2px;
}
.usage-readout-value {
font-size: 22px;
font-weight: 600;
color: var(--fg-bright);
font-variant-numeric: tabular-nums;
font-family: var(--font-mono);
letter-spacing: -0.02em;
}
.usage-readout-label {
font-size: 10px;
font-family: var(--font-display);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-dim);
}
/* Usage bar chart */
.usage-chart { padding-top: 4px; }
.usage-bar-row {
display: grid;
grid-template-columns: 90px 1fr 60px;
align-items: center;
gap: 10px;
padding: 4px 0;
}
.usage-bar-label {
font-size: 11px;
color: var(--fg-dim);
text-align: right;
font-variant-numeric: tabular-nums;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.usage-bar-track {
height: 16px;
background: var(--bg-highlight);
border-radius: 2px;
overflow: hidden;
}
.usage-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 2px;
min-width: 2px;
transition: width 0.3s ease;
box-shadow: 0 0 6px var(--accent-glow);
}
.usage-bar-value {
font-size: 11px;
color: var(--fg-dim);
font-variant-numeric: tabular-nums;
text-align: right;
}
/* Usage range/group buttons */
.usage-range-group {
display: flex;
gap: 2px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
overflow: hidden;
}
.usage-range-btn, .usage-group-btn {
background: var(--bg);
color: var(--fg-dim);
border: none;
font-family: var(--font-display);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 5px 10px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.usage-range-btn:hover, .usage-group-btn:hover {
background: var(--bg-highlight);
color: var(--fg);
}
.usage-range-btn.active, .usage-group-btn.active {
background: var(--accent-dim);
color: var(--accent);
}
.usage-range-btn:focus-visible, .usage-group-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
/* ==========================================================================
Governance: Permission grid (modal checkboxes)
========================================================================== */
.perm-fieldset {
border: none;
padding: 0;
margin: 12px 0 0;
}
.perm-fieldset legend {
font-family: var(--font-display);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-dim);
padding: 0;
margin-bottom: 4px;
}
.perm-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 16px;
padding: 8px 0;
}
.perm-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--fg);
padding: 3px 0;
cursor: pointer;
text-transform: none;
letter-spacing: normal;
}
.perm-checkbox input[type="checkbox"] {
width: auto;
margin: 0;
accent-color: var(--accent);
}
/* ==========================================================================
Governance: Responsive
========================================================================== */
@media (max-width: 700px) {
#admin-roles .admin-colheaders, #admin-roles .admin-row {
grid-template-columns: 1fr 100px;
}
.admin-col-rperms { display: none; }
#admin-policies .admin-colheaders, #admin-policies .admin-row {
grid-template-columns: 1fr 70px 50px 100px;
}
.admin-col-pstatus, .admin-col-ppriority { display: none; }
#admin-templates .admin-colheaders, #admin-templates .admin-row {
grid-template-columns: 1fr 100px;
}
.admin-col-tmcat, .admin-col-tmvars { display: none; }
#admin-audit .admin-colheaders, #admin-audit .admin-row {
grid-template-columns: 60px 1fr 100px;
}
.admin-col-auser, .admin-col-adetail { display: none; }
.usage-readout-value { font-size: 18px; }
.usage-bar-row { grid-template-columns: 70px 1fr 50px; }
.perm-grid { grid-template-columns: 1fr; }
}
/* ==========================================================================
Reduced motion — console-specific
========================================================================== */
@media (prefers-reduced-motion: reduce) {
.node-group-chevron { transition: none; }
.health-bar-fill { transition: none; }
.csb-state, .node-row, .node-group-header { transition: none; }
.node-link, .dash-cell-node, .pagination button { transition: none; }
.dash-row.has-link::after, .node-group-header::before { transition: none; }
#new-ws-box select, #new-ws-box input, #new-ws-buttons button { transition: none; }
.admin-tab, .admin-row, .admin-btn-danger, .admin-btn-action { transition: none; }
.admin-action-btn, .modal-cancel, .modal-submit { transition: none; }
.admin-modal input, .admin-modal select { transition: none; }
}