mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-03-28 17:43:06 +01:00
Merge pull request #8 from rightup/dev
Updated CAD calibration logic & new dialog for displaying packet information
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pymc_repeater"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
authors = [
|
||||
{name = "Lloyd", email = "lloyd@rightup.co.uk"},
|
||||
]
|
||||
@@ -31,7 +31,7 @@ keywords = ["mesh", "networking", "lora", "repeater", "daemon", "iot"]
|
||||
|
||||
|
||||
dependencies = [
|
||||
"pymc_core[hardware]>=1.0.3",
|
||||
"pymc_core[hardware]>=1.0.4",
|
||||
"pyyaml>=6.0.0",
|
||||
"cherrypy>=18.0.0",
|
||||
]
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.0.2"
|
||||
__version__ = "1.0.3"
|
||||
|
||||
@@ -195,6 +195,9 @@ class RepeaterHandler(BaseHandler):
|
||||
# Record packet for charts
|
||||
packet_record = {
|
||||
"timestamp": time.time(),
|
||||
"header": f"0x{packet.header:02X}" if hasattr(packet, "header") and packet.header is not None else None,
|
||||
"payload": packet.payload.hex() if hasattr(packet, "payload") and packet.payload else None,
|
||||
"payload_length": len(packet.payload) if hasattr(packet, "payload") and packet.payload else 0,
|
||||
"type": payload_type,
|
||||
"route": route_type,
|
||||
"length": len(packet.payload or b""),
|
||||
@@ -215,6 +218,7 @@ class RepeaterHandler(BaseHandler):
|
||||
"forwarded_path": (
|
||||
[f"{b:02X}" for b in forwarded_path] if forwarded_path is not None else None
|
||||
),
|
||||
|
||||
}
|
||||
|
||||
# If this is a duplicate, try to attach it to the original packet
|
||||
|
||||
@@ -58,38 +58,83 @@ class CADCalibrationEngine:
|
||||
self.calibration_thread = None
|
||||
|
||||
def get_test_ranges(self, spreading_factor: int):
|
||||
"""Get CAD test ranges based on spreading factor - comprehensive coverage"""
|
||||
"""Get CAD test ranges"""
|
||||
# Higher values = less sensitive, lower values = more sensitive
|
||||
# Test from LESS sensitive to MORE sensitive to find the sweet spot
|
||||
sf_ranges = {
|
||||
7: (range(17, 26, 1), range(7, 15, 1)), # Full range coverage
|
||||
8: (range(17, 26, 1), range(7, 15, 1)), # Full range coverage
|
||||
9: (range(19, 28, 1), range(8, 16, 1)), # Full range coverage
|
||||
10: (range(21, 30, 1), range(9, 17, 1)), # Full range coverage
|
||||
11: (range(23, 32, 1), range(10, 18, 1)), # Full range coverage
|
||||
12: (range(25, 34, 1), range(11, 19, 1)), # Full range coverage
|
||||
7: (range(22, 30, 1), range(12, 20, 1)),
|
||||
8: (range(22, 30, 1), range(12, 20, 1)),
|
||||
9: (range(24, 32, 1), range(14, 22, 1)),
|
||||
10: (range(26, 34, 1), range(16, 24, 1)),
|
||||
11: (range(28, 36, 1), range(18, 26, 1)),
|
||||
12: (range(30, 38, 1), range(20, 28, 1)),
|
||||
}
|
||||
return sf_ranges.get(spreading_factor, sf_ranges[8])
|
||||
|
||||
async def test_cad_config(self, radio, det_peak: int, det_min: int, samples: int = 8) -> Dict[str, Any]:
|
||||
"""Test a single CAD configuration with multiple samples"""
|
||||
async def test_cad_config(self, radio, det_peak: int, det_min: int, samples: int = 20) -> Dict[str, Any]:
|
||||
"""Test CAD configuration with proper spacing and baseline measurement"""
|
||||
detections = 0
|
||||
baseline_detections = 0
|
||||
|
||||
for _ in range(samples):
|
||||
# First, get baseline with very insensitive settings (should detect nothing)
|
||||
baseline_samples = 5
|
||||
for _ in range(baseline_samples):
|
||||
try:
|
||||
# Use very high thresholds that should detect nothing
|
||||
baseline_result = await radio.perform_cad(det_peak=35, det_min=25, timeout=0.3)
|
||||
if baseline_result:
|
||||
baseline_detections += 1
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.1) # 100ms between baseline samples
|
||||
|
||||
# Wait before actual test
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Now test the actual configuration
|
||||
for i in range(samples):
|
||||
try:
|
||||
result = await radio.perform_cad(det_peak=det_peak, det_min=det_min, timeout=0.3)
|
||||
if result:
|
||||
detections += 1
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.01) # Reduced sleep time
|
||||
|
||||
# Variable delay to avoid sampling artifacts
|
||||
delay = 0.05 + (i % 3) * 0.05 # 50ms, 100ms, 150ms rotation
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# Calculate adjusted detection rate
|
||||
baseline_rate = (baseline_detections / baseline_samples) * 100
|
||||
detection_rate = (detections / samples) * 100
|
||||
|
||||
# Subtract baseline noise
|
||||
adjusted_rate = max(0, detection_rate - baseline_rate)
|
||||
|
||||
return {
|
||||
'det_peak': det_peak,
|
||||
'det_min': det_min,
|
||||
'samples': samples,
|
||||
'samples': samples,
|
||||
'detections': detections,
|
||||
'detection_rate': (detections / samples) * 100,
|
||||
'detection_rate': detection_rate,
|
||||
'baseline_rate': baseline_rate,
|
||||
'adjusted_rate': adjusted_rate, # This is the useful metric
|
||||
'sensitivity_score': self._calculate_sensitivity_score(det_peak, det_min, adjusted_rate)
|
||||
}
|
||||
|
||||
def _calculate_sensitivity_score(self, det_peak: int, det_min: int, adjusted_rate: float) -> float:
|
||||
"""Calculate a sensitivity score - higher is better balance"""
|
||||
# Ideal detection rate is around 10-30% for good sensitivity without false positives
|
||||
ideal_rate = 20.0
|
||||
rate_penalty = abs(adjusted_rate - ideal_rate) / ideal_rate
|
||||
|
||||
# Prefer moderate sensitivity settings (not too extreme)
|
||||
sensitivity_penalty = (abs(det_peak - 25) + abs(det_min - 15)) / 20.0
|
||||
|
||||
# Lower penalty = higher score
|
||||
score = max(0, 100 - (rate_penalty * 50) - (sensitivity_penalty * 20))
|
||||
return score
|
||||
|
||||
def broadcast_to_clients(self, data):
|
||||
"""Send data to all connected SSE clients"""
|
||||
# Store the message for clients to pick up
|
||||
@@ -217,15 +262,29 @@ class CADCalibrationEngine:
|
||||
time.sleep(delay_ms / 1000.0)
|
||||
|
||||
if self.running:
|
||||
# Find best result
|
||||
# Find best result based on sensitivity score (not just detection rate)
|
||||
best_result = None
|
||||
recommended_result = None
|
||||
if self.results:
|
||||
best_result = max(self.results.values(), key=lambda x: x['detection_rate'])
|
||||
# Find result with highest sensitivity score (best balance)
|
||||
best_result = max(self.results.values(), key=lambda x: x.get('sensitivity_score', 0))
|
||||
|
||||
# Also find result with ideal adjusted detection rate (10-30%)
|
||||
ideal_results = [r for r in self.results.values() if 10 <= r.get('adjusted_rate', 0) <= 30]
|
||||
if ideal_results:
|
||||
# Among ideal results, pick the one with best sensitivity score
|
||||
recommended_result = max(ideal_results, key=lambda x: x.get('sensitivity_score', 0))
|
||||
else:
|
||||
recommended_result = best_result
|
||||
|
||||
self.broadcast_to_clients({
|
||||
"type": "completed",
|
||||
"message": "Calibration completed",
|
||||
"results": {"best": best_result} if best_result else None
|
||||
"results": {
|
||||
"best": best_result,
|
||||
"recommended": recommended_result,
|
||||
"total_tests": len(self.results)
|
||||
} if best_result else None
|
||||
})
|
||||
else:
|
||||
self.broadcast_to_clients({"type": "status", "message": "Calibration stopped"})
|
||||
|
||||
@@ -178,6 +178,9 @@ class RepeaterDaemon:
|
||||
|
||||
packet_record = {
|
||||
"timestamp": time.time(),
|
||||
"header": f"0x{packet.header:02X}" if hasattr(packet, "header") and packet.header is not None else None,
|
||||
"payload": packet.payload.hex() if hasattr(packet, "payload") and packet.payload else None,
|
||||
"payload_length": len(packet.payload) if hasattr(packet, "payload") and packet.payload else 0,
|
||||
"type": packet.get_payload_type(), # 0x09 for trace
|
||||
"route": packet.get_route_type(), # Should be direct (1)
|
||||
"length": len(packet.payload or b""),
|
||||
|
||||
@@ -97,6 +97,21 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Packet Details Dialog -->
|
||||
<div id="packet-dialog" class="dialog-overlay">
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-header">
|
||||
<h3>Packet Details</h3>
|
||||
<button class="dialog-close" onclick="closePacketDialog()">×</button>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div class="packet-details-grid">
|
||||
<!-- Content will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize charts
|
||||
let packetRateChart = null;
|
||||
@@ -381,7 +396,7 @@
|
||||
}
|
||||
|
||||
let mainRow = `
|
||||
<tr class="${hasDuplicates ? 'has-duplicates' : ''}">
|
||||
<tr class="${hasDuplicates ? 'has-duplicates' : ''} clickable-row" onclick="showPacketDetails(${JSON.stringify(pkt).replace(/"/g, '"')})">
|
||||
<td data-label="Time">${time}</td>
|
||||
<td data-label="Type"><span class="packet-type">${type}</span></td>
|
||||
<td data-label="Route"><span class="route-${route.toLowerCase().replace('_', '-')}">${route}</span></td>
|
||||
@@ -435,7 +450,7 @@
|
||||
}
|
||||
|
||||
return `
|
||||
<tr class="duplicate-row">
|
||||
<tr class="duplicate-row clickable-row" onclick="showPacketDetails(${JSON.stringify(dupe).replace(/"/g, '"')})">
|
||||
<td data-label="Time" style="padding-left: 30px;">↳ ${dupeTime}</td>
|
||||
<td data-label="Type"><span class="packet-type-dim">${type}</span></td>
|
||||
<td data-label="Route"><span class="route-${dupeRoute.toLowerCase().replace('_', '-')}">${dupeRoute}</span></td>
|
||||
@@ -506,6 +521,188 @@
|
||||
signalQualityChart.update();
|
||||
}
|
||||
|
||||
// Packet Details Dialog Functions
|
||||
function showPacketDetails(packet) {
|
||||
const dialog = document.getElementById('packet-dialog');
|
||||
const detailsGrid = dialog.querySelector('.packet-details-grid');
|
||||
|
||||
// Format timestamp
|
||||
const timestamp = new Date(packet.timestamp * 1000).toLocaleString();
|
||||
|
||||
// Format payload for display (with line breaks every 32 characters)
|
||||
const formatPayload = (payload) => {
|
||||
if (!payload) return 'None';
|
||||
return payload.match(/.{1,32}/g).join('\n');
|
||||
};
|
||||
|
||||
// Create the packet details HTML with sections
|
||||
detailsGrid.innerHTML = `
|
||||
<div class="detail-section">
|
||||
<h4>Basic Information</h4>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Timestamp:</span>
|
||||
<span class="detail-value">${timestamp}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Packet Hash:</span>
|
||||
<span class="detail-value monospace">${packet.packet_hash}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Header:</span>
|
||||
<span class="detail-value monospace">${packet.header || 'None'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Type:</span>
|
||||
<span class="detail-value">${packet.type} (${getTypeName(packet.type)})</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Route:</span>
|
||||
<span class="detail-value">${packet.route} (${getRouteName(packet.route)})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>Payload Data</h4>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Payload Length:</span>
|
||||
<span class="detail-value">${packet.payload_length || packet.length} bytes</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Payload (Hex):</span>
|
||||
<div class="detail-value payload-hex">${formatPayload(packet.payload)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>Path Information</h4>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Original Path:</span>
|
||||
<span class="detail-value monospace">${packet.original_path ? '[' + packet.original_path.join(', ') + ']' : 'None'}</span>
|
||||
</div>
|
||||
${packet.transmitted ? `
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Forwarded Path:</span>
|
||||
<span class="detail-value monospace">${packet.forwarded_path ? '[' + packet.forwarded_path.join(', ') + ']' : 'None'}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Source Hash:</span>
|
||||
<span class="detail-value monospace">${packet.src_hash || 'None'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Destination Hash:</span>
|
||||
<span class="detail-value monospace">${packet.dst_hash || 'None'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>Signal & Processing</h4>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">RSSI:</span>
|
||||
<span class="detail-value">${packet.rssi} dBm</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">SNR:</span>
|
||||
<span class="detail-value">${packet.snr.toFixed(2)} dB</span>
|
||||
</div>
|
||||
${packet.is_trace && packet.path_snr_details && packet.path_snr_details.length > 0 ? `
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Path SNRs:</span>
|
||||
<div class="detail-value">
|
||||
<div class="path-snrs-detail">
|
||||
${packet.path_snr_details.map((pathSnr, index) => {
|
||||
let snrClass = 'snr-poor';
|
||||
if (pathSnr.snr_db >= 10) snrClass = 'snr-excellent';
|
||||
else if (pathSnr.snr_db >= 5) snrClass = 'snr-good';
|
||||
else if (pathSnr.snr_db >= 0) snrClass = 'snr-fair';
|
||||
|
||||
return `<div class="path-snr-item-detail">
|
||||
<span class="hop-index">${index + 1}.</span>
|
||||
<span class="path-hash">${pathSnr.hash}</span>
|
||||
<span class="snr-value ${snrClass}">${pathSnr.snr_db.toFixed(1)}dB</span>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Score:</span>
|
||||
<span class="detail-value">${packet.score.toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">TX Delay:</span>
|
||||
<span class="detail-value">${packet.tx_delay_ms.toFixed(1)} ms</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Transmitted:</span>
|
||||
<span class="detail-value ${packet.transmitted ? 'status-tx' : 'status-dropped'}">${packet.transmitted ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Is Duplicate:</span>
|
||||
<span class="detail-value">${packet.is_duplicate ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
${packet.drop_reason ? `
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Drop Reason:</span>
|
||||
<span class="detail-value status-dropped">${packet.drop_reason}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
dialog.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closePacketDialog() {
|
||||
const dialog = document.getElementById('packet-dialog');
|
||||
dialog.style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
function getTypeName(type) {
|
||||
const typeNames = {
|
||||
0: 'REQ',
|
||||
1: 'RESPONSE',
|
||||
2: 'TXT_MSG',
|
||||
3: 'ACK',
|
||||
4: 'ADVERT',
|
||||
5: 'GRP_TXT',
|
||||
6: 'GRP_DATA',
|
||||
7: 'ANON_REQ',
|
||||
8: 'PATH',
|
||||
9: 'TRACE',
|
||||
15: 'RAW_CUSTOM'
|
||||
};
|
||||
return typeNames[type] || `Unknown (0x${type.toString(16).toUpperCase()})`;
|
||||
}
|
||||
|
||||
function getRouteName(route) {
|
||||
const routeNames = {
|
||||
0: 'TRANSPORT_FLOOD',
|
||||
1: 'FLOOD',
|
||||
2: 'DIRECT',
|
||||
3: 'TRANSPORT_DIRECT'
|
||||
};
|
||||
return routeNames[route] || `Unknown (${route})`;
|
||||
}
|
||||
|
||||
// Close dialog when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const dialog = document.getElementById('packet-dialog');
|
||||
if (event.target === dialog) {
|
||||
closePacketDialog();
|
||||
}
|
||||
});
|
||||
|
||||
// Close dialog on Escape key
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closePacketDialog();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Send Advert button
|
||||
function sendAdvert() {
|
||||
const btn = document.getElementById('send-advert-btn');
|
||||
@@ -797,6 +994,249 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Clickable row styling */
|
||||
.clickable-row {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.clickable-row:hover {
|
||||
background-color: rgba(78, 201, 176, 0.1);
|
||||
}
|
||||
|
||||
/* Dialog styling */
|
||||
.dialog-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: linear-gradient(135deg, #1e1e1e 0%, #252526 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(86, 156, 214, 0.3);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid rgba(86, 156, 214, 0.2);
|
||||
background: linear-gradient(90deg, rgba(86, 156, 214, 0.1) 0%, rgba(86, 156, 214, 0.05) 100%);
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
color: #569cd6;
|
||||
font-size: 1.4em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #d4d4d4;
|
||||
font-size: 1.8em;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
background-color: rgba(244, 135, 113, 0.2);
|
||||
color: #f48771;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(80vh - 80px);
|
||||
}
|
||||
|
||||
.packet-details-grid {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
border: 1px solid rgba(86, 156, 214, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #569cd6;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(86, 156, 214, 0.3);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: 16px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #dcdcaa;
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #d4d4d4;
|
||||
font-size: 0.9em;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-value.monospace {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: rgba(220, 220, 170, 0.1);
|
||||
color: #dcdcaa;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(220, 220, 170, 0.2);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.payload-hex {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: rgba(30, 30, 30, 0.8);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(86, 156, 214, 0.2);
|
||||
font-size: 0.8em;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
/* Path SNR details in dialog */
|
||||
.path-snrs-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(86, 156, 214, 0.2);
|
||||
}
|
||||
|
||||
.path-snr-item-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.path-snr-item-detail .hop-index {
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.path-snr-item-detail .path-hash {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
color: #dcdcaa;
|
||||
background: rgba(220, 220, 170, 0.1);
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
min-width: 32px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(220, 220, 170, 0.2);
|
||||
}
|
||||
|
||||
.path-snr-item-detail .snr-value {
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Mobile optimization */
|
||||
@media (max-width: 768px) {
|
||||
.dialog-content {
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 16px 20px 12px;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.payload-hex {
|
||||
font-size: 0.75em;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.path-snrs-detail {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.path-snr-item-detail .path-hash {
|
||||
font-size: 0.8em;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.path-snr-item-detail .snr-value {
|
||||
font-size: 0.8em;
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile optimization */
|
||||
@media (max-width: 768px) {
|
||||
/* Keep path info on same line, allow wrapping if needed */
|
||||
|
||||
Reference in New Issue
Block a user