Stabilize competitiveness dashboard
All checks were successful
deploy / deploy (push) Successful in 1m3s
All checks were successful
deploy / deploy (push) Successful in 1m3s
Proof: Competitiveness now renders from a timed display snapshot with fixed row slots, a pause/resume control, and full-width stacked cards and tables; static UI coverage, full npm test, and the operator dashboard build validate the cleanup. Assumptions: this is a UI-only operator cleanup for the active maker timing and competitiveness turn; it does not change strategy decisions, execution, DB config, persistence, live funds, or response policy. Still fake: venue-native terminal fill ids and fee-complete realized PnL remain unavailable.
This commit is contained in:
parent
686b922342
commit
49187379ef
3 changed files with 231 additions and 82 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import EmptyState from '../components/EmptyState.jsx';
|
import EmptyState from '../components/EmptyState.jsx';
|
||||||
import MetricCard from '../components/MetricCard.jsx';
|
import MetricCard from '../components/MetricCard.jsx';
|
||||||
|
|
@ -8,6 +8,10 @@ import { formatAgeFromTimestamp, formatBoolean, formatEur, formatTimestamp, trun
|
||||||
|
|
||||||
const RESPONDED_STATES = new Set(['submitted', 'awaiting_outcome', 'not_filled', 'completed']);
|
const RESPONDED_STATES = new Set(['submitted', 'awaiting_outcome', 'not_filled', 'completed']);
|
||||||
const TRADING_PAIR_MODES = new Set(['maker', 'taker', 'both']);
|
const TRADING_PAIR_MODES = new Set(['maker', 'taker', 'both']);
|
||||||
|
const COMPETITIVENESS_REFRESH_MS = 5_000;
|
||||||
|
const COMPETITIVENESS_GROUP_ROW_COUNT = 12;
|
||||||
|
const COMPETITIVENESS_DETAIL_ROW_COUNT = 8;
|
||||||
|
const COMPETITIVENESS_LATENCY_ROW_COUNT = 6;
|
||||||
|
|
||||||
async function copyIdentifier(value) {
|
async function copyIdentifier(value) {
|
||||||
if (!value || !navigator?.clipboard?.writeText) return;
|
if (!value || !navigator?.clipboard?.writeText) return;
|
||||||
|
|
@ -270,12 +274,52 @@ function pairDisplayLabel(pairId, pairConfig) {
|
||||||
return `${pair.asset_in_symbol || pair.asset_in || pair.assetIn} -> ${pair.asset_out_symbol || pair.asset_out || pair.assetOut}`;
|
return `${pair.asset_in_symbol || pair.asset_in || pair.assetIn} -> ${pair.asset_out_symbol || pair.asset_out || pair.assetOut}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fixedRows(items, count) {
|
||||||
|
const rows = (items || []).slice(0, count);
|
||||||
|
while (rows.length < count) rows.push(null);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
function MakerCompetitivenessSection({ summary, pairConfig }) {
|
function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
const total = summary?.total || {};
|
const latestSummaryRef = useRef(summary || {});
|
||||||
const groups = summary?.groups || [];
|
const [displaySummary, setDisplaySummary] = useState(() => summary || {});
|
||||||
const ageBuckets = summary?.age_buckets || [];
|
const [displayUpdatedAt, setDisplayUpdatedAt] = useState(() => new Date().toISOString());
|
||||||
const latestErrors = summary?.latest_errors || [];
|
const [updatesPaused, setUpdatesPaused] = useState(false);
|
||||||
const policySkips = summary?.policy_skips || [];
|
const total = displaySummary?.total || {};
|
||||||
|
const groups = displaySummary?.groups || [];
|
||||||
|
const ageBuckets = displaySummary?.age_buckets || [];
|
||||||
|
const latestErrors = displaySummary?.latest_errors || [];
|
||||||
|
const policySkips = displaySummary?.policy_skips || [];
|
||||||
|
const latencyStages = displaySummary?.latency_stages || [];
|
||||||
|
const groupRows = fixedRows(groups, COMPETITIVENESS_GROUP_ROW_COUNT);
|
||||||
|
const ageBucketRows = fixedRows(ageBuckets, COMPETITIVENESS_GROUP_ROW_COUNT);
|
||||||
|
const latestErrorRows = fixedRows(latestErrors, COMPETITIVENESS_DETAIL_ROW_COUNT);
|
||||||
|
const policySkipRows = fixedRows(policySkips, COMPETITIVENESS_DETAIL_ROW_COUNT);
|
||||||
|
const latencyStageRows = fixedRows(latencyStages, COMPETITIVENESS_LATENCY_ROW_COUNT);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestSummaryRef.current = summary || {};
|
||||||
|
}, [summary]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updatesPaused) return undefined;
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setDisplaySummary(latestSummaryRef.current || {});
|
||||||
|
setDisplayUpdatedAt(new Date().toISOString());
|
||||||
|
}, COMPETITIVENESS_REFRESH_MS);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [updatesPaused]);
|
||||||
|
|
||||||
|
function toggleUpdatesPaused() {
|
||||||
|
setUpdatesPaused((current) => {
|
||||||
|
const nextPaused = !current;
|
||||||
|
if (current) {
|
||||||
|
setDisplaySummary(latestSummaryRef.current || {});
|
||||||
|
setDisplayUpdatedAt(new Date().toISOString());
|
||||||
|
}
|
||||||
|
return nextPaused;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
|
|
@ -290,17 +334,29 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
<div className="pills">
|
<div className="pills">
|
||||||
<Pill label={`${total.count || 0} rows`} stateLabel="info" />
|
<Pill label={`${total.count || 0} rows`} stateLabel="info" />
|
||||||
<Pill label={`${total.quote_not_found_or_finished_count || 0} stale/finished`} stateLabel={(total.quote_not_found_or_finished_count || 0) > 0 ? 'warning' : 'unknown'} />
|
<Pill label={`${total.quote_not_found_or_finished_count || 0} stale/finished`} stateLabel={(total.quote_not_found_or_finished_count || 0) > 0 ? 'warning' : 'unknown'} />
|
||||||
|
<Pill label={updatesPaused ? 'Updates paused' : 'Timed snapshot'} stateLabel={updatesPaused ? 'warning' : 'healthy'} />
|
||||||
|
<button
|
||||||
|
aria-pressed={updatesPaused}
|
||||||
|
className="button secondary trace-copy-button"
|
||||||
|
onClick={toggleUpdatesPaused}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{updatesPaused ? 'Resume updates' : 'Pause updates'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="metric-grid">
|
<div className="status-subtle competitiveness-snapshot-note">
|
||||||
|
Display snapshot {formatTimestamp(displayUpdatedAt)}. Live updates are applied every {COMPETITIVENESS_REFRESH_MS / 1000}s to keep table height stable.
|
||||||
|
</div>
|
||||||
|
<div className="metric-stack">
|
||||||
<MetricCard label="Accepted" meta={formatRate(total.accepted_rate)} value={String(total.accepted_count || 0)} />
|
<MetricCard label="Accepted" meta={formatRate(total.accepted_rate)} value={String(total.accepted_count || 0)} />
|
||||||
<MetricCard label="Relay failed" meta="Executor reached result" value={String(total.relay_failed_count || 0)} />
|
<MetricCard label="Relay failed" meta="Executor reached result" value={String(total.relay_failed_count || 0)} />
|
||||||
<MetricCard label="Already finished" meta={formatRate(total.stale_or_finished_rate)} value={String(total.quote_not_found_or_finished_count || 0)} />
|
<MetricCard label="Already finished" meta={formatRate(total.stale_or_finished_rate)} value={String(total.quote_not_found_or_finished_count || 0)} />
|
||||||
<MetricCard label="Policy skips" meta="No relay submission" value={String(total.policy_skip_count || 0)} />
|
<MetricCard label="Policy skips" meta="No relay submission" value={String(total.policy_skip_count || 0)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TableFrame>
|
<TableFrame className="competitiveness-frame">
|
||||||
<table>
|
<table className="competitiveness-table competitiveness-groups-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Pair</th>
|
<th>Pair</th>
|
||||||
|
|
@ -315,10 +371,17 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{groups.length ? groups.slice(0, 12).map((group, index) => {
|
{groupRows.map((group, index) => {
|
||||||
|
if (!group) {
|
||||||
|
return (
|
||||||
|
<tr className="competitiveness-placeholder-row" key={`group-placeholder:${index}`}>
|
||||||
|
<td colSpan={9}>{index === 0 && !groups.length ? 'No competitiveness rows are available yet.' : ''}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
const quoteToRelay = group.latency_stages?.find((stage) => stage.stage === 'quote_to_relay_result_ms');
|
const quoteToRelay = group.latency_stages?.find((stage) => stage.stage === 'quote_to_relay_result_ms');
|
||||||
return (
|
return (
|
||||||
<tr key={`${group.pair}:${group.direction}:${group.request_kind}:${group.edge_bps ?? 'edge'}:${group.result_code}:${group.quote_age_bucket}:${index}`}>
|
<tr key={`${group.pair}:${group.direction}:${group.request_kind}:${group.edge_bps ?? 'edge'}:${group.result_code}:${group.quote_age_bucket}:${group.notional_bucket}:${group.outcome_status}`}>
|
||||||
<td>
|
<td>
|
||||||
<div>{pairDisplayLabel(group.pair, pairConfig)}</div>
|
<div>{pairDisplayLabel(group.pair, pairConfig)}</div>
|
||||||
<div className="status-subtle mono">{truncateMiddle(group.pair || '', 42)}</div>
|
<div className="status-subtle mono">{truncateMiddle(group.pair || '', 42)}</div>
|
||||||
|
|
@ -345,16 +408,14 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}) : (
|
})}
|
||||||
<tr><td colSpan={9}>No competitiveness rows are available yet.</td></tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</TableFrame>
|
</TableFrame>
|
||||||
|
|
||||||
<div className="two-column-grid">
|
<div className="stack-grid competitiveness-table-stack">
|
||||||
<TableFrame>
|
<TableFrame className="competitiveness-frame">
|
||||||
<table>
|
<table className="competitiveness-table competitiveness-latency-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Latency stage</th>
|
<th>Latency stage</th>
|
||||||
|
|
@ -364,22 +425,29 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(summary?.latency_stages || []).length ? summary.latency_stages.map((stage) => (
|
{latencyStageRows.map((stage, index) => {
|
||||||
<tr key={stage.stage}>
|
if (!stage) {
|
||||||
<td>{stageLabel(stage.stage)}</td>
|
return (
|
||||||
<td>{formatTimingMs(stage.p50_ms)}</td>
|
<tr className="competitiveness-placeholder-row" key={`latency-stage-placeholder:${index}`}>
|
||||||
<td>{formatTimingMs(stage.p90_ms)}</td>
|
<td colSpan={4}>{index === 0 && !latencyStages.length ? 'No stage timing percentiles are available yet.' : ''}</td>
|
||||||
<td>{formatTimingMs(stage.p99_ms)}</td>
|
</tr>
|
||||||
</tr>
|
);
|
||||||
)) : (
|
}
|
||||||
<tr><td colSpan={4}>No stage timing percentiles are available yet.</td></tr>
|
return (
|
||||||
)}
|
<tr key={stage.stage}>
|
||||||
|
<td>{stageLabel(stage.stage)}</td>
|
||||||
|
<td>{formatTimingMs(stage.p50_ms)}</td>
|
||||||
|
<td>{formatTimingMs(stage.p90_ms)}</td>
|
||||||
|
<td>{formatTimingMs(stage.p99_ms)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</TableFrame>
|
</TableFrame>
|
||||||
|
|
||||||
<TableFrame>
|
<TableFrame className="competitiveness-frame">
|
||||||
<table>
|
<table className="competitiveness-table competitiveness-age-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Age bucket</th>
|
<th>Age bucket</th>
|
||||||
|
|
@ -389,27 +457,34 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ageBuckets.length ? ageBuckets.slice(0, 12).map((bucket, index) => (
|
{ageBucketRows.map((bucket, index) => {
|
||||||
<tr key={`${bucket.pair}:${bucket.quote_age_bucket}:${bucket.outcome_status}:${index}`}>
|
if (!bucket) {
|
||||||
<td>
|
return (
|
||||||
<div>{bucket.quote_age_bucket}</div>
|
<tr className="competitiveness-placeholder-row" key={`age-bucket-placeholder:${index}`}>
|
||||||
<div className="status-subtle">{pairDisplayLabel(bucket.pair, pairConfig)}</div>
|
<td colSpan={4}>{index === 0 && !ageBuckets.length ? 'No quote-age buckets are available yet.' : ''}</td>
|
||||||
</td>
|
</tr>
|
||||||
<td>{plainCodeLabel(bucket.outcome_status)}</td>
|
);
|
||||||
<td>{bucket.count}</td>
|
}
|
||||||
<td>{bucket.accepted_count || 0}</td>
|
return (
|
||||||
</tr>
|
<tr key={`${bucket.pair}:${bucket.quote_age_bucket}:${bucket.outcome_status}:${bucket.notional_bucket || ''}`}>
|
||||||
)) : (
|
<td>
|
||||||
<tr><td colSpan={4}>No quote-age buckets are available yet.</td></tr>
|
<div>{bucket.quote_age_bucket}</div>
|
||||||
)}
|
<div className="status-subtle">{pairDisplayLabel(bucket.pair, pairConfig)}</div>
|
||||||
|
</td>
|
||||||
|
<td>{plainCodeLabel(bucket.outcome_status)}</td>
|
||||||
|
<td>{bucket.count}</td>
|
||||||
|
<td>{bucket.accepted_count || 0}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</TableFrame>
|
</TableFrame>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="two-column-grid">
|
<div className="stack-grid competitiveness-table-stack">
|
||||||
<TableFrame>
|
<TableFrame className="competitiveness-frame">
|
||||||
<table>
|
<table className="competitiveness-table competitiveness-detail-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Latest relay errors</th>
|
<th>Latest relay errors</th>
|
||||||
|
|
@ -418,28 +493,39 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{latestErrors.length ? latestErrors.map((error) => (
|
{latestErrorRows.map((error, index) => {
|
||||||
<tr key={`${error.quote_id}:${error.result_at}`}>
|
if (!error) {
|
||||||
<td>
|
return (
|
||||||
<IdentifierRow label="Quote" value={error.quote_id} />
|
<tr className="competitiveness-placeholder-row" key={`latest-error-placeholder:${index}`}>
|
||||||
<div className="status-subtle">{pairDisplayLabel(error.pair, pairConfig)}</div>
|
<td colSpan={3}>{index === 0 && !latestErrors.length ? 'No relay errors are available yet.' : ''}</td>
|
||||||
<div className="status-subtle">{plainCodeLabel(error.failure_category || error.result_code)}</div>
|
</tr>
|
||||||
</td>
|
);
|
||||||
<td>
|
}
|
||||||
<div>{formatTimingMs(error.quote_age_ms) || 'Unavailable'}</div>
|
return (
|
||||||
<div className="status-subtle">{error.quote_age_bucket}</div>
|
<tr key={`${error.quote_id}:${error.result_at}`}>
|
||||||
</td>
|
<td>
|
||||||
<td>{error.error_message || 'Error text unavailable'}</td>
|
<IdentifierRow label="Quote" value={error.quote_id} />
|
||||||
</tr>
|
<div className="status-subtle">{pairDisplayLabel(error.pair, pairConfig)}</div>
|
||||||
)) : (
|
<div className="status-subtle">{plainCodeLabel(error.failure_category || error.result_code)}</div>
|
||||||
<tr><td colSpan={3}>No relay errors are available yet.</td></tr>
|
</td>
|
||||||
)}
|
<td>
|
||||||
|
<div>{formatTimingMs(error.quote_age_ms) || 'Unavailable'}</div>
|
||||||
|
<div className="status-subtle">{error.quote_age_bucket}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="competitiveness-error-text" title={error.error_message || ''}>
|
||||||
|
{error.error_message || 'Error text unavailable'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</TableFrame>
|
</TableFrame>
|
||||||
|
|
||||||
<TableFrame>
|
<TableFrame className="competitiveness-frame">
|
||||||
<table>
|
<table className="competitiveness-table competitiveness-detail-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Policy skips</th>
|
<th>Policy skips</th>
|
||||||
|
|
@ -448,24 +534,31 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{policySkips.length ? policySkips.map((skip) => (
|
{policySkipRows.map((skip, index) => {
|
||||||
<tr key={`${skip.quote_id}:${skip.decision_at}`}>
|
if (!skip) {
|
||||||
<td>
|
return (
|
||||||
<IdentifierRow label="Quote" value={skip.quote_id} />
|
<tr className="competitiveness-placeholder-row" key={`policy-skip-placeholder:${index}`}>
|
||||||
<div className="status-subtle">{plainCodeLabel(skip.reason_code)}</div>
|
<td colSpan={3}>{index === 0 && !policySkips.length ? 'No policy skips are available yet.' : ''}</td>
|
||||||
</td>
|
</tr>
|
||||||
<td>
|
);
|
||||||
<div>{formatTimingMs(skip.quote_age_ms) || 'Unavailable'}</div>
|
}
|
||||||
<div className="status-subtle">{`max ${formatTimingMs(skip.max_quote_age_ms) || 'Unavailable'}`}</div>
|
return (
|
||||||
</td>
|
<tr key={`${skip.quote_id}:${skip.decision_at}`}>
|
||||||
<td>
|
<td>
|
||||||
<div>{skip.pair_config_version ? `v${skip.pair_config_version}` : 'Version unavailable'}</div>
|
<IdentifierRow label="Quote" value={skip.quote_id} />
|
||||||
<div className="status-subtle mono">{truncateMiddle(skip.pair_config_id || '', 36)}</div>
|
<div className="status-subtle">{plainCodeLabel(skip.reason_code)}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
)) : (
|
<div>{formatTimingMs(skip.quote_age_ms) || 'Unavailable'}</div>
|
||||||
<tr><td colSpan={3}>No policy skips are available yet.</td></tr>
|
<div className="status-subtle">{`max ${formatTimingMs(skip.max_quote_age_ms) || 'Unavailable'}`}</div>
|
||||||
)}
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>{skip.pair_config_version ? `v${skip.pair_config_version}` : 'Version unavailable'}</div>
|
||||||
|
<div className="status-subtle mono">{truncateMiddle(skip.pair_config_id || '', 36)}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</TableFrame>
|
</TableFrame>
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,12 @@ select {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.metric-card {
|
.metric-card {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
@ -272,6 +278,45 @@ select {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.competitiveness-snapshot-note {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitiveness-table-stack {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitiveness-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
min-width: 1040px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitiveness-table tbody tr {
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitiveness-detail-table tbody tr {
|
||||||
|
height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitiveness-table td {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitiveness-placeholder-row td {
|
||||||
|
height: inherit;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.competitiveness-error-text {
|
||||||
|
display: -webkit-box;
|
||||||
|
max-height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-row {
|
.checkbox-row {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,18 @@ test('strategy page exposes maker timing waterfall and competitiveness summaries
|
||||||
assert.match(strategySource, /maker_competitiveness/);
|
assert.match(strategySource, /maker_competitiveness/);
|
||||||
assert.match(strategySource, /pairDisplayLabel/);
|
assert.match(strategySource, /pairDisplayLabel/);
|
||||||
assert.match(strategySource, /group\.edge_bps/);
|
assert.match(strategySource, /group\.edge_bps/);
|
||||||
assert.match(stylesSource, /\.two-column-grid/);
|
assert.match(strategySource, /Pause updates/);
|
||||||
|
assert.match(strategySource, /Resume updates/);
|
||||||
|
assert.match(strategySource, /displaySummary/);
|
||||||
|
assert.match(strategySource, /fixedRows/);
|
||||||
|
assert.match(strategySource, /COMPETITIVENESS_REFRESH_MS/);
|
||||||
|
assert.match(strategySource, /latencyStageRows/);
|
||||||
|
assert.match(stylesSource, /\.metric-stack/);
|
||||||
|
assert.match(stylesSource, /\.competitiveness-table-stack/);
|
||||||
|
assert.match(stylesSource, /\.competitiveness-table tbody tr/);
|
||||||
|
assert.match(stylesSource, /\.competitiveness-detail-table tbody tr/);
|
||||||
|
assert.match(stylesSource, /\.competitiveness-error-text/);
|
||||||
|
assert.match(stylesSource, /\.competitiveness-placeholder-row td/);
|
||||||
assert.match(stylesSource, /\.timing-waterfall-table/);
|
assert.match(stylesSource, /\.timing-waterfall-table/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue