mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-09-16 03:55:34 +00:00
701 lines
28 KiB
TypeScript
701 lines
28 KiB
TypeScript
import type { QueryResultRow } from 'pg';
|
|
|
|
type QueryFn = <T extends QueryResultRow = QueryResultRow>(
|
|
text: string,
|
|
params?: unknown[],
|
|
) => Promise<{ rows: T[] }>;
|
|
|
|
type OwnerRepositoryDeps = {
|
|
query: QueryFn;
|
|
};
|
|
|
|
export type OwnerRepository = ReturnType<typeof createOwnerRepository>;
|
|
|
|
export function createOwnerRepository(deps: OwnerRepositoryDeps) {
|
|
const { query } = deps;
|
|
|
|
async function fetchLastHopStrength(rxNodeIds: string[], allOwnerNodeIds: string[], since?: string) {
|
|
// $1 = rxNodeIds (the selected node to filter received packets)
|
|
// $2 = allOwnerNodeIds (all owned nodes to exclude from last-hop analysis)
|
|
// $3 = since (optional time filter)
|
|
const params: unknown[] = [rxNodeIds, allOwnerNodeIds];
|
|
const timeFilter = since
|
|
? `p.time >= $3::timestamptz`
|
|
: `p.time > NOW() - INTERVAL '7 days'`;
|
|
if (since) params.push(since);
|
|
|
|
return query<{
|
|
bucket: string;
|
|
last_hop_node_id: string | null;
|
|
last_hop_name: string;
|
|
resolution: 'direct' | 'resolved' | 'inferred' | 'unresolved';
|
|
avg_snr: number | null;
|
|
avg_rssi: number | null;
|
|
sample_count: number;
|
|
}>(
|
|
`WITH clashing_prefixes AS (
|
|
SELECT LEFT(node_id, 2) AS prefix
|
|
FROM node_identity_nodes
|
|
WHERE role IN (1, 3)
|
|
INTERSECT
|
|
SELECT LEFT(node_id, 2) AS prefix
|
|
FROM node_identity_nodes
|
|
WHERE role = 2
|
|
),
|
|
owner_packets AS (
|
|
SELECT DISTINCT ON (p.rx_node_id, COALESCE(p.packet_hash, p.time::text))
|
|
p.time,
|
|
p.packet_hash,
|
|
p.rx_node_id,
|
|
p.src_node_id,
|
|
p.hop_count,
|
|
p.rssi,
|
|
p.snr,
|
|
CASE
|
|
WHEN p.hop_count = 0
|
|
AND p.src_node_id IS NOT NULL
|
|
AND p.src_node_id != p.rx_node_id
|
|
THEN UPPER(LEFT(p.src_node_id, 2))
|
|
WHEN p.src_node_id = p.rx_node_id
|
|
AND COALESCE(array_length(p.path_hashes, 1), 0) >= 2
|
|
THEN UPPER(p.path_hashes[array_length(p.path_hashes, 1) - 1])
|
|
WHEN p.src_node_id IS NOT NULL
|
|
AND COALESCE(array_length(p.path_hashes, 1), 0) >= 1
|
|
THEN UPPER(p.path_hashes[array_length(p.path_hashes, 1)])
|
|
ELSE NULL
|
|
END AS receiver_side_hash
|
|
FROM node_identity_packets p
|
|
WHERE p.rx_node_id_raw IN (
|
|
SELECT meshcore_canonical_node_id(id)
|
|
FROM unnest($1::text[]) AS selected(id)
|
|
UNION
|
|
SELECT source_node_id
|
|
FROM node_identity_aliases
|
|
WHERE canonical_node_id = ANY(
|
|
SELECT meshcore_canonical_node_id(id)
|
|
FROM unnest($1::text[]) AS selected(id)
|
|
)
|
|
)
|
|
AND ${timeFilter}
|
|
AND p.packet_type NOT IN (8, 9)
|
|
AND p.src_node_id != p.rx_node_id
|
|
AND (p.snr IS NOT NULL OR p.rssi IS NOT NULL)
|
|
AND NOT (
|
|
p.hop_count <= 1
|
|
AND COALESCE(array_length(p.path_hashes, 1), 0) >= 1
|
|
AND UPPER(p.path_hashes[array_length(p.path_hashes, 1)]) IN (SELECT prefix FROM clashing_prefixes)
|
|
)
|
|
ORDER BY p.rx_node_id, COALESCE(p.packet_hash, p.time::text), p.time ASC
|
|
),
|
|
unique_receiver_targets AS (
|
|
SELECT DISTINCT rx_node_id, receiver_side_hash
|
|
FROM owner_packets
|
|
WHERE receiver_side_hash IS NOT NULL
|
|
AND hop_count IS NOT NULL
|
|
AND hop_count > 0
|
|
),
|
|
resolved_last_hop AS (
|
|
SELECT
|
|
uh.rx_node_id,
|
|
uh.receiver_side_hash,
|
|
n.node_id,
|
|
COALESCE(n.name, n.node_id) AS name,
|
|
COUNT(*) OVER (PARTITION BY uh.rx_node_id, uh.receiver_side_hash) AS match_count,
|
|
ROW_NUMBER() OVER (PARTITION BY uh.rx_node_id, uh.receiver_side_hash ORDER BY n.node_id) AS rn
|
|
FROM unique_receiver_targets uh
|
|
JOIN node_identity_nodes n
|
|
ON (n.role IS NULL OR n.role NOT IN (1, 3))
|
|
AND UPPER(n.node_id) LIKE uh.receiver_side_hash || '%'
|
|
),
|
|
inferred_last_hop AS (
|
|
SELECT
|
|
uh.rx_node_id,
|
|
uh.receiver_side_hash,
|
|
n.node_id,
|
|
COALESCE(n.name, n.node_id) AS name,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY uh.rx_node_id, uh.receiver_side_hash
|
|
ORDER BY
|
|
CASE
|
|
WHEN nl.force_viable = true OR nl.itm_viable = true THEN 0
|
|
WHEN nl.itm_path_loss_db IS NOT NULL AND nl.itm_path_loss_db <= 137.5 THEN 1
|
|
ELSE 2
|
|
END,
|
|
((COALESCE(n.lat, 0) - COALESCE(rx.lat, 0)) * (COALESCE(n.lat, 0) - COALESCE(rx.lat, 0)))
|
|
+ ((COALESCE(n.lon, 0) - COALESCE(rx.lon, 0)) * (COALESCE(n.lon, 0) - COALESCE(rx.lon, 0))),
|
|
n.node_id
|
|
) AS rn
|
|
FROM unique_receiver_targets uh
|
|
JOIN node_identity_nodes rx ON rx.node_id = uh.rx_node_id
|
|
JOIN node_identity_nodes n
|
|
ON (n.role IS NULL OR n.role NOT IN (1, 3))
|
|
AND UPPER(LEFT(n.node_id, 2)) = UPPER(LEFT(uh.receiver_side_hash, 2))
|
|
LEFT JOIN node_identity_links nl
|
|
ON (
|
|
(nl.node_a_id = uh.rx_node_id AND nl.node_b_id = n.node_id)
|
|
OR (nl.node_b_id = uh.rx_node_id AND nl.node_a_id = n.node_id)
|
|
)
|
|
WHERE (
|
|
nl.force_viable = true
|
|
OR nl.itm_viable = true
|
|
OR (nl.itm_path_loss_db IS NOT NULL AND nl.itm_path_loss_db <= 137.5)
|
|
)
|
|
),
|
|
classified AS (
|
|
SELECT
|
|
time_bucket('1 hour', op.time)::text AS bucket,
|
|
CASE
|
|
WHEN op.hop_count = 0
|
|
AND op.src_node_id IS NOT NULL
|
|
AND NOT (op.src_node_id = ANY(
|
|
SELECT meshcore_canonical_node_id(id)
|
|
FROM unnest($2::text[]) AS owned(id)
|
|
))
|
|
AND src.node_id IS NOT NULL
|
|
AND (src.role IS NULL OR src.role NOT IN (1, 3))
|
|
THEN op.src_node_id
|
|
WHEN rl.match_count = 1 AND rl.rn = 1 THEN rl.node_id
|
|
WHEN ilh.rn = 1 THEN ilh.node_id
|
|
ELSE NULL
|
|
END AS last_hop_node_id,
|
|
CASE
|
|
WHEN op.hop_count = 0
|
|
AND op.src_node_id IS NOT NULL
|
|
AND NOT (op.src_node_id = ANY(
|
|
SELECT meshcore_canonical_node_id(id)
|
|
FROM unnest($2::text[]) AS owned(id)
|
|
))
|
|
AND src.node_id IS NOT NULL
|
|
AND (src.role IS NULL OR src.role NOT IN (1, 3))
|
|
THEN COALESCE(src.name, op.src_node_id)
|
|
WHEN rl.match_count = 1 AND rl.rn = 1 THEN rl.name
|
|
WHEN ilh.rn = 1 THEN ilh.name
|
|
ELSE 'Unresolved'
|
|
END AS last_hop_name,
|
|
CASE
|
|
WHEN op.hop_count = 0
|
|
AND op.src_node_id IS NOT NULL
|
|
AND NOT (op.src_node_id = ANY(
|
|
SELECT meshcore_canonical_node_id(id)
|
|
FROM unnest($2::text[]) AS owned(id)
|
|
))
|
|
AND src.node_id IS NOT NULL
|
|
AND (src.role IS NULL OR src.role NOT IN (1, 3))
|
|
THEN 'direct'
|
|
WHEN rl.match_count = 1 AND rl.rn = 1 THEN 'resolved'
|
|
WHEN ilh.rn = 1 THEN 'inferred'
|
|
ELSE 'unresolved'
|
|
END AS resolution,
|
|
op.snr,
|
|
op.rssi
|
|
FROM owner_packets op
|
|
LEFT JOIN node_identity_nodes src ON src.node_id = op.src_node_id
|
|
LEFT JOIN resolved_last_hop rl
|
|
ON rl.rx_node_id = op.rx_node_id
|
|
AND rl.receiver_side_hash = op.receiver_side_hash
|
|
AND rl.rn = 1
|
|
LEFT JOIN inferred_last_hop ilh
|
|
ON ilh.rx_node_id = op.rx_node_id
|
|
AND ilh.receiver_side_hash = op.receiver_side_hash
|
|
AND ilh.rn = 1
|
|
)
|
|
SELECT
|
|
bucket,
|
|
last_hop_node_id,
|
|
last_hop_name,
|
|
resolution,
|
|
AVG(snr)::double precision AS avg_snr,
|
|
AVG(rssi)::double precision AS avg_rssi,
|
|
COUNT(*)::int AS sample_count
|
|
FROM classified
|
|
GROUP BY bucket, last_hop_node_id, last_hop_name, resolution
|
|
ORDER BY bucket ASC, sample_count DESC, last_hop_name ASC`,
|
|
params,
|
|
);
|
|
}
|
|
|
|
async function fetchOwnerLiveData(selectedNodeId: string) {
|
|
const [
|
|
ownerNodeResult,
|
|
incomingResult,
|
|
packetResult,
|
|
heardByResult,
|
|
linkHealthResult,
|
|
advertTrendResult,
|
|
telemetryResult,
|
|
heardNeighborsResult,
|
|
packetsSentResult,
|
|
packetsReceivedResult,
|
|
] = await Promise.all([
|
|
query<{
|
|
node_id: string;
|
|
name: string | null;
|
|
network: string;
|
|
iata: string | null;
|
|
advert_count: number | null;
|
|
last_seen: string | null;
|
|
lat: number | null;
|
|
lon: number | null;
|
|
role: number | null;
|
|
members: string[];
|
|
}>(
|
|
`SELECT node_id, name, network, iata, advert_count, last_seen, lat, lon, role,
|
|
identity_source_ids AS members
|
|
FROM node_identity_nodes
|
|
WHERE node_id = meshcore_canonical_node_id($1)
|
|
LIMIT 1`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{
|
|
node_id: string;
|
|
name: string | null;
|
|
network: string | null;
|
|
iata: string | null;
|
|
lat: number | null;
|
|
lon: number | null;
|
|
packets_24h: number;
|
|
last_seen: string | null;
|
|
}>(
|
|
`SELECT
|
|
p.src_node_id AS node_id,
|
|
n.name,
|
|
n.network,
|
|
n.iata,
|
|
n.lat,
|
|
n.lon,
|
|
COUNT(*)::int AS packets_24h,
|
|
MAX(p.time)::text AS last_seen
|
|
FROM node_identity_packets p
|
|
LEFT JOIN node_identity_nodes n ON n.node_id = p.src_node_id
|
|
WHERE p.rx_node_id_raw IN (
|
|
SELECT meshcore_canonical_node_id($1)
|
|
UNION
|
|
SELECT source_node_id FROM node_identity_aliases
|
|
WHERE canonical_node_id = meshcore_canonical_node_id($1)
|
|
)
|
|
AND p.hop_count = 0
|
|
AND p.src_node_id IS NOT NULL
|
|
AND p.src_node_id <> meshcore_canonical_node_id($1)
|
|
AND p.time > NOW() - INTERVAL '24 hours'
|
|
GROUP BY p.src_node_id, n.name, n.network, n.iata, n.lat, n.lon
|
|
ORDER BY packets_24h DESC
|
|
LIMIT 250`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{
|
|
time: string;
|
|
packet_type: number | null;
|
|
route_type: number | null;
|
|
hop_count: number | null;
|
|
packet_hash: string | null;
|
|
src_node_id: string | null;
|
|
src_node_name: string | null;
|
|
sender: string | null;
|
|
body: string | null;
|
|
}>(
|
|
`WITH recent AS (
|
|
SELECT
|
|
p.time,
|
|
p.packet_type,
|
|
p.route_type,
|
|
p.hop_count,
|
|
p.packet_hash,
|
|
p.src_node_id,
|
|
p.payload
|
|
FROM node_identity_packets p
|
|
WHERE p.rx_node_id_raw IN (
|
|
SELECT meshcore_canonical_node_id($1)
|
|
UNION
|
|
SELECT source_node_id FROM node_identity_aliases
|
|
WHERE canonical_node_id = meshcore_canonical_node_id($1)
|
|
)
|
|
AND p.time > NOW() - INTERVAL '30 days'
|
|
ORDER BY p.time DESC
|
|
LIMIT 5000
|
|
),
|
|
ranked AS (
|
|
SELECT
|
|
recent.time,
|
|
recent.packet_type,
|
|
recent.route_type,
|
|
recent.hop_count,
|
|
recent.packet_hash,
|
|
recent.src_node_id,
|
|
recent.payload,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY COALESCE(
|
|
recent.packet_hash,
|
|
CONCAT_WS(':',
|
|
COALESCE(recent.src_node_id, ''),
|
|
COALESCE(recent.packet_type::text, ''),
|
|
COALESCE(recent.route_type::text, ''),
|
|
COALESCE(recent.hop_count::text, ''),
|
|
COALESCE(recent.payload->'decrypted'->>'sender', ''),
|
|
COALESCE(recent.payload->'decrypted'->>'text', ''),
|
|
DATE_TRUNC('second', recent.time)::text
|
|
)
|
|
)
|
|
ORDER BY recent.time DESC
|
|
) AS rn
|
|
FROM recent
|
|
)
|
|
SELECT
|
|
r.time::text AS time,
|
|
r.packet_type,
|
|
r.route_type,
|
|
r.hop_count,
|
|
r.packet_hash,
|
|
r.src_node_id,
|
|
src.name AS src_node_name,
|
|
COALESCE(r.payload->'decrypted'->>'sender', src.name, r.src_node_id) AS sender,
|
|
COALESCE(
|
|
r.payload->'decrypted'->>'text',
|
|
r.payload->'decrypted'->>'message',
|
|
r.payload->'decrypted'->>'body',
|
|
r.payload->'decoded'->>'text',
|
|
r.payload->>'message'
|
|
) AS body
|
|
FROM ranked r
|
|
LEFT JOIN node_identity_nodes src ON src.node_id = r.src_node_id
|
|
WHERE r.rn = 1
|
|
ORDER BY r.time DESC
|
|
LIMIT 9`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{
|
|
node_id: string;
|
|
name: string | null;
|
|
network: string | null;
|
|
iata: string | null;
|
|
lat: number | null;
|
|
lon: number | null;
|
|
packets_24h: number;
|
|
packets_7d: number;
|
|
last_seen: string | null;
|
|
best_hops: number | null;
|
|
}>(
|
|
`SELECT
|
|
p.rx_node_id AS node_id,
|
|
n.name,
|
|
n.network,
|
|
n.iata,
|
|
n.lat,
|
|
n.lon,
|
|
COUNT(DISTINCT CASE WHEN p.time > NOW() - INTERVAL '24 hours' THEN p.packet_hash END)::int AS packets_24h,
|
|
COUNT(DISTINCT p.packet_hash)::int AS packets_7d,
|
|
MAX(p.time)::text AS last_seen,
|
|
MIN(p.hop_count) AS best_hops
|
|
FROM node_identity_packets p
|
|
LEFT JOIN node_identity_nodes n ON n.node_id = p.rx_node_id
|
|
WHERE p.src_node_id_raw IN (
|
|
SELECT meshcore_canonical_node_id($1)
|
|
UNION
|
|
SELECT source_node_id FROM node_identity_aliases
|
|
WHERE canonical_node_id = meshcore_canonical_node_id($1)
|
|
)
|
|
AND p.rx_node_id IS NOT NULL
|
|
AND p.rx_node_id <> meshcore_canonical_node_id($1)
|
|
AND p.time > NOW() - INTERVAL '7 days'
|
|
GROUP BY p.rx_node_id, n.name, n.network, n.iata, n.lat, n.lon
|
|
ORDER BY packets_24h DESC, packets_7d DESC, last_seen DESC
|
|
LIMIT 20`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{
|
|
peer_node_id: string;
|
|
peer_name: string | null;
|
|
peer_network: string | null;
|
|
owner_to_peer: number;
|
|
peer_to_owner: number;
|
|
observed_count: number;
|
|
itm_path_loss_db: number | null;
|
|
itm_viable: boolean | null;
|
|
force_viable: boolean;
|
|
last_observed: string | null;
|
|
}>(
|
|
`SELECT
|
|
CASE WHEN nl.node_a_id = meshcore_canonical_node_id($1)
|
|
THEN nl.node_b_id ELSE nl.node_a_id END AS peer_node_id,
|
|
peer.name AS peer_name,
|
|
peer.network AS peer_network,
|
|
CASE WHEN nl.node_a_id = meshcore_canonical_node_id($1)
|
|
THEN nl.count_a_to_b ELSE nl.count_b_to_a END AS owner_to_peer,
|
|
CASE WHEN nl.node_a_id = meshcore_canonical_node_id($1)
|
|
THEN nl.count_b_to_a ELSE nl.count_a_to_b END AS peer_to_owner,
|
|
nl.observed_count,
|
|
nl.itm_path_loss_db,
|
|
nl.itm_viable,
|
|
nl.force_viable,
|
|
nl.last_observed::text AS last_observed
|
|
FROM node_identity_links nl
|
|
JOIN node_identity_nodes peer ON peer.node_id = CASE
|
|
WHEN nl.node_a_id = meshcore_canonical_node_id($1) THEN nl.node_b_id
|
|
ELSE nl.node_a_id
|
|
END
|
|
WHERE (nl.node_a_id = meshcore_canonical_node_id($1)
|
|
OR nl.node_b_id = meshcore_canonical_node_id($1))
|
|
AND (
|
|
nl.force_viable = true
|
|
OR nl.itm_viable = true
|
|
OR (nl.itm_path_loss_db IS NOT NULL AND nl.itm_path_loss_db <= 137.5)
|
|
)
|
|
ORDER BY
|
|
COALESCE(nl.itm_viable, false) DESC,
|
|
nl.force_viable DESC,
|
|
nl.observed_count DESC,
|
|
nl.itm_path_loss_db ASC NULLS LAST
|
|
LIMIT 12`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{ bucket: string; adverts: number }>(
|
|
`SELECT
|
|
time_bucket('1 hour', time)::text AS bucket,
|
|
COUNT(DISTINCT packet_hash)::int AS adverts
|
|
FROM node_identity_packets
|
|
WHERE src_node_id_raw IN (
|
|
SELECT meshcore_canonical_node_id($1)
|
|
UNION
|
|
SELECT source_node_id FROM node_identity_aliases
|
|
WHERE canonical_node_id = meshcore_canonical_node_id($1)
|
|
)
|
|
AND packet_type = 4
|
|
AND time > NOW() - INTERVAL '24 hours'
|
|
GROUP BY bucket
|
|
ORDER BY bucket`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{
|
|
time: string;
|
|
battery_mv: number | null;
|
|
uptime_secs: string | null;
|
|
tx_air_secs: string | null;
|
|
rx_air_secs: string | null;
|
|
channel_utilization: number | null;
|
|
air_util_tx: number | null;
|
|
solar_mv: number | null;
|
|
board_temp_c: number | null;
|
|
wifi_rssi: number | null;
|
|
wifi_ssid: string | null;
|
|
wifi_uptime_ms: number | null;
|
|
ntp_synced: boolean | null;
|
|
ntp_sync_age_ms: number | null;
|
|
boot_count: number | null;
|
|
reset_reason: string | null;
|
|
max_loop_ms: number | null;
|
|
max_loop_at_ms: number | null;
|
|
nodes_heard_24h: number | null;
|
|
air_util_rx: number | null;
|
|
last_rx_rssi: number | null;
|
|
last_rx_snr: number | null;
|
|
tx_power_dbm: number | null;
|
|
config_version: string | null;
|
|
config_crc32: string | null;
|
|
fs_free_bytes: number | null;
|
|
fs_total_bytes: number | null;
|
|
nvs_free_entries: number | null;
|
|
channel_id: number | null;
|
|
git_commit: string | null;
|
|
boot_epoch: number | null;
|
|
mqtt_broker_uri: string | null;
|
|
mqtt_broker_username: string | null;
|
|
mqtt_uptime_ms: number | null;
|
|
mqtt_reconnect_attempts_1h: number | null;
|
|
mqtt_session_status_publishes: number | null;
|
|
mqtt_session_packet_publishes: number | null;
|
|
mqtt_last_offline_epoch: number | null;
|
|
uptime_ms: number | null;
|
|
rx_publish_calls: number | null;
|
|
tx_publish_calls: number | null;
|
|
}>(
|
|
`SELECT
|
|
time::text AS time,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'battery_mv') = 'number' THEN (stats->>'battery_mv')::double precision
|
|
ELSE battery_mv::double precision
|
|
END AS battery_mv,
|
|
uptime_secs::text AS uptime_secs,
|
|
tx_air_secs::text AS tx_air_secs,
|
|
rx_air_secs::text AS rx_air_secs,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'channel_utilization') = 'number' THEN (stats->>'channel_utilization')::double precision
|
|
ELSE channel_utilization
|
|
END AS channel_utilization,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'air_util_tx') = 'number' THEN (stats->>'air_util_tx')::double precision
|
|
ELSE air_util_tx
|
|
END AS air_util_tx,
|
|
CASE WHEN jsonb_typeof(stats->'solar_mv') = 'number' THEN (stats->>'solar_mv')::double precision ELSE NULL END AS solar_mv,
|
|
CASE WHEN jsonb_typeof(stats->'board_temp_c') = 'number' THEN (stats->>'board_temp_c')::double precision ELSE NULL END AS board_temp_c,
|
|
CASE WHEN jsonb_typeof(stats->'wifi_rssi') = 'number' THEN (stats->>'wifi_rssi')::double precision ELSE NULL END AS wifi_rssi,
|
|
CASE WHEN jsonb_typeof(stats->'wifi_ssid') = 'string' THEN stats->>'wifi_ssid' ELSE NULL END AS wifi_ssid,
|
|
CASE WHEN jsonb_typeof(stats->'wifi_uptime_ms') = 'number' THEN (stats->>'wifi_uptime_ms')::double precision ELSE NULL END AS wifi_uptime_ms,
|
|
CASE WHEN jsonb_typeof(stats->'ntp_synced') = 'boolean' THEN (stats->>'ntp_synced')::boolean ELSE NULL END AS ntp_synced,
|
|
CASE WHEN jsonb_typeof(stats->'ntp_sync_age_ms') = 'number' THEN (stats->>'ntp_sync_age_ms')::double precision ELSE NULL END AS ntp_sync_age_ms,
|
|
CASE WHEN jsonb_typeof(stats->'boot_count') = 'number' THEN (stats->>'boot_count')::double precision ELSE NULL END AS boot_count,
|
|
CASE WHEN jsonb_typeof(stats->'reset_reason') = 'string' THEN stats->>'reset_reason' ELSE NULL END AS reset_reason,
|
|
CASE WHEN jsonb_typeof(stats->'max_loop_ms') = 'number' THEN (stats->>'max_loop_ms')::double precision ELSE NULL END AS max_loop_ms,
|
|
CASE WHEN jsonb_typeof(stats->'max_loop_at_ms') = 'number' THEN (stats->>'max_loop_at_ms')::double precision ELSE NULL END AS max_loop_at_ms,
|
|
CASE WHEN jsonb_typeof(stats->'nodes_heard_24h') = 'number' THEN (stats->>'nodes_heard_24h')::double precision ELSE NULL END AS nodes_heard_24h,
|
|
CASE WHEN jsonb_typeof(stats->'air_util_rx') = 'number' THEN (stats->>'air_util_rx')::double precision ELSE NULL END AS air_util_rx,
|
|
CASE WHEN jsonb_typeof(stats->'last_rx_rssi') = 'number' THEN (stats->>'last_rx_rssi')::double precision ELSE NULL END AS last_rx_rssi,
|
|
CASE WHEN jsonb_typeof(stats->'last_rx_snr') = 'number' THEN (stats->>'last_rx_snr')::double precision ELSE NULL END AS last_rx_snr,
|
|
CASE WHEN jsonb_typeof(stats->'tx_power_dbm') = 'number' THEN (stats->>'tx_power_dbm')::double precision ELSE NULL END AS tx_power_dbm,
|
|
CASE WHEN jsonb_typeof(stats->'config_version') = 'string' THEN stats->>'config_version' ELSE NULL END AS config_version,
|
|
CASE WHEN jsonb_typeof(stats->'config_crc32') IN ('string', 'number') THEN stats->>'config_crc32' ELSE NULL END AS config_crc32,
|
|
CASE WHEN jsonb_typeof(stats->'fs_free_bytes') = 'number' THEN (stats->>'fs_free_bytes')::double precision ELSE NULL END AS fs_free_bytes,
|
|
CASE WHEN jsonb_typeof(stats->'fs_total_bytes') = 'number' THEN (stats->>'fs_total_bytes')::double precision ELSE NULL END AS fs_total_bytes,
|
|
CASE WHEN jsonb_typeof(stats->'nvs_free_entries') = 'number' THEN (stats->>'nvs_free_entries')::double precision ELSE NULL END AS nvs_free_entries,
|
|
CASE WHEN jsonb_typeof(stats->'channel_id') = 'number' THEN (stats->>'channel_id')::double precision ELSE NULL END AS channel_id,
|
|
CASE WHEN jsonb_typeof(stats->'git_commit') = 'string' THEN stats->>'git_commit' ELSE NULL END AS git_commit,
|
|
CASE WHEN jsonb_typeof(stats->'boot_epoch') = 'number' THEN (stats->>'boot_epoch')::double precision ELSE NULL END AS boot_epoch,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'mqtt') = 'object' AND jsonb_typeof(stats->'mqtt'->'broker_uri') = 'string'
|
|
THEN stats->'mqtt'->>'broker_uri'
|
|
ELSE NULL
|
|
END AS mqtt_broker_uri,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'mqtt') = 'object' AND jsonb_typeof(stats->'mqtt'->'broker_username') = 'string'
|
|
THEN stats->'mqtt'->>'broker_username'
|
|
ELSE NULL
|
|
END AS mqtt_broker_username,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'mqtt') = 'object' AND jsonb_typeof(stats->'mqtt'->'uptime_ms') = 'number'
|
|
THEN (stats->'mqtt'->>'uptime_ms')::double precision
|
|
ELSE NULL
|
|
END AS mqtt_uptime_ms,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'mqtt') = 'object' AND jsonb_typeof(stats->'mqtt'->'reconnect_attempts_1h') = 'number'
|
|
THEN (stats->'mqtt'->>'reconnect_attempts_1h')::double precision
|
|
ELSE NULL
|
|
END AS mqtt_reconnect_attempts_1h,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'mqtt') = 'object' AND jsonb_typeof(stats->'mqtt'->'session_status_publishes') = 'number'
|
|
THEN (stats->'mqtt'->>'session_status_publishes')::double precision
|
|
ELSE NULL
|
|
END AS mqtt_session_status_publishes,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'mqtt') = 'object' AND jsonb_typeof(stats->'mqtt'->'session_packet_publishes') = 'number'
|
|
THEN (stats->'mqtt'->>'session_packet_publishes')::double precision
|
|
ELSE NULL
|
|
END AS mqtt_session_packet_publishes,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'mqtt') = 'object' AND jsonb_typeof(stats->'mqtt'->'last_offline_epoch') = 'number'
|
|
THEN (stats->'mqtt'->>'last_offline_epoch')::double precision
|
|
ELSE NULL
|
|
END AS mqtt_last_offline_epoch,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'uptime_ms') = 'number' THEN (stats->>'uptime_ms')::double precision
|
|
ELSE NULL
|
|
END AS uptime_ms,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'rx_publish_calls') = 'number' THEN (stats->>'rx_publish_calls')::double precision
|
|
ELSE NULL
|
|
END AS rx_publish_calls,
|
|
CASE
|
|
WHEN jsonb_typeof(stats->'tx_publish_calls') = 'number' THEN (stats->>'tx_publish_calls')::double precision
|
|
ELSE NULL
|
|
END AS tx_publish_calls
|
|
FROM node_identity_status_samples
|
|
WHERE node_id = meshcore_canonical_node_id($1)
|
|
AND network = COALESCE(
|
|
(SELECT network FROM node_identity_nodes
|
|
WHERE node_id = meshcore_canonical_node_id($1)
|
|
LIMIT 1),
|
|
'ukmesh'
|
|
)
|
|
AND time > NOW() - INTERVAL '24 hours'
|
|
ORDER BY time ASC`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{
|
|
id: string;
|
|
rssi: number | null;
|
|
snr: number | null;
|
|
last_seen: string | null;
|
|
sample_time: string;
|
|
}>(
|
|
`WITH latest AS (
|
|
SELECT time, neighbors
|
|
FROM node_neighbor_samples
|
|
WHERE meshcore_canonical_node_id(node_id) = meshcore_canonical_node_id($1)
|
|
AND network = COALESCE(
|
|
(SELECT network FROM node_identity_nodes
|
|
WHERE node_id = meshcore_canonical_node_id($1)
|
|
LIMIT 1),
|
|
'ukmesh'
|
|
)
|
|
ORDER BY time DESC
|
|
LIMIT 1
|
|
)
|
|
SELECT
|
|
COALESCE(item->>'id', item->>'node_id', item->>'pubkey', item->>'public_key') AS id,
|
|
CASE
|
|
WHEN jsonb_typeof(item->'rssi') = 'number' THEN (item->>'rssi')::double precision
|
|
WHEN jsonb_typeof(item->'RSSI') = 'number' THEN (item->>'RSSI')::double precision
|
|
ELSE NULL
|
|
END AS rssi,
|
|
CASE
|
|
WHEN jsonb_typeof(item->'snr') = 'number' THEN (item->>'snr')::double precision
|
|
WHEN jsonb_typeof(item->'SNR') = 'number' THEN (item->>'SNR')::double precision
|
|
ELSE NULL
|
|
END AS snr,
|
|
COALESCE(
|
|
CASE WHEN jsonb_typeof(item->'last_seen') IN ('string', 'number') THEN item->>'last_seen' ELSE NULL END,
|
|
CASE WHEN jsonb_typeof(item->'lastSeen') IN ('string', 'number') THEN item->>'lastSeen' ELSE NULL END
|
|
) AS last_seen,
|
|
latest.time::text AS sample_time
|
|
FROM latest
|
|
CROSS JOIN LATERAL jsonb_array_elements(
|
|
CASE WHEN jsonb_typeof(latest.neighbors) = 'array' THEN latest.neighbors ELSE '[]'::jsonb END
|
|
) AS neighbor(item)
|
|
WHERE COALESCE(item->>'id', item->>'node_id', item->>'pubkey', item->>'public_key') IS NOT NULL
|
|
ORDER BY last_seen DESC NULLS LAST
|
|
LIMIT 32`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{ packets_24h: number }>(
|
|
`SELECT COUNT(*)::int AS packets_24h
|
|
FROM node_identity_packets
|
|
WHERE src_node_id_raw IN (
|
|
SELECT meshcore_canonical_node_id($1)
|
|
UNION
|
|
SELECT source_node_id FROM node_identity_aliases
|
|
WHERE canonical_node_id = meshcore_canonical_node_id($1)
|
|
)
|
|
AND time > NOW() - INTERVAL '24 hours'`,
|
|
[selectedNodeId],
|
|
),
|
|
query<{ packets_24h: number }>(
|
|
`SELECT COUNT(*)::int AS packets_24h
|
|
FROM node_identity_packets
|
|
WHERE rx_node_id_raw IN (
|
|
SELECT meshcore_canonical_node_id($1)
|
|
UNION
|
|
SELECT source_node_id FROM node_identity_aliases
|
|
WHERE canonical_node_id = meshcore_canonical_node_id($1)
|
|
)
|
|
AND time > NOW() - INTERVAL '24 hours'`,
|
|
[selectedNodeId],
|
|
),
|
|
]);
|
|
|
|
return {
|
|
ownerNodeResult,
|
|
incomingResult,
|
|
packetResult,
|
|
heardByResult,
|
|
linkHealthResult,
|
|
advertTrendResult,
|
|
telemetryResult,
|
|
heardNeighborsResult,
|
|
packetsSentResult,
|
|
packetsReceivedResult,
|
|
};
|
|
}
|
|
|
|
return {
|
|
fetchOwnerLiveData,
|
|
fetchLastHopStrength,
|
|
};
|
|
}
|