Includes: - Backend services: ingestion (:8001), weather API (:8002), gateway (:8003), billing (:8004) with BTCPay integration - Shared asyncpg pool, TimescaleDB hypertable, Redis, Mosquitto MQTT - React frontend: Dashboard (MapLibre) and Messaging (chat UI) - Bridge daemon for Pi + Meshtastic (Serial/TCP T-Deck support) - Production Docker Compose, Nginx reverse proxy, ops scripts - DEPLOY.md with step-by-step deployment guide
48 lines
2.2 KiB
JavaScript
48 lines
2.2 KiB
JavaScript
import { useLatest } from '../hooks/useApi'
|
|
|
|
function fmt(v, unit) {
|
|
if (v == null) return '—'
|
|
return `${Number(v).toFixed(1)} ${unit}`
|
|
}
|
|
|
|
function timeAgo(iso) {
|
|
if (!iso) return '—'
|
|
const diff = Date.now() - new Date(iso).getTime()
|
|
const sec = Math.floor(diff / 1000)
|
|
if (sec < 60) return `${sec}s ago`
|
|
const min = Math.floor(sec / 60)
|
|
if (min < 60) return `${min}m ago`
|
|
const hr = Math.floor(min / 60)
|
|
return `${hr}h ago`
|
|
}
|
|
|
|
export default function NodePopup({ node }) {
|
|
const { reading } = useLatest(node.mesh_node_id)
|
|
|
|
return (
|
|
<div style={{ minWidth: '220px' }}>
|
|
<div style={{ fontWeight: 600, fontSize: '1rem', marginBottom: '0.25rem', color: '#38bdf8' }}>
|
|
{node.name || node.mesh_node_id}
|
|
</div>
|
|
<div style={{ fontSize: '0.75rem', color: '#94a3b8', marginBottom: '0.5rem' }}>
|
|
{node.mesh_node_id} · {timeAgo(node.last_seen)}
|
|
</div>
|
|
|
|
{reading ? (
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.35rem 0.75rem', fontSize: '0.875rem' }}>
|
|
<div><span style={{ color: '#94a3b8' }}>Temp</span><br/><strong>{fmt(reading.temperature_c, '°C')}</strong></div>
|
|
<div><span style={{ color: '#94a3b8' }}>Humidity</span><br/><strong>{fmt(reading.humidity_percent, '%')}</strong></div>
|
|
<div><span style={{ color: '#94a3b8' }}>Pressure</span><br/><strong>{fmt(reading.pressure_pa / 100, 'hPa')}</strong></div>
|
|
<div><span style={{ color: '#94a3b8' }}>Wind</span><br/><strong>{fmt(reading.wind_speed_ms, 'm/s')} {reading.wind_direction != null ? `@ ${reading.wind_direction}°` : ''}</strong></div>
|
|
<div><span style={{ color: '#94a3b8' }}>PM2.5</span><br/><strong>{fmt(reading.pm25_ugm3, 'µg/m³')}</strong></div>
|
|
<div><span style={{ color: '#94a3b8' }}>PM10</span><br/><strong>{fmt(reading.pm10_ugm3, 'µg/m³')}</strong></div>
|
|
<div><span style={{ color: '#94a3b8' }}>Battery</span><br/><strong>{fmt(reading.battery_voltage, 'V')}</strong></div>
|
|
<div><span style={{ color: '#94a3b8' }}>Solar</span><br/><strong>{fmt(reading.solar_voltage, 'V')}</strong></div>
|
|
</div>
|
|
) : (
|
|
<div style={{ fontSize: '0.875rem', color: '#94a3b8' }}>No recent readings available.</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|