diff --git a/public/app.js b/public/app.js index 01d989d4..efc1a482 100644 --- a/public/app.js +++ b/public/app.js @@ -42,6 +42,18 @@ function transportBadge(rt, scopeName) { return ' ' + escapeHtml(label) + ''; } +/** + * Dedicated Scope column cell (as opposed to transportBadge's inline pill + * on the Type column) — same three-state semantics: non-transport routes + * show a dash, transport routes with no resolved region show a muted + * "unknown", and a resolved match shows the region name. + */ +function scopeCellHtml(rt, scopeName) { + if (!isTransportRoute(rt)) return '—'; + if (scopeName) return '' + escapeHtml(scopeName) + ''; + return 'unknown'; +} + /** * Compute breakdown byte ranges from raw_hex on the client. * Mirrors cmd/server/decoder.go BuildBreakdown(). Used so per-observation raw_hex diff --git a/public/packets.js b/public/packets.js index ed585cee..e056ef92 100644 --- a/public/packets.js +++ b/public/packets.js @@ -1626,7 +1626,7 @@ RegionTimeHashSize HB - TypeObserverPathRptDetails + TypeScopeObserverPathRptDetails @@ -2010,6 +2010,7 @@ { key: 'hash', label: 'Hash' }, { key: 'size', label: 'Size' }, { key: 'type', label: 'Type' }, + { key: 'scope', label: 'Scope' }, { key: 'observer', label: 'Observer' }, { key: 'path', label: 'Path' }, { key: 'rpt', label: 'Rpt' }, @@ -2019,7 +2020,7 @@ // #1249: observer column must stay visible at narrow widths so the IATA // badge (#1188) renders on mobile. Without observer in scope the user // can't see who heard the packet at all. - const defaultHidden = isNarrow ? ['region', 'hash', 'path', 'rpt', 'size'] : ['region']; + const defaultHidden = isNarrow ? ['region', 'hash', 'path', 'rpt', 'size', 'scope'] : ['region']; let visibleCols; try { visibleCols = JSON.parse(localStorage.getItem('packets-visible-cols')); @@ -2264,6 +2265,7 @@ ${groupSize ? groupSize + 'B' : '—'} ${groupHashBytes} ${p.payload_type != null ? `${groupTypeName}${transportBadge(p.route_type, p.scope_name)}` : '—'} + ${scopeCellHtml(p.route_type, p.scope_name)} ${isSingle ? escapeHtml(truncate(obsNameOnly(headerObserverId), 16)) + obsIataBadge(p) : escapeHtml(truncate(obsNameOnly(headerObserverId), 10)) + groupedObserverIataBadgesHtml(p)} ${groupPathStr} ${p.observation_count > 1 ? ' ' + p.observation_count + '' : (isSingle ? '' : p.count)} @@ -2298,6 +2300,7 @@ ${size}B ${childHashBytes} ${typeName}${transportBadge(c.route_type, c.scope_name)} + ${scopeCellHtml(c.route_type, c.scope_name)} ${escapeHtml(truncate(obsNameOnly(c.observer_id), 16))}${obsIataBadge(c)} ${childPathStr} @@ -2334,6 +2337,7 @@ ${size}B ${hashBytes} ${typeName}${transportBadge(p.route_type, p.scope_name)} + ${scopeCellHtml(p.route_type, p.scope_name)} ${escapeHtml(truncate(obsNameOnly(p.observer_id), 16))}${obsIataBadge(p)} ${pathStr} @@ -2695,6 +2699,7 @@ case 'type': accessor = function(p) { return typeName(p.payload_type); }; break; case 'hash': accessor = function(p) { return p.hash || ''; }; break; case 'observer': accessor = function(p) { return obsName(p.observer_id); }; break; + case 'scope': accessor = function(p) { return p.scope_name || ''; }; break; case 'size': accessor = function(p) { return p.packet_size || 0; }; break; case 'hb': accessor = function(p) { return p.hash_byte_count != null ? p.hash_byte_count : (p.hash_size || 0); }; break; case 'rpt': accessor = function(p) { diff --git a/public/style.css b/public/style.css index b46ab281..4b02fe18 100644 --- a/public/style.css +++ b/public/style.css @@ -1436,6 +1436,18 @@ body.scroll-locked { overflow: hidden; } .badge-transport-unknown { background: var(--text-muted-bg, #6b728020); color: var(--text-muted, #6b7280); } +/* Dedicated Scope column pill (#71 Packets column). Reuses the + badge-transport-unknown muted look for the "no match" state so it reads + consistently with the inline T? pill on the Type column. */ +.badge-scope { + display: inline-block; padding: 1px 6px; border-radius: 4px; + font-size: 10px; font-weight: 700; font-family: var(--mono); + background: var(--transport-badge-bg, #f59e0b20); color: var(--transport-badge-fg, #d97706); + letter-spacing: .3px; +} +.badge-scope-unknown { + background: var(--text-muted-bg, #6b728020); color: var(--text-muted, #6b7280); +} .badge-obs { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; @@ -2137,6 +2149,7 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; } .obs-table td:first-child { white-space: nowrap; } .obs-table td:nth-child(6) { max-width: none; overflow: visible; } .col-observer { min-width: 70px; max-width: none; } +.col-scope { min-width: 60px; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .spark-bar { position: relative; min-width: 60px; max-width: 100px; flex: 1; height: 18px; background: var(--border); border-radius: 4px; overflow: hidden; display: inline-block; vertical-align: middle; } @media (max-width: 640px) { .spark-bar { max-width: 60px; } } .spark-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-hover, #60a5fa)); border-radius: 4px; transition: width 0.3s; } @@ -2993,7 +3006,7 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); } /* Hide low-value columns on mobile */ @media (max-width: 640px) { - .col-region, .col-rpt, .col-size, .col-hashsize, .col-pubkey { display: none; } + .col-region, .col-rpt, .col-size, .col-hashsize, .col-pubkey, .col-scope { display: none; } } /* Clickable hop links */ @@ -3185,6 +3198,7 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); } .hide-col-hash .col-hash, .hide-col-size .col-size, .hide-col-type .col-type, +.hide-col-scope .col-scope, .hide-col-observer .col-observer, .hide-col-path .col-path, .hide-col-rpt .col-rpt,