diff --git a/server/api_test.go b/server/api_test.go index 9ddcfcd..4923f23 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -447,8 +447,10 @@ func TestUIScoredFalseSuppressesVerdicts(t *testing.T) { t.Error("loss curve is not gated on d.scored; it would render when unscored") } - // The table function must accept a scored parameter. - if !strings.Contains(page, "function table(rungs, scored)") { + // The table function must accept a scored parameter and the corridor's + // dependencies (so an unpriced DERIVATIVE rung can name what it routes + // through without overflowing the row). + if !strings.Contains(page, "function table(rungs, scored, depends)") { t.Error("table() does not accept a scored parameter") } diff --git a/server/index.html b/server/index.html index 7ebb14c..c831327 100644 --- a/server/index.html +++ b/server/index.html @@ -63,14 +63,26 @@ background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 1.15rem 1.25rem; margin-bottom: 1.25rem; } + .integrity-card { display: grid; grid-template-columns: auto 1fr; gap: .75rem 1rem; align-items: start; margin-bottom: .35rem; } .badge { - display: inline-block; font: 600 .72rem/1 var(--mono); letter-spacing: .06em; - padding: .4rem .6rem; border-radius: 5px; border: 1px solid currentColor; - text-transform: uppercase; + display: inline-flex; align-items: center; gap: .45rem; width: fit-content; + font: 700 .78rem/1 var(--mono); letter-spacing: .06em; + padding: .55rem .7rem; border-radius: 7px; border: 2px solid currentColor; + text-transform: uppercase; white-space: nowrap; } - .b-direct { color: var(--muted); } - .b-derivative { color: var(--warn); } - .b-nomarket { color: var(--bad); } + .badge::before { font-size: 1rem; line-height: 1; } + .b-direct { color: var(--ok); background: var(--ok-soft); } + .b-direct::before { content: "◆"; } + .b-derivative { color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, var(--panel)); } + .b-derivative::before { content: "↪"; } + .b-nomarket { color: var(--bad); background: var(--bad-soft); } + .b-nomarket::before { content: "∅"; } + .integrity-copy { min-width: 0; } + .integrity-title { font-weight: 700; margin: 0; } + .integrity-help { color: var(--muted); font-size: .88rem; margin: .15rem 0 0; } + .integrity-dependency { font-family: var(--mono); font-size: .8rem; margin: .35rem 0 0; color: var(--warn); } + .integrity-card .badge[title] { cursor: help; } + @media (max-width: 520px) { .integrity-card { grid-template-columns: 1fr; gap: .45rem; } } .finding { font-size: 1.02rem; margin: .85rem 0 0; } .rec-none { margin-top: .9rem; padding: .7rem .85rem; border-radius: 6px; @@ -136,6 +148,10 @@ .legend-item { display: flex; align-items: center; gap: .45rem; min-width: 0; } .legend-item span:last-child { font-size: .8rem; } .case { font-size: .88rem; color: var(--muted); } + .integrity-cell { white-space: nowrap; } + .integrity-cell .badge { font-size: .65rem; padding: .3rem .45rem; border-width: 1px; } + .integrity-cell .badge::before { font-size: .8rem; } + .integrity-cell .integrity-via { font-family: var(--mono); font-size: .72rem; color: var(--warn); margin-left: .35rem; } .case strong { color: var(--ink); font-weight: 600; } svg { width: 100%; height: auto; display: block; } .axis { fill: var(--muted); font: 10px var(--mono); } @@ -234,6 +250,50 @@

Wayfare

const badgeClass = (i) => i === 'NO-MARKET' ? 'b-nomarket' : i === 'DERIVATIVE' ? 'b-derivative' : 'b-direct'; +const integrityInfo = (state, dependsOn = []) => { + if (state === 'NO-MARKET') return { + title: 'No market exists', + help: 'No path exists at the tested size. This is the absence of a price — not a bad price.', + tooltip: 'NO-MARKET means no path exists at all: there is no loss percentage to compute.' + }; + if (state === 'DERIVATIVE') { + const via = dependsOn.length ? dependsOn.map(a => a.code || a).join(' and ') : 'another fiat token'; + return { + title: 'Depends on an upstream corridor', + help: `Every path routes through ${via}; there is no independent market.`, + dependency: `depends on ${via}`, + tooltip: `DERIVATIVE means every available path traverses another fiat-pegged token, so this corridor inherits its liquidity and failure modes.` + }; + } + return { + title: 'Independent market found', + help: 'At least one path reaches the destination without another fiat token in between.', + tooltip: 'DIRECT means an independent market exists: at least one path reaches the destination without routing through another fiat token.' + }; +}; + +function integrityElement(state, dependsOn = '') { + const info = integrityInfo(state, Array.isArray(dependsOn) ? dependsOn : []); + const dependency = info.dependency ? `

${esc(info.dependency)}

` : ''; + return `
+ ${esc(state)} +

${esc(info.title)}

${esc(info.help)}

${dependency}
+
`; +} + +// Compact form for table cells: the state badge plus a short dependency +// note, with the full explanation kept in the hover tooltip. The headline +// and trend views carry the complete card; inside a nowrap table cell the +// card's description would overflow the row. +function integrityCell(state, dependsOn = []) { + const info = integrityInfo(state, Array.isArray(dependsOn) ? dependsOn : []); + const tip = info.tooltip + (info.dependency ? ' — ' + info.dependency : ''); + const dependency = info.dependency + ? `${esc(info.dependency)}` : ''; + return `${esc(state)}${dependency}`; +} + const verdictClass = (v) => 'v-' + String(v).toLowerCase(); const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', @@ -269,7 +329,7 @@

Wayfare

// Headline: integrity state and the finding, before any numbers. parts.push(`${provenance}
- ${esc(d.integrity)} + ${integrityElement(d.integrity, d.depends_on || [])}

${esc(d.finding)}

${d.scored ? recommendationBlock(d) : unscoredBlock(d)}

Reference ${esc(d.reference_pair)} mid @@ -282,7 +342,7 @@

Wayfare

parts.push(`

Loss against mid, by size

${curve(priced)}
`); } - parts.push(`

Measurements

${table(d.rungs, d.scored)}
`); + parts.push(`

Measurements

${table(d.rungs, d.scored, d.depends_on || [])}
`); if (d.scored) parts.push(legend()); @@ -363,20 +423,24 @@

Wayfare

`; } -function table(rungs, scored) { +function table(rungs, scored, depends) { const rows = rungs.map(r => { if (!r.priced) { const why = r.error ? esc(r.error) : r.integrity === 'NO-MARKET' ? 'no path exists at this size' : 'not priced'; + // An unpriced rung still reports the corridor's structural state, so a + // DERIVATIVE rung names what it routes through; the compact cell keeps + // the row from overflowing the table. + const cell = integrityCell(r.integrity, depends); if (scored) { return `${esc(r.send_amount)} ${why} - ${esc(r.integrity)}`; + ${cell}`; } return `${esc(r.send_amount)} ${why} - ${esc(r.integrity)}`; + ${cell}`; } const q = r.quote; if (scored) { @@ -623,7 +687,7 @@

Trend — ${esc(d.corridor)}

const latest = runs[runs.length - 1]; parts.push(`

Trend — ${esc(d.corridor)}

- ${esc(latest.integrity)} + ${integrityElement(latest.integrity, latest.depends_on || [])}   latest of ${runs.length} stored run(s) · ${esc(fmtRunTime(runs[0].recorded_at))} → ${esc(fmtRunTime(latest.recorded_at))} UTC

${referenceSummary(runs)}${integritySummary(runs)}

@@ -830,16 +894,14 @@

Stored runs

const rows = runs.slice().reverse().map(r => { const ref = r.reference || {}; const src = ref.scored_against || ref.source || 'unknown'; - const via = (r.depends_on || []).length - ? ` via ${esc(r.depends_on.join(', '))}` : ''; const div = ref.divergence_pct ? ` div ${esc(ref.divergence_pct)}%` : ''; return ` ${esc(r.recorded_at)} ${r.seq} - ${esc(r.integrity)}${via} ${esc(r.floor_loss_pct)}% ${esc(r.worst_loss_pct)}% + ${integrityCell(r.integrity, r.depends_on || [])} ${esc(src)}${div} `; }).join('');