mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-10-23 14:22:47 +00:00
Refine beta links UI and lock live feed behavior
This commit is contained in:
1 parent
e86912db9e
commit
ec861c4332
5 files changed
+48
-136
No files matched your search
@@ -16,6 +16,16 @@ interface FilterPanelProps {
|
||||
betaPathConfidence?: number | null;
|
||||
}
|
||||
|
||||
export const LinksLegend: React.FC<{ compact?: boolean; muted?: boolean }> = ({ compact = false, muted = false }) => (
|
||||
<div className={`links-legend-inline${compact ? ' links-legend-inline--compact' : ''}${muted ? ' links-legend-inline--muted' : ''}`}>
|
||||
<div className="links-legend-inline__title">Links Legend</div>
|
||||
<div className="links-legend-inline__row"><span className="links-legend__swatch" style={{ background: '#22c55e' }} /> Good (≤120 dB)</div>
|
||||
<div className="links-legend-inline__row"><span className="links-legend__swatch" style={{ background: '#fbbf24' }} /> Marginal (121-135 dB)</div>
|
||||
<div className="links-legend-inline__row"><span className="links-legend__swatch" style={{ background: '#ef4444' }} /> Weak (>135 dB)</div>
|
||||
<div className="links-legend-inline__row"><span className="links-legend__swatch" style={{ background: '#d1d5db' }} /> Unknown (no dB yet)</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const FILTER_ROWS: Array<{ key: keyof Filters; label: string; color: string; hollow?: boolean }> = [
|
||||
{ key: 'livePackets', label: 'Live Feed', color: '#00c4ff' },
|
||||
{ key: 'packetPaths', label: 'Packet Paths', color: '#00c4ff', hollow: true },
|
||||
@@ -30,33 +40,6 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onChange, bet
|
||||
onChange({ ...filters, [key]: !filters[key] });
|
||||
};
|
||||
|
||||
const applyPreset = (preset: 'minimal' | 'links' | 'beta') => {
|
||||
const next: Filters = { ...filters };
|
||||
if (preset === 'minimal') {
|
||||
next.livePackets = true;
|
||||
next.packetPaths = false;
|
||||
next.betaPaths = false;
|
||||
next.links = false;
|
||||
next.coverage = false;
|
||||
next.clientNodes = false;
|
||||
} else if (preset === 'links') {
|
||||
next.livePackets = true;
|
||||
next.packetPaths = false;
|
||||
next.betaPaths = true;
|
||||
next.links = true;
|
||||
next.coverage = false;
|
||||
next.clientNodes = false;
|
||||
} else {
|
||||
next.livePackets = true;
|
||||
next.packetPaths = false;
|
||||
next.betaPaths = true;
|
||||
next.links = true;
|
||||
next.coverage = true;
|
||||
next.clientNodes = false;
|
||||
}
|
||||
onChange(next);
|
||||
};
|
||||
|
||||
const confidenceLabel = betaPathConfidence == null
|
||||
? 'N/A'
|
||||
: betaPathConfidence >= 0.75
|
||||
@@ -68,11 +51,6 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onChange, bet
|
||||
return (
|
||||
<div className="filter-panel">
|
||||
<div className="filter-panel__title">Layers</div>
|
||||
<div className="filter-presets">
|
||||
<button className="filter-preset-btn" onClick={() => applyPreset('minimal')}>Minimal</button>
|
||||
<button className="filter-preset-btn" onClick={() => applyPreset('links')}>Links + Beta</button>
|
||||
<button className="filter-preset-btn" onClick={() => applyPreset('beta')}>Full Beta</button>
|
||||
</div>
|
||||
{filters.betaPaths && (
|
||||
<div className="filter-beta-note">
|
||||
Beta Confidence: <strong>{confidenceLabel}</strong>
|
||||
@@ -123,6 +101,7 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onChange, bet
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<LinksLegend muted={!filters.links} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import { MapContainer, TileLayer, useMap, Pane, Polygon, Polyline, Tooltip } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, useMap, Pane, Polygon, Polyline } from 'react-leaflet';
|
||||
import type { LatLngExpression, Map as LeafletMap, Polyline as LeafletPolyline } from 'leaflet';
|
||||
import type { MeshNode, PacketArc } from '../../hooks/useNodes.js';
|
||||
import type { NodeCoverage } from '../../hooks/useCoverage.js';
|
||||
@@ -294,16 +294,8 @@ export const MapView: React.FC<MapViewProps> = ({
|
||||
weight,
|
||||
opacity,
|
||||
}}
|
||||
interactive
|
||||
>
|
||||
<Tooltip direction="center" sticky>
|
||||
<div className="link-tooltip">
|
||||
<div className="link-tooltip__row"><span>Seen</span><strong>{line.observedCount}</strong></div>
|
||||
<div className="link-tooltip__row"><span>Path Loss</span><strong>{line.pathLossDb == null ? 'unknown' : `${Math.round(line.pathLossDb)} dB`}</strong></div>
|
||||
<div className="link-tooltip__row"><span>A→B / B→A</span><strong>{line.countAToB} / {line.countBToA}</strong></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Polyline>
|
||||
interactive={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Pane>
|
||||
@@ -358,16 +350,6 @@ export const MapView: React.FC<MapViewProps> = ({
|
||||
)}
|
||||
</MapContainer>
|
||||
|
||||
{showLinks && (
|
||||
<div className="links-legend">
|
||||
<div className="links-legend__title">Links Legend</div>
|
||||
<div className="links-legend__row"><span className="links-legend__swatch" style={{ background: '#22c55e' }} /> Good (≤120 dB)</div>
|
||||
<div className="links-legend__row"><span className="links-legend__swatch" style={{ background: '#fbbf24' }} /> Marginal (121-135 dB)</div>
|
||||
<div className="links-legend__row"><span className="links-legend__swatch" style={{ background: '#ef4444' }} /> Weak (>135 dB)</div>
|
||||
<div className="links-legend__row"><span className="links-legend__swatch" style={{ background: '#d1d5db' }} /> Unknown (no dB yet)</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* deck.gl overlay — arcs only */}
|
||||
<PacketArcLayer
|
||||
arcs={arcs}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { AggregatedPacket, MeshNode } from '../hooks/useNodes.js';
|
||||
|
||||
const TYPE_LABELS: Record<number, string> = {
|
||||
@@ -21,27 +21,14 @@ interface Props {
|
||||
pinnedPacketId?: string | null;
|
||||
}
|
||||
|
||||
const ROW_HEIGHT = 32;
|
||||
const VISIBLE_ROWS = 8;
|
||||
const OVERSCAN = 4;
|
||||
|
||||
export const PacketFeed: React.FC<Props> = React.memo(({ packets, nodes, onPacketClick, pinnedPacketId }) => {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const total = packets.length;
|
||||
const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
|
||||
const end = Math.min(total, start + VISIBLE_ROWS + OVERSCAN * 2);
|
||||
const visible = useMemo(
|
||||
() => packets.slice(start, end).map((packet, idx) => ({ packet, index: start + idx })),
|
||||
[packets, start, end],
|
||||
);
|
||||
const visible = useMemo(() => packets.slice(0, VISIBLE_ROWS), [packets]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`packet-feed${packets.length >= VISIBLE_ROWS ? ' packet-feed--overflow' : ''}`}
|
||||
onScroll={(e) => setScrollTop((e.target as HTMLDivElement).scrollTop)}
|
||||
>
|
||||
<div style={{ height: total * ROW_HEIGHT, position: 'relative' }}>
|
||||
{visible.map(({ packet: p, index }) => {
|
||||
<div className="packet-feed">
|
||||
{visible.map((p) => {
|
||||
const typeLabel = p.packetType !== undefined
|
||||
? (TYPE_LABELS[p.packetType] ?? `T${p.packetType}`)
|
||||
: '???';
|
||||
@@ -63,7 +50,6 @@ export const PacketFeed: React.FC<Props> = React.memo(({ packets, nodes, onPacke
|
||||
<div
|
||||
key={p.id}
|
||||
className={`packet-item packet-item--clickable${isPinned ? ' packet-item--pinned' : ''}`}
|
||||
style={{ position: 'absolute', top: index * ROW_HEIGHT, left: 0, right: 0 }}
|
||||
onClick={() => onPacketClick?.(p)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -87,7 +73,6 @@ export const PacketFeed: React.FC<Props> = React.memo(({ packets, nodes, onPacke
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { Map as LeafletMap } from 'leaflet';
|
||||
import { NodeSearch } from '../Map/NodeSearch.js';
|
||||
import { FILTER_ROWS, type Filters } from '../FilterPanel/FilterPanel.js';
|
||||
import { FILTER_ROWS, LinksLegend, type Filters } from '../FilterPanel/FilterPanel.js';
|
||||
import type { MeshNode } from '../../hooks/useNodes.js';
|
||||
|
||||
type MobileControlsProps = {
|
||||
@@ -42,9 +42,11 @@ export const MobileControls: React.FC<MobileControlsProps> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mobile-links-legend-wrap">
|
||||
<LinksLegend compact muted={!filters.links} />
|
||||
</div>
|
||||
<div className="mobile-search">
|
||||
<NodeSearch map={map} nodes={nodes} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -327,33 +327,6 @@ html, body, #root {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-presets {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-preset-btn {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-panel-alt);
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), color var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.filter-preset-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-bright);
|
||||
}
|
||||
|
||||
.filter-beta-note {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
@@ -544,48 +517,39 @@ html, body, #root {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.link-tooltip {
|
||||
min-width: 180px;
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.link-tooltip__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.links-legend {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 70px;
|
||||
z-index: 820;
|
||||
.links-legend-inline {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 10px 12px;
|
||||
min-width: 180px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
pointer-events: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.links-legend__title {
|
||||
.links-legend-inline--muted {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.links-legend-inline--compact {
|
||||
margin: 6px 8px 0;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
|
||||
.links-legend-inline__title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.links-legend__row {
|
||||
.links-legend-inline__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.links-legend__swatch {
|
||||
@@ -603,16 +567,12 @@ html, body, #root {
|
||||
z-index: 800;
|
||||
width: 340px;
|
||||
height: 220px;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
pointer-events: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
/* Fade the oldest (topmost) item only when the feed is full */
|
||||
.packet-feed--overflow {
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 40px, black 100%);
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, black 40px, black 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.packet-item {
|
||||
@@ -1758,11 +1718,15 @@ html, body, #root {
|
||||
.mobile-filter-grid .filter-row { padding: 8px 6px; }
|
||||
|
||||
/* ── Mobile search: shown below filter grid ──────────────────────────── */
|
||||
.mobile-search {
|
||||
.mobile-search {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.mobile-links-legend-wrap {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 2px;
|
||||
}
|
||||
.mobile-search .node-search {
|
||||
position: static;
|
||||
transform: none;
|
||||
|
||||
Reference in new issue
Block a user