fix: 平台构建
This commit is contained in:
@@ -20,6 +20,10 @@ The machine-side run executor lives in the independent repository `git@git.npc0.
|
|||||||
|
|
||||||
Do not place implementation code outside the matching root. Shared contracts must be generated or copied through explicit contract packages, not imported by reaching across ownership boundaries casually.
|
Do not place implementation code outside the matching root. Shared contracts must be generated or copied through explicit contract packages, not imported by reaching across ownership boundaries casually.
|
||||||
|
|
||||||
|
## CSS Authoring Rules
|
||||||
|
|
||||||
|
Any task that touches CSS must keep CSS declarations compressed and compact. Do not expand a single selector's style block across many lines when it can be written as a concise one-line rule.
|
||||||
|
|
||||||
## OpenSpec Rules
|
## OpenSpec Rules
|
||||||
|
|
||||||
- Use judgment before creating an OpenSpec change. Create one when the work changes behavior, architecture, public/API contracts, validation rules, persistence, security boundaries, cross-root workflows, or the required product/visual direction.
|
- Use judgment before creating an OpenSpec change. Create one when the work changes behavior, architecture, public/API contracts, validation rules, persistence, security boundaries, cross-root workflows, or the required product/visual direction.
|
||||||
@@ -35,6 +39,12 @@ When creating a task, include the following prompt boundaries before implementat
|
|||||||
- Directional prompt (方向提示词): state the expected implementation direction, affected project root, relevant existing patterns to preserve, and verification command or evidence expected.
|
- Directional prompt (方向提示词): state the expected implementation direction, affected project root, relevant existing patterns to preserve, and verification command or evidence expected.
|
||||||
- Boundary prompt (任务边界): explicitly list out-of-scope areas, forbidden product expansions, and files or roots that must not be touched unless the task explicitly requires them.
|
- Boundary prompt (任务边界): explicitly list out-of-scope areas, forbidden product expansions, and files or roots that must not be touched unless the task explicitly requires them.
|
||||||
|
|
||||||
|
## Git Completion Rules
|
||||||
|
|
||||||
|
For every substantial session task that modifies repository files, automatically stage only the files changed for that task, create a concise git commit after verification succeeds, and push the current branch to its configured git remote before sending the final response.
|
||||||
|
|
||||||
|
Do not include unrelated pre-existing worktree changes in the commit. If commit or push is blocked by missing credentials, a missing remote, conflicts, or a failing required verification step, report the blocker clearly instead of broadening the commit scope.
|
||||||
|
|
||||||
## Structure Rules
|
## Structure Rules
|
||||||
|
|
||||||
Backend roots must keep these concerns in fixed directories:
|
Backend roots must keep these concerns in fixed directories:
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ Dropdown and contextual action menus must follow these rules:
|
|||||||
The platform_web visual system is a game operations console, not a generic SaaS dashboard. The default theme is black mecha; the selectable alternate theme is magical-girl. Future UI work must preserve the current style contract:
|
The platform_web visual system is a game operations console, not a generic SaaS dashboard. The default theme is black mecha; the selectable alternate theme is magical-girl. Future UI work must preserve the current style contract:
|
||||||
|
|
||||||
- Use the shared theme tokens in `theme/tokens.ts` and shared styles in `theme/base.css`; do not add page-local opaque card systems, one-off dark dashboards, or unrelated visual languages.
|
- Use the shared theme tokens in `theme/tokens.ts` and shared styles in `theme/base.css`; do not add page-local opaque card systems, one-off dark dashboards, or unrelated visual languages.
|
||||||
|
- Repeated console modules, record lists, compact status rows, metric strips, action rows, and access lists should use the shared `console-module`, `console-record-list`, `console-record`, `console-row-list`, `console-row`, `console-stat-strip`, `console-row-actions`, and `access-list` classes. Treat older page-specific operation/user/maintenance class names as compatibility aliases, not new style ownership.
|
||||||
- Keep major surfaces translucent enough for the selected desktop/background to remain visible: side navigation, account/profile controls, metric cards, console panels, server cards, server detail headers, tables, drawers, dialogs, plugin groups, and operation history.
|
- Keep major surfaces translucent enough for the selected desktop/background to remain visible: side navigation, account/profile controls, metric cards, console panels, server cards, server detail headers, tables, drawers, dialogs, plugin groups, and operation history.
|
||||||
- Preserve theme-specific materials: black mecha uses dark cockpit panels, angular clipped frames, cyan scanner lines, and amber energy accents; magical-girl uses pink jelly glass, gold star frames, ribbon glow, and visible magic-circle motifs.
|
- Preserve theme-specific materials: black mecha uses dark cockpit panels, angular clipped frames, cyan scanner lines, and amber energy accents; magical-girl uses pink jelly glass, gold star frames, ribbon glow, and visible magic-circle motifs.
|
||||||
- Theme changes must be isolated and complete. Switching from magical-girl to black mecha must clear previous magical variables and update root theme marker, sidebar subtitle, active navigation frame, swatch strip, and shared surface accessories together.
|
- Theme changes must be isolated and complete. Switching from magical-girl to black mecha must clear previous magical variables and update root theme marker, sidebar subtitle, active navigation frame, swatch strip, and shared surface accessories together.
|
||||||
- Do not double-frame nested content. A `.state-view` inside an already framed shared parent such as a console panel, card, table wrapper, plugin group, or operation item is content only; the parent owns the visible border and accessory layer.
|
- Do not double-frame nested content. A `.state-view` inside an already framed shared parent such as a console panel, card, table wrapper, plugin group, `console-record`, or compatibility operation item is content only; the parent owns the visible border and accessory layer.
|
||||||
- Keep the primary menu as a compact admin sidebar with two states: expanded text menu and collapsed icon rail. Do not reintroduce the single-column / double-column menu toggle. The same route order should render as a restrained dark mecha operations rail in black mecha and as a semi-transparent pink frosted-glass rail with star-framed active states in magical-girl.
|
- Keep the primary menu as a compact admin sidebar with two states: expanded text menu and collapsed icon rail. Do not reintroduce the single-column / double-column menu toggle. The same route order should render as a restrained dark mecha operations rail in black mecha and as a semi-transparent pink frosted-glass rail with star-framed active states in magical-girl.
|
||||||
- Use `components/MagicalParticleLayer.tsx` for full-workspace theme-aware ultimate effects. Each theme should have a distinct low-cost “大招” scene, not a dense field of tiny rotating particles. Do not reintroduce hardcoded fixed decorative DOM/CSS elements such as page-local sparkles, hearts, moons, snowflakes, or sigils.
|
- Use `components/MagicalParticleLayer.tsx` for full-workspace theme-aware ultimate effects. Each theme should have a distinct low-cost “大招” scene, not a dense field of tiny rotating particles. Do not reintroduce hardcoded fixed decorative DOM/CSS elements such as page-local sparkles, hearts, moons, snowflakes, or sigils.
|
||||||
- Built-in backgrounds must stay original CSS/generated motif desktops. Do not bundle recognizable third-party character art. User-uploaded backgrounds are allowed and must render behind readable contrast overlays.
|
- Built-in backgrounds must stay original CSS/generated motif desktops. Do not bundle recognizable third-party character art. User-uploaded backgrounds are allowed and must render behind readable contrast overlays.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function AIConfigDiffReviewPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="console-panel ai-diff-review-panel" aria-label="AI config diff review">
|
<section className="console-panel console-module ai-diff-review-panel" aria-label="AI config diff review">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2><FileCheck2 size={16} /> AI 配置审查</h2>
|
<h2><FileCheck2 size={16} /> AI 配置审查</h2>
|
||||||
<button type="button" className="icon-command" disabled={loading || Boolean(busyId)} onClick={() => void refresh()}><RotateCw size={14} /><span>刷新</span></button>
|
<button type="button" className="icon-command" disabled={loading || Boolean(busyId)} onClick={() => void refresh()}><RotateCw size={14} /><span>刷新</span></button>
|
||||||
@@ -58,15 +58,15 @@ export function AIConfigDiffReviewPanel() {
|
|||||||
{loading && <LoadingState label="正在同步 AI 配置差异…" compact />}
|
{loading && <LoadingState label="正在同步 AI 配置差异…" compact />}
|
||||||
{!loading && error && <ErrorState title="AI 配置审查不可用" reason={error} diagnosticId="ai-config-diffs" onRetry={() => void refresh()} compact />}
|
{!loading && error && <ErrorState title="AI 配置审查不可用" reason={error} diagnosticId="ai-config-diffs" onRetry={() => void refresh()} compact />}
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<div className="operation-list">
|
<div className="console-record-list">
|
||||||
{items.length === 0 && <p className="operations-module-empty">当前没有 AI 配置差异。</p>}
|
{items.length === 0 && <p className="console-empty-note">当前没有 AI 配置差异。</p>}
|
||||||
{items.slice(0, 12).map((item) => (
|
{items.slice(0, 12).map((item) => (
|
||||||
<div key={item.id} className="operation-item">
|
<div key={item.id} className="console-record">
|
||||||
<div className="operation-item-head"><strong>{item.serverInstanceId} · {item.key}</strong><span className={`status-pill status-${item.state === "approved" ? "succeeded" : item.state === "pending" ? "warning" : "disabled"}`}>{item.state}</span></div>
|
<div className="console-record-head"><strong>{item.serverInstanceId} · {item.key}</strong><span className={`status-pill status-${item.state === "approved" ? "succeeded" : item.state === "pending" ? "warning" : "disabled"}`}>{item.state}</span></div>
|
||||||
<div className="operation-meta"><span>请求 {item.requestId}</span><span>版本 {item.configVersion}</span><span>{item.model || "Platform model"}</span><span>到期 {new Date(item.expiresAt).toLocaleString()}</span>{item.jobId && <span>任务 {item.jobId}</span>}</div>
|
<div className="console-record-meta"><span>请求 {item.requestId}</span><span>版本 {item.configVersion}</span><span>{item.model || "Platform model"}</span><span>到期 {new Date(item.expiresAt).toLocaleString()}</span>{item.jobId && <span>任务 {item.jobId}</span>}</div>
|
||||||
<p>{item.diffSummary}</p>
|
<p>{item.diffSummary}</p>
|
||||||
{item.proposedConfig && <pre className="log-view ai-config-proposal">{item.proposedConfig}</pre>}
|
{item.proposedConfig && <pre className="log-view ai-config-proposal">{item.proposedConfig}</pre>}
|
||||||
{item.state === "pending" && <div className="row-actions"><button type="button" disabled={Boolean(busyId)} onClick={() => setSelected(item)}><FileCheck2 size={14} /><span>审查并批准</span></button></div>}
|
{item.state === "pending" && <div className="row-actions console-row-actions"><button type="button" disabled={Boolean(busyId)} onClick={() => setSelected(item)}><FileCheck2 size={14} /><span>审查并批准</span></button></div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -98,15 +98,15 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
|||||||
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => setConfirming(true)}>{busy ? "提交中…" : "执行"}</button>
|
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => setConfirming(true)}>{busy ? "提交中…" : "执行"}</button>
|
||||||
</div>
|
</div>
|
||||||
{installation ? (
|
{installation ? (
|
||||||
<div className="operation-item plugin-lifecycle-state">
|
<div className="console-record plugin-lifecycle-state">
|
||||||
<div className="operation-item-head"><strong>{installation.currentState} → {installation.desiredState}</strong><span className="status-pill status-active">{installation.compatibility || "pending"}</span></div>
|
<div className="console-record-head"><strong>{installation.currentState} → {installation.desiredState}</strong><span className="status-pill status-active">{installation.compatibility || "pending"}</span></div>
|
||||||
<div className="operation-meta">
|
<div className="console-record-meta">
|
||||||
<span>当前 {installation.currentVersion || "--"}</span><span>目标 {installation.targetVersion || "--"}</span><span>依赖 {installation.dependencyState || "unknown"}</span>
|
<span>当前 {installation.currentVersion || "--"}</span><span>目标 {installation.targetVersion || "--"}</span><span>依赖 {installation.dependencyState || "unknown"}</span>
|
||||||
{installation.jobId && <span>任务 {installation.jobId}</span>}{installation.auditEventId && <span>审计 {installation.auditEventId}</span>}{installation.alertId && <span>告警 {installation.alertId}</span>}
|
{installation.jobId && <span>任务 {installation.jobId}</span>}{installation.auditEventId && <span>审计 {installation.auditEventId}</span>}{installation.alertId && <span>告警 {installation.alertId}</span>}
|
||||||
</div>
|
</div>
|
||||||
{installation.failureReason && <p className="operation-error">{installation.failureReason}</p>}
|
{installation.failureReason && <p className="operation-error">{installation.failureReason}</p>}
|
||||||
</div>
|
</div>
|
||||||
) : <p className="operations-module-empty">该服务器尚无插件生命周期记录。</p>}
|
) : <p className="console-empty-note">该服务器尚无插件生命周期记录。</p>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ConfirmDialog open={confirming} title={`确认${lifecycleOperationLabel(operation)}`} description={`插件 ${pluginName},服务器 ${selectedServerId || "--"}${targetVersion ? `,目标版本 ${targetVersion}` : ""}。`} confirmLabel={lifecycleOperationLabel(operation)} danger={disruptiveOperations.includes(operation)} busy={busy} onCancel={() => { if (!busy) setConfirming(false); }} onConfirm={() => void submit()} />
|
<ConfirmDialog open={confirming} title={`确认${lifecycleOperationLabel(operation)}`} description={`插件 ${pluginName},服务器 ${selectedServerId || "--"}${targetVersion ? `,目标版本 ${targetVersion}` : ""}。`} confirmLabel={lifecycleOperationLabel(operation)} danger={disruptiveOperations.includes(operation)} busy={busy} onCancel={() => { if (!busy) setConfirming(false); }} onConfirm={() => void submit()} />
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function ProductionGovernancePanel({ compact = false, title = "容量与
|
|||||||
const visibleEndpoints = compact ? capacity?.endpoints.slice(0, 3) ?? [] : capacity?.endpoints ?? [];
|
const visibleEndpoints = compact ? capacity?.endpoints.slice(0, 3) ?? [] : capacity?.endpoints ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="console-panel production-governance-panel" aria-label="production capacity and alerts">
|
<section className="console-panel console-module production-governance-panel" aria-label="production capacity and alerts">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2><AlertTriangle size={16} /> {title}</h2>
|
<h2><AlertTriangle size={16} /> {title}</h2>
|
||||||
<button type="button" className="icon-command" disabled={loading || Boolean(busyKey)} onClick={() => void refresh()} title="刷新容量与告警">
|
<button type="button" className="icon-command" disabled={loading || Boolean(busyKey)} onClick={() => void refresh()} title="刷新容量与告警">
|
||||||
@@ -82,39 +82,39 @@ export function ProductionGovernancePanel({ compact = false, title = "容量与
|
|||||||
{!loading && error && <ErrorState title="生产治理状态不可用" reason={error} diagnosticId="production-governance" onRetry={() => void refresh()} compact />}
|
{!loading && error && <ErrorState title="生产治理状态不可用" reason={error} diagnosticId="production-governance" onRetry={() => void refresh()} compact />}
|
||||||
{!loading && !error && capacity && (
|
{!loading && !error && capacity && (
|
||||||
<>
|
<>
|
||||||
<dl className="operations-pulse-strip production-capacity-strip">
|
<dl className="console-stat-strip console-stat-strip-spaced">
|
||||||
<div><dt>运行槽位</dt><dd>{capacity.totalRunningJobs}/{capacity.totalMaxJobs}</dd></div>
|
<div><dt>运行槽位</dt><dd>{capacity.totalRunningJobs}/{capacity.totalMaxJobs}</dd></div>
|
||||||
<div><dt>排队</dt><dd>{capacity.totalQueuedJobs}</dd></div>
|
<div><dt>排队</dt><dd>{capacity.totalQueuedJobs}</dd></div>
|
||||||
<div><dt>未关闭告警</dt><dd>{capacity.activeAlerts}</dd></div>
|
<div><dt>未关闭告警</dt><dd>{capacity.activeAlerts}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<div className="operations-endpoint-list" aria-label="capacity endpoints">
|
<div className="console-row-list" aria-label="capacity endpoints">
|
||||||
{visibleEndpoints.map((endpoint) => (
|
{visibleEndpoints.map((endpoint) => (
|
||||||
<div key={endpoint.runEndpointId} className="operations-endpoint-row">
|
<div key={endpoint.runEndpointId} className="console-row">
|
||||||
<span><strong>{endpoint.displayName}</strong><small>{endpoint.pressureCodes?.join(", ") || "capacity.available"}</small></span>
|
<span><strong>{endpoint.displayName}</strong><small>{endpoint.pressureCodes?.join(", ") || "capacity.available"}</small></span>
|
||||||
<span className={cx("status-pill", endpoint.pressureCodes?.length ? "status-warning" : `status-${endpoint.status}`)}>{endpoint.pressureCodes?.length ? "压力" : endpoint.status}</span>
|
<span className={cx("status-pill", endpoint.pressureCodes?.length ? "status-warning" : `status-${endpoint.status}`)}>{endpoint.pressureCodes?.length ? "压力" : endpoint.status}</span>
|
||||||
<span>{endpoint.runningJobs}/{endpoint.maxJobs} · 队列 {endpoint.queuedJobs}</span>
|
<span>{endpoint.runningJobs}/{endpoint.maxJobs} · 队列 {endpoint.queuedJobs}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-list production-alert-list" aria-label="durable alerts">
|
<div className="console-record-list console-record-list-spaced production-alert-list" aria-label="durable alerts">
|
||||||
{visibleAlerts.length === 0 && <p className="operations-module-empty">当前没有持久告警。</p>}
|
{visibleAlerts.length === 0 && <p className="console-empty-note">当前没有持久告警。</p>}
|
||||||
{visibleAlerts.map((alert) => {
|
{visibleAlerts.map((alert) => {
|
||||||
const pending = busyKey.startsWith(`${alert.id}:`);
|
const pending = busyKey.startsWith(`${alert.id}:`);
|
||||||
return (
|
return (
|
||||||
<div key={alert.id} className="operation-item">
|
<div key={alert.id} className="console-record operation-item">
|
||||||
<div className="operation-item-head">
|
<div className="console-record-head">
|
||||||
<strong>{alert.title}</strong>
|
<strong>{alert.title}</strong>
|
||||||
<span className={cx("status-pill", alert.severity === "critical" ? "status-failed" : alert.state === "resolved" ? "status-succeeded" : "status-warning")}>{alert.state}</span>
|
<span className={cx("status-pill", alert.severity === "critical" ? "status-failed" : alert.state === "resolved" ? "status-succeeded" : "status-warning")}>{alert.state}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{alert.message}</p>
|
<p>{alert.message}</p>
|
||||||
<div className="operation-meta">
|
<div className="console-record-meta">
|
||||||
<span>{alert.sourceKind} · {alert.sourceId}</span>
|
<span>{alert.sourceKind} · {alert.sourceId}</span>
|
||||||
<span>发生 {alert.occurrenceCount} 次</span>
|
<span>发生 {alert.occurrenceCount} 次</span>
|
||||||
{alert.lastJobId && <span>任务 {alert.lastJobId}</span>}
|
{alert.lastJobId && <span>任务 {alert.lastJobId}</span>}
|
||||||
{alert.lastAuditEventId && <span>审计 {alert.lastAuditEventId}</span>}
|
{alert.lastAuditEventId && <span>审计 {alert.lastAuditEventId}</span>}
|
||||||
</div>
|
</div>
|
||||||
{alert.state !== "resolved" && (
|
{alert.state !== "resolved" && (
|
||||||
<div className="row-actions production-alert-actions">
|
<div className="row-actions console-row-actions production-alert-actions">
|
||||||
{alert.state === "active" && <button type="button" disabled={pending} onClick={() => setIntent({ alert, action: "acknowledge" })}><Check size={14} /><span>确认</span></button>}
|
{alert.state === "active" && <button type="button" disabled={pending} onClick={() => setIntent({ alert, action: "acknowledge" })}><Check size={14} /><span>确认</span></button>}
|
||||||
<button type="button" disabled={pending} onClick={() => setIntent({ alert, action: "resolve" })}><CheckCheck size={14} /><span>解决</span></button>
|
<button type="button" disabled={pending} onClick={() => setIntent({ alert, action: "resolve" })}><CheckCheck size={14} /><span>解决</span></button>
|
||||||
{alert.retryable && <button type="button" disabled={pending} onClick={() => setIntent({ alert, action: "retry" })}><Activity size={14} /><span>重试源</span></button>}
|
{alert.retryable && <button type="button" disabled={pending} onClick={() => setIntent({ alert, action: "retry" })}><Activity size={14} /><span>重试源</span></button>}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="operations-command-grid" aria-label="平台运营模块">
|
<section className="operations-command-grid" aria-label="平台运营模块">
|
||||||
<article className="console-panel operations-module" aria-label="任务脉冲">
|
<article className="console-panel console-module" aria-label="任务脉冲">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2><Workflow size={16} /> 任务脉冲</h2>
|
<h2><Workflow size={16} /> 任务脉冲</h2>
|
||||||
<span className="page-status">{moduleFreshnessLabel(core.status === "ready" ? core.refreshedAt : core.refreshedAt)}</span>
|
<span className="page-status">{moduleFreshnessLabel(core.status === "ready" ? core.refreshedAt : core.refreshedAt)}</span>
|
||||||
@@ -233,20 +233,20 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
|||||||
{core.status === "error" && <ErrorState title="任务状态不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />}
|
{core.status === "error" && <ErrorState title="任务状态不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />}
|
||||||
{core.status === "ready" && jobs && (
|
{core.status === "ready" && jobs && (
|
||||||
<>
|
<>
|
||||||
<dl className="operations-pulse-strip">
|
<dl className="console-stat-strip">
|
||||||
<div><dt>执行中</dt><dd>{jobs.active.length}</dd></div>
|
<div><dt>执行中</dt><dd>{jobs.active.length}</dd></div>
|
||||||
<div><dt>等待重试</dt><dd>{jobs.retrying.length}</dd></div>
|
<div><dt>等待重试</dt><dd>{jobs.retrying.length}</dd></div>
|
||||||
<div><dt>近期失败</dt><dd>{jobs.failed.length}</dd></div>
|
<div><dt>近期失败</dt><dd>{jobs.failed.length}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{jobs.active.length === 0 && jobs.failed.length === 0 ? (
|
{jobs.active.length === 0 && jobs.failed.length === 0 ? (
|
||||||
<p className="operations-module-empty">当前没有执行中或失败任务。</p>
|
<p className="console-empty-note">当前没有执行中或失败任务。</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="operations-job-list">
|
<div className="console-row-list">
|
||||||
{[...jobs.active.slice(0, 3), ...jobs.failed.slice(0, 2)].map((job) => (
|
{[...jobs.active.slice(0, 3), ...jobs.failed.slice(0, 2)].map((job) => (
|
||||||
<button
|
<button
|
||||||
key={job.id}
|
key={job.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={cx("operations-job-row", job.state === "failed" && "operations-job-row-failed")}
|
className={cx("console-row console-row-button", job.state === "failed" && "console-row-danger")}
|
||||||
onClick={() => onNavigate(job.serverInstanceId ? "serverDetail" : "maintenance", job.serverInstanceId ? { serverId: job.serverInstanceId } : undefined)}
|
onClick={() => onNavigate(job.serverInstanceId ? "serverDetail" : "maintenance", job.serverInstanceId ? { serverId: job.serverInstanceId } : undefined)}
|
||||||
>
|
>
|
||||||
<span><strong>{job.capability}</strong><small>{job.serverInstanceId ? `服务器 ${job.serverInstanceId}` : "平台任务"}</small></span>
|
<span><strong>{job.capability}</strong><small>{job.serverInstanceId ? `服务器 ${job.serverInstanceId}` : "平台任务"}</small></span>
|
||||||
@@ -260,7 +260,7 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
|||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="console-panel operations-module" aria-label="运行节点状态">
|
<article className="console-panel console-module" aria-label="运行节点状态">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2><ServerCog size={16} /> 运行节点</h2>
|
<h2><ServerCog size={16} /> 运行节点</h2>
|
||||||
<button type="button" className="icon-command" onClick={() => onNavigate("maintenance")}><CircleGauge size={14} /> 排障</button>
|
<button type="button" className="icon-command" onClick={() => onNavigate("maintenance")}><CircleGauge size={14} /> 排障</button>
|
||||||
@@ -269,17 +269,17 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
|||||||
{core.status === "error" && <ErrorState title="节点状态不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />}
|
{core.status === "error" && <ErrorState title="节点状态不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />}
|
||||||
{core.status === "ready" && endpointSummary && (
|
{core.status === "ready" && endpointSummary && (
|
||||||
<>
|
<>
|
||||||
<dl className="operations-pulse-strip">
|
<dl className="console-stat-strip">
|
||||||
<div><dt>在线</dt><dd>{endpointSummary.online}</dd></div>
|
<div><dt>在线</dt><dd>{endpointSummary.online}</dd></div>
|
||||||
<div><dt>活跃任务</dt><dd>{endpointSummary.activeJobs}</dd></div>
|
<div><dt>活跃任务</dt><dd>{endpointSummary.activeJobs}</dd></div>
|
||||||
<div><dt>排队</dt><dd>{endpointSummary.queuedJobs}</dd></div>
|
<div><dt>排队</dt><dd>{endpointSummary.queuedJobs}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{core.data.endpoints.length === 0 ? (
|
{core.data.endpoints.length === 0 ? (
|
||||||
<p className="operations-module-empty">Platform 未返回运行节点。</p>
|
<p className="console-empty-note">Platform 未返回运行节点。</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="operations-endpoint-list">
|
<div className="console-row-list">
|
||||||
{core.data.endpoints.slice(0, 4).map((endpoint) => (
|
{core.data.endpoints.slice(0, 4).map((endpoint) => (
|
||||||
<div key={endpoint.id} className="operations-endpoint-row">
|
<div key={endpoint.id} className="console-row">
|
||||||
<span><strong>{endpoint.displayName}</strong><small>{endpoint.version}</small></span>
|
<span><strong>{endpoint.displayName}</strong><small>{endpoint.version}</small></span>
|
||||||
<span className={cx("status-pill", `status-${endpoint.status}`)}>{endpointStatusLabel(endpoint.status)}</span>
|
<span className={cx("status-pill", `status-${endpoint.status}`)}>{endpointStatusLabel(endpoint.status)}</span>
|
||||||
<span>{endpoint.capacity.runningJobs}/{endpoint.capacity.maxJobs} 运行</span>
|
<span>{endpoint.capacity.runningJobs}/{endpoint.capacity.maxJobs} 运行</span>
|
||||||
@@ -311,7 +311,7 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
|||||||
<UsageMeter label="内存" percent={metricAverages.memory} />
|
<UsageMeter label="内存" percent={metricAverages.memory} />
|
||||||
<UsageMeter label="磁盘" percent={metricAverages.disk} />
|
<UsageMeter label="磁盘" percent={metricAverages.disk} />
|
||||||
</div>
|
</div>
|
||||||
<span className="operations-module-freshness">{moduleFreshnessLabel(metricAverages.refreshedAt)}</span>
|
<span className="console-note">{moduleFreshnessLabel(metricAverages.refreshedAt)}</span>
|
||||||
{(usage.status === "error" || metrics.status === "error") && (
|
{(usage.status === "error" || metrics.status === "error") && (
|
||||||
<button type="button" className="operations-inline-warning" onClick={() => { void refreshUsage(); void refreshMetrics(); }}>
|
<button type="button" className="operations-inline-warning" onClick={() => { void refreshUsage(); void refreshMetrics(); }}>
|
||||||
<AlertTriangle size={14} /> 部分指标源不可用,点击重试
|
<AlertTriangle size={14} /> 部分指标源不可用,点击重试
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
|||||||
<span>能力 {endpoint.capabilities.slice(0, 4).join(" / ") || "未上报"}</span>
|
<span>能力 {endpoint.capabilities.slice(0, 4).join(" / ") || "未上报"}</span>
|
||||||
<span>相关服务器 {relatedServers.length}</span>
|
<span>相关服务器 {relatedServers.length}</span>
|
||||||
</details>
|
</details>
|
||||||
<div className="maintenance-actions">
|
<div className="console-row-actions">
|
||||||
<button type="button" className="theme-upload" onClick={() => void refreshEndpoints()}>
|
<button type="button" className="theme-upload" onClick={() => void refreshEndpoints()}>
|
||||||
刷新节点
|
刷新节点
|
||||||
</button>
|
</button>
|
||||||
@@ -220,17 +220,17 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
|||||||
<EmptyState title="暂无失败任务" description="最近任务没有失败记录;如果节点异常,请先查看运行节点心跳和容量。" actionLabel="刷新任务" onAction={() => void refreshJobs()} />
|
<EmptyState title="暂无失败任务" description="最近任务没有失败记录;如果节点异常,请先查看运行节点心跳和容量。" actionLabel="刷新任务" onAction={() => void refreshJobs()} />
|
||||||
)}
|
)}
|
||||||
{jobs.status === "ready" && failedJobs.length > 0 && (
|
{jobs.status === "ready" && failedJobs.length > 0 && (
|
||||||
<div className="operation-list">
|
<div className="console-record-list">
|
||||||
{failedJobs.map((job) => {
|
{failedJobs.map((job) => {
|
||||||
const server = job.serverInstanceId ? serverById.get(job.serverInstanceId) : undefined;
|
const server = job.serverInstanceId ? serverById.get(job.serverInstanceId) : undefined;
|
||||||
const endpoint = endpointById.get(job.runEndpointId);
|
const endpoint = endpointById.get(job.runEndpointId);
|
||||||
return (
|
return (
|
||||||
<div key={job.id} className="operation-item">
|
<div key={job.id} className="console-record">
|
||||||
<div className="operation-item-head">
|
<div className="console-record-head">
|
||||||
<strong>{job.capability}</strong>
|
<strong>{job.capability}</strong>
|
||||||
<span className="status-pill status-error">{jobStateLabel(job.state)}</span>
|
<span className="status-pill status-error">{jobStateLabel(job.state)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-meta">
|
<div className="console-record-meta">
|
||||||
<span>
|
<span>
|
||||||
任务 <code>{job.id}</code>
|
任务 <code>{job.id}</code>
|
||||||
</span>
|
</span>
|
||||||
@@ -242,7 +242,7 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
|||||||
</span>
|
</span>
|
||||||
<span>{formatTimestamp(job.updatedAt)}</span>
|
<span>{formatTimestamp(job.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="maintenance-actions">
|
<div className="console-row-actions">
|
||||||
<button type="button" className="theme-upload" onClick={() => void retryJob(job)}>
|
<button type="button" className="theme-upload" onClick={() => void retryJob(job)}>
|
||||||
<RotateCcw size={13} />
|
<RotateCcw size={13} />
|
||||||
重试
|
重试
|
||||||
@@ -273,14 +273,14 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
|||||||
<EmptyState title="暂无审计事件" description="平台还没有记录任何审计事件。" actionLabel="刷新" onAction={() => void refreshEvents()} />
|
<EmptyState title="暂无审计事件" description="平台还没有记录任何审计事件。" actionLabel="刷新" onAction={() => void refreshEvents()} />
|
||||||
)}
|
)}
|
||||||
{events.status === "ready" && events.data.length > 0 && (
|
{events.status === "ready" && events.data.length > 0 && (
|
||||||
<div className="operation-list">
|
<div className="console-record-list">
|
||||||
{events.data.slice(0, 30).map((event) => (
|
{events.data.slice(0, 30).map((event) => (
|
||||||
<div key={event.id} className="operation-item">
|
<div key={event.id} className="console-record">
|
||||||
<div className="operation-item-head">
|
<div className="console-record-head">
|
||||||
<strong>{event.summary || `${event.action} ${event.resourceKind}`}</strong>
|
<strong>{event.summary || `${event.action} ${event.resourceKind}`}</strong>
|
||||||
<span className={cx("status-pill", auditResultClass(event))}>{event.result}</span>
|
<span className={cx("status-pill", auditResultClass(event))}>{event.result}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-meta">
|
<div className="console-record-meta">
|
||||||
<span>
|
<span>
|
||||||
事件 <code>{event.id}</code>
|
事件 <code>{event.id}</code>
|
||||||
</span>
|
</span>
|
||||||
@@ -290,7 +290,7 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
|||||||
</span>
|
</span>
|
||||||
<span>{formatTimestamp(event.createdAt)}</span>
|
<span>{formatTimestamp(event.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="maintenance-actions">
|
<div className="console-row-actions">
|
||||||
<button type="button" className="theme-upload" onClick={() => void refreshEvents()}>
|
<button type="button" className="theme-upload" onClick={() => void refreshEvents()}>
|
||||||
查看审计链路
|
查看审计链路
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1783,7 +1783,7 @@ function PluginControlsSection({ serverId, instance, plugins, artifacts, session
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="operation-list" aria-label="plugin controls">
|
<div className="console-record-list" aria-label="plugin controls">
|
||||||
{groups.length === 0 && (
|
{groups.length === 0 && (
|
||||||
<EmptyState title="该服务器没有可控制的插件" description="安装插件后,这里会按插件分组显示可用的日常操作。" />
|
<EmptyState title="该服务器没有可控制的插件" description="安装插件后,这里会按插件分组显示可用的日常操作。" />
|
||||||
)}
|
)}
|
||||||
@@ -2206,17 +2206,17 @@ function HistorySection({ serverId, serverOperations, jobs, artifacts, metricHis
|
|||||||
{serverOperations.length === 0 ? (
|
{serverOperations.length === 0 ? (
|
||||||
<EmptyState title="暂无操作记录" description="在本页发起启动、停止、插件控制或配置写入后,这里会显示完整的操作生命周期。" />
|
<EmptyState title="暂无操作记录" description="在本页发起启动、停止、插件控制或配置写入后,这里会显示完整的操作生命周期。" />
|
||||||
) : (
|
) : (
|
||||||
<div className="operation-list">
|
<div className="console-record-list">
|
||||||
{serverOperations.map((operation) => (
|
{serverOperations.map((operation) => (
|
||||||
<div key={operation.id} className="operation-item">
|
<div key={operation.id} className="console-record">
|
||||||
<div className="operation-item-head">
|
<div className="console-record-head">
|
||||||
<strong>{operation.intent}</strong>
|
<strong>{operation.intent}</strong>
|
||||||
<ResultBadge
|
<ResultBadge
|
||||||
status={operation.status}
|
status={operation.status}
|
||||||
label={operation.status === "pending" ? "进行中" : operation.status === "succeeded" ? "成功" : "失败"}
|
label={operation.status === "pending" ? "进行中" : operation.status === "succeeded" ? "成功" : "失败"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-meta">
|
<div className="console-record-meta">
|
||||||
<span>
|
<span>
|
||||||
操作 <code>{operation.id}</code>
|
操作 <code>{operation.id}</code>
|
||||||
</span>
|
</span>
|
||||||
@@ -2249,16 +2249,16 @@ function HistorySection({ serverId, serverOperations, jobs, artifacts, metricHis
|
|||||||
{jobs.length === 0 ? (
|
{jobs.length === 0 ? (
|
||||||
<EmptyState title="暂无任务" description="该服务器还没有平台侧任务记录。" />
|
<EmptyState title="暂无任务" description="该服务器还没有平台侧任务记录。" />
|
||||||
) : (
|
) : (
|
||||||
<div className="operation-list">
|
<div className="console-record-list">
|
||||||
{jobs.slice(0, 20).map((job) => (
|
{jobs.slice(0, 20).map((job) => (
|
||||||
<div key={job.id} className="operation-item">
|
<div key={job.id} className="console-record">
|
||||||
<div className="operation-item-head">
|
<div className="console-record-head">
|
||||||
<strong>{job.capability}</strong>
|
<strong>{job.capability}</strong>
|
||||||
<span className={cx("status-pill", job.state === "succeeded" ? "status-active" : job.state === "failed" ? "status-error" : "status-disabled")}>
|
<span className={cx("status-pill", job.state === "succeeded" ? "status-active" : job.state === "failed" ? "status-error" : "status-disabled")}>
|
||||||
{job.state}
|
{job.state}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-meta">
|
<div className="console-record-meta">
|
||||||
<span>
|
<span>
|
||||||
任务 <code>{job.id}</code>
|
任务 <code>{job.id}</code>
|
||||||
</span>
|
</span>
|
||||||
@@ -2291,18 +2291,18 @@ function HistorySection({ serverId, serverOperations, jobs, artifacts, metricHis
|
|||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2>持久化观测</h2>
|
<h2>持久化观测</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-list">
|
<div className="console-record-list">
|
||||||
<div className="operation-item">
|
<div className="console-record">
|
||||||
<div className="operation-item-head"><strong>指标样本</strong><span className="status-pill status-active">{metricHistory.length} 条</span></div>
|
<div className="console-record-head"><strong>指标样本</strong><span className="status-pill status-active">{metricHistory.length} 条</span></div>
|
||||||
<div className="operation-meta"><span>最新采集 {metricHistory.length > 0 ? new Date(metricHistory[metricHistory.length - 1].collectedAt).toLocaleString() : "暂无"}</span></div>
|
<div className="console-record-meta"><span>最新采集 {metricHistory.length > 0 ? new Date(metricHistory[metricHistory.length - 1].collectedAt).toLocaleString() : "暂无"}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-item">
|
<div className="console-record">
|
||||||
<div className="operation-item-head"><strong>备份记录</strong><span className="status-pill status-active">{backups.length} 条</span></div>
|
<div className="console-record-head"><strong>备份记录</strong><span className="status-pill status-active">{backups.length} 条</span></div>
|
||||||
<div className="operation-meta">{backups.slice(0, 4).map((backup) => <span key={backup.id}>{backup.id} · {backup.state} · {backup.checksum.slice(0, 18)}</span>)}</div>
|
<div className="console-record-meta">{backups.slice(0, 4).map((backup) => <span key={backup.id}>{backup.id} · {backup.state} · {backup.checksum.slice(0, 18)}</span>)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-item">
|
<div className="console-record">
|
||||||
<div className="operation-item-head"><strong>远端适配器声明</strong><span className="status-pill status-active">{remoteAdapters.length} 个</span></div>
|
<div className="console-record-head"><strong>远端适配器声明</strong><span className="status-pill status-active">{remoteAdapters.length} 个</span></div>
|
||||||
<div className="operation-meta">{remoteAdapters.slice(0, 4).map((adapter) => <span key={adapter.key}>{adapter.key} · {adapter.kind} · {adapter.targetKeys.join(", ")}</span>)}</div>
|
<div className="console-record-meta">{remoteAdapters.slice(0, 4).map((adapter) => <span key={adapter.key}>{adapter.key} · {adapter.kind} · {adapter.targetKeys.join(", ")}</span>)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -2347,16 +2347,16 @@ function ArtifactDownloadPanel({ serverId, artifacts }: ArtifactDownloadPanelPro
|
|||||||
{artifacts.length === 0 ? (
|
{artifacts.length === 0 ? (
|
||||||
<EmptyState title="暂无可下载制品" description="该服务器当前没有已完成的可用制品。" />
|
<EmptyState title="暂无可下载制品" description="该服务器当前没有已完成的可用制品。" />
|
||||||
) : (
|
) : (
|
||||||
<div className="operation-list">
|
<div className="console-record-list">
|
||||||
{artifacts.slice(0, 12).map((artifact) => {
|
{artifacts.slice(0, 12).map((artifact) => {
|
||||||
const itemResult = result[artifact.id];
|
const itemResult = result[artifact.id];
|
||||||
return (
|
return (
|
||||||
<div key={artifact.id} className="operation-item">
|
<div key={artifact.id} className="console-record">
|
||||||
<div className="operation-item-head">
|
<div className="console-record-head">
|
||||||
<strong>{artifact.id}</strong>
|
<strong>{artifact.id}</strong>
|
||||||
<ResultBadge status={itemResult?.status ?? "pending"} label={itemResult?.label ?? artifact.state} />
|
<ResultBadge status={itemResult?.status ?? "pending"} label={itemResult?.label ?? artifact.state} />
|
||||||
</div>
|
</div>
|
||||||
<div className="operation-meta">
|
<div className="console-record-meta">
|
||||||
<span>服务器 {serverId}</span>
|
<span>服务器 {serverId}</span>
|
||||||
<span>{formatBytes(artifact.sizeBytes)}</span>
|
<span>{formatBytes(artifact.sizeBytes)}</span>
|
||||||
<span>{artifact.checksum}</span>
|
<span>{artifact.checksum}</span>
|
||||||
|
|||||||
@@ -301,9 +301,9 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
|
|||||||
) : !loading && !loadError && filteredUsers.length === 0 ? (
|
) : !loading && !loadError && filteredUsers.length === 0 ? (
|
||||||
<EmptyState title="没有匹配用户" description="调整名称、角色或状态筛选后再试。" actionLabel="清除筛选" onAction={() => { setKeyword(""); setRoleFilter(""); setStatusFilter("all"); }} />
|
<EmptyState title="没有匹配用户" description="调整名称、角色或状态筛选后再试。" actionLabel="清除筛选" onAction={() => { setKeyword(""); setRoleFilter(""); setStatusFilter("all"); }} />
|
||||||
) : (
|
) : (
|
||||||
<div className="resource-list user-management-list">
|
<div className="resource-list access-list">
|
||||||
{filteredUsers.map((user) => (
|
{filteredUsers.map((user) => (
|
||||||
<article key={user.id} className="resource-list-item user-management-item">
|
<article key={user.id} className="resource-list-item access-list-row">
|
||||||
<div>
|
<div>
|
||||||
<strong>{user.displayName}</strong>
|
<strong>{user.displayName}</strong>
|
||||||
<span className="provider-id">{user.email ?? user.id}</span>
|
<span className="provider-id">{user.email ?? user.id}</span>
|
||||||
@@ -314,7 +314,7 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
|
|||||||
</span>
|
</span>
|
||||||
<span>{user.roles.map(roleLabel).join(" / ")}</span>
|
<span>{user.roles.map(roleLabel).join(" / ")}</span>
|
||||||
<span>{profileSummary(user)}</span>
|
<span>{profileSummary(user)}</span>
|
||||||
<div className="user-actions" aria-label={`${user.displayName} 状态操作`}>
|
<div className="console-row-actions" aria-label={`${user.displayName} 状态操作`}>
|
||||||
<button type="button" className="theme-upload" disabled={mutationDisabled || operations.isPending(user.id, "编辑用户")} aria-label={`编辑 ${user.displayName}`} onClick={() => startEdit(user)}>
|
<button type="button" className="theme-upload" disabled={mutationDisabled || operations.isPending(user.id, "编辑用户")} aria-label={`编辑 ${user.displayName}`} onClick={() => startEdit(user)}>
|
||||||
<UserPen size={13} />
|
<UserPen size={13} />
|
||||||
编辑
|
编辑
|
||||||
|
|||||||
@@ -39,11 +39,19 @@ This directory owns the platform_web visual system. Keep the console in a unifie
|
|||||||
- `data-theme-palette` on the document root is the source of truth for theme-specific CSS selectors. Do not infer the active theme from route state, component-local state, or background preset IDs.
|
- `data-theme-palette` on the document root is the source of truth for theme-specific CSS selectors. Do not infer the active theme from route state, component-local state, or background preset IDs.
|
||||||
- Theme changes must be visually atomic: root marker, CSS variables, menu/sidebar chrome, active navigation frame, and shared surface accessories should all reflect the same selected palette immediately.
|
- Theme changes must be visually atomic: root marker, CSS variables, menu/sidebar chrome, active navigation frame, and shared surface accessories should all reflect the same selected palette immediately.
|
||||||
|
|
||||||
|
## Typography Rules
|
||||||
|
|
||||||
|
- Theme-level font differences must come from palette variables: `--font-ui`, `--font-display`, `--font-mono`, `--title-weight`, `--title-tracking`, `--heading-weight`, and `--heading-tracking`.
|
||||||
|
- Page and component CSS should not hardcode independent font stacks for headings, code, logs, diffs, or textareas. Use the shared font variables so black mecha and magical-girl can diverge without page-local overrides.
|
||||||
|
- Display headings should use `--font-display`; normal UI copy should inherit `--font-ui`; code-like surfaces such as logs and diffs should use `--font-mono`.
|
||||||
|
|
||||||
## Surface Rules
|
## Surface Rules
|
||||||
|
|
||||||
- Use shared classes such as `metric-card`, `overview-card`, `console-panel`, `catalog-card`, `server-card`, `server-detail-header`, `resource-table-wrap`, `provider-table-wrap`, `drawer-panel`, `confirm-panel`, `plugin-group`, and `operation-item` instead of creating page-local card styles.
|
- Use shared outer surface classes such as `metric-card`, `overview-card`, `console-panel`, `catalog-card`, `server-card`, `server-detail-header`, `resource-table-wrap`, `provider-table-wrap`, `drawer-panel`, `confirm-panel`, and `plugin-group` instead of creating page-local card styles.
|
||||||
|
- Repeated console content must reuse the internal shared classes: `console-module` for panel modules, `console-stat-strip` for compact metric strips, `console-row-list` and `console-row` for compact status rows, `console-record-list` and `console-record` for operation/history records, `console-row-actions` for action rows, and `access-list` / `access-list-row` for access-management rows.
|
||||||
|
- Legacy page-specific classes such as `operations-module`, `operations-pulse-strip`, `operations-endpoint-row`, `operation-list`, `operation-item`, `maintenance-actions`, `user-actions`, and `user-management-item` are compatibility aliases only. New UI should not make these classes the style owner; pair or replace them with the shared `console-*` / `access-*` classes.
|
||||||
- Shared framed surfaces should use `var(--panel-material)`, `var(--panel-shadow)`, `var(--frame-corner)`, and `var(--frame-accent)` so each theme can change structure, fill, and glow style beyond simple color swaps.
|
- Shared framed surfaces should use `var(--panel-material)`, `var(--panel-shadow)`, `var(--frame-corner)`, and `var(--frame-accent)` so each theme can change structure, fill, and glow style beyond simple color swaps.
|
||||||
- A visual region should have only one ornamental frame at a hierarchy level. If a `.state-view` is nested inside a shared framed parent such as `.console-panel`, `.catalog-card`, `.server-card`, `.resource-table-wrap`, `.provider-table-wrap`, `.server-table-wrap`, `.plugin-group`, or `.operation-item`, the parent owns the frame and the nested state view must render as transparent, borderless content with no `::before` or `::after` accessory.
|
- A visual region should have only one ornamental frame at a hierarchy level. If a `.state-view` is nested inside a shared framed parent such as `.console-panel`, `.catalog-card`, `.server-card`, `.resource-table-wrap`, `.provider-table-wrap`, `.server-table-wrap`, `.plugin-group`, `.console-record`, or compatibility `.operation-item`, the parent owns the frame and the nested state view must render as transparent, borderless content with no `::before` or `::after` accessory.
|
||||||
- Standalone `.state-view` instances may keep their own readable state treatment when they are not inside an already framed surface.
|
- Standalone `.state-view` instances may keep their own readable state treatment when they are not inside an already framed surface.
|
||||||
- Menu frames should use `var(--menu-item-bg)`, `var(--menu-item-active-bg)`, `var(--menu-glyph-bg)`, `var(--menu-title-shadow)`, and `var(--menu-active-outline)` so each theme changes active-state treatment, icon material, and rail/sidebar structure.
|
- Menu frames should use `var(--menu-item-bg)`, `var(--menu-item-active-bg)`, `var(--menu-glyph-bg)`, `var(--menu-title-shadow)`, and `var(--menu-active-outline)` so each theme changes active-state treatment, icon material, and rail/sidebar structure.
|
||||||
- Action dropdowns and contextual menus are small operational overlays, not decorative panels. They must stay anchored to the trigger, fit within the viewport, use compact rows, and avoid moving or resizing the parent card, row, grid, or table.
|
- Action dropdowns and contextual menus are small operational overlays, not decorative panels. They must stay anchored to the trigger, fit within the viewport, use compact rows, and avoid moving or resizing the parent card, row, grid, or table.
|
||||||
@@ -58,7 +66,8 @@ This directory owns the platform_web visual system. Keep the console in a unifie
|
|||||||
## Adding UI
|
## Adding UI
|
||||||
|
|
||||||
1. Reuse existing shared surface, command, table, form, status, and state-view classes first.
|
1. Reuse existing shared surface, command, table, form, status, and state-view classes first.
|
||||||
2. If a new shared pattern is truly needed, add it in `base.css` and describe its intended use here.
|
2. For new panel-internal records, rows, stat strips, and action rows, start from `console-record`, `console-row`, `console-stat-strip`, and `console-row-actions` before adding a narrowly scoped modifier class.
|
||||||
3. When placing empty/loading/error states inside an existing shared panel, verify the state view does not introduce a second framed panel or accessory layer.
|
3. If a new shared pattern is truly needed, add it in `base.css` and describe its intended use here.
|
||||||
4. If a new palette or background preset is added, update `tokens.ts`, `tokens.test.ts`, and any CSS contract tests together.
|
4. When placing empty/loading/error states inside an existing shared panel, verify the state view does not introduce a second framed panel or accessory layer.
|
||||||
5. Run the relevant focused checks before claiming completion. Use `npm run typecheck`, `npm test`, `npm run build`, and `scripts/check-structure.sh` when the scope warrants them; run `openspec validate <change> --strict` only when an OpenSpec change was created.
|
5. If a new palette or background preset is added, update `tokens.ts`, `tokens.test.ts`, and any CSS contract tests together.
|
||||||
|
6. Run the relevant focused checks before claiming completion. Use `npm run typecheck`, `npm test`, `npm run build`, and `scripts/check-structure.sh` when the scope warrants them; run `openspec validate <change> --strict` only when an OpenSpec change was created.
|
||||||
|
|||||||
+78
-79
File diff suppressed because one or more lines are too long
@@ -174,6 +174,13 @@ export const themePalettes: ThemePalette[] = [
|
|||||||
"--ink": "#e7f7ff",
|
"--ink": "#e7f7ff",
|
||||||
"--ink-soft": "#b9d2dc",
|
"--ink-soft": "#b9d2dc",
|
||||||
"--ink-faint": "#84a1ad",
|
"--ink-faint": "#84a1ad",
|
||||||
|
"--font-ui": "Inter, \"PingFang SC\", \"Microsoft YaHei\", ui-sans-serif, system-ui, sans-serif",
|
||||||
|
"--font-display": "\"Bahnschrift\", \"DIN Alternate\", \"PingFang SC\", \"Microsoft YaHei\", ui-sans-serif, system-ui, sans-serif",
|
||||||
|
"--font-mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
||||||
|
"--title-weight": "760",
|
||||||
|
"--title-tracking": "0.01em",
|
||||||
|
"--heading-weight": "780",
|
||||||
|
"--heading-tracking": "0.005em",
|
||||||
...mechaMaterialVariables,
|
...mechaMaterialVariables,
|
||||||
"--line": "rgba(116, 236, 255, 0.32)",
|
"--line": "rgba(116, 236, 255, 0.32)",
|
||||||
"--line-strong": "rgba(132, 243, 255, 0.7)",
|
"--line-strong": "rgba(132, 243, 255, 0.7)",
|
||||||
@@ -212,6 +219,13 @@ export const themePalettes: ThemePalette[] = [
|
|||||||
"--ink": "#fff8fd",
|
"--ink": "#fff8fd",
|
||||||
"--ink-soft": "#ffe8f5",
|
"--ink-soft": "#ffe8f5",
|
||||||
"--ink-faint": "#ffd0e9",
|
"--ink-faint": "#ffd0e9",
|
||||||
|
"--font-ui": "Inter, \"PingFang SC\", \"Microsoft YaHei\", ui-rounded, ui-sans-serif, system-ui, sans-serif",
|
||||||
|
"--font-display": "\"Trebuchet MS\", \"PingFang SC\", \"Microsoft YaHei\", ui-rounded, ui-sans-serif, system-ui, sans-serif",
|
||||||
|
"--font-mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
||||||
|
"--title-weight": "720",
|
||||||
|
"--title-tracking": "0",
|
||||||
|
"--heading-weight": "740",
|
||||||
|
"--heading-tracking": "0",
|
||||||
...magicalMaterialVariables,
|
...magicalMaterialVariables,
|
||||||
"--line": "rgba(255, 201, 230, 0.42)",
|
"--line": "rgba(255, 201, 230, 0.42)",
|
||||||
"--line-strong": "rgba(255, 225, 139, 0.76)",
|
"--line-strong": "rgba(255, 225, 139, 0.76)",
|
||||||
|
|||||||
Reference in New Issue
Block a user