分表
This commit is contained in:
@@ -1,60 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { getHealth, getNodes, getPositions, getTextMessages } from './api'
|
||||
import { getHealth, getMapReports, getNodeInfo, getPositions, getTextMessages } from './api'
|
||||
import ChatPanel from './components/ChatPanel.vue'
|
||||
import MeshMap from './components/MeshMap.vue'
|
||||
import NodeListPanel from './components/NodeListPanel.vue'
|
||||
import type { HealthStatus, MapNode, NodeInfoById, NodeInfoMap, PositionRecord, TextMessage } from './types'
|
||||
import type { HealthStatus, MapNode, MapReport, NodeInfo, NodeInfoById, PositionRecord, TextMessage } from './types'
|
||||
|
||||
const loading = ref(true)
|
||||
const nodePageLoading = ref(false)
|
||||
const error = ref('')
|
||||
const selectedNodeId = ref<string | null>(null)
|
||||
const health = ref<HealthStatus | null>(null)
|
||||
const mapNodeSource = ref<NodeInfoMap[]>([])
|
||||
const pagedNodes = ref<NodeInfoMap[]>([])
|
||||
const nodeInfoSource = ref<NodeInfo[]>([])
|
||||
const mapReportSource = ref<MapReport[]>([])
|
||||
const pagedNodeInfo = ref<NodeInfo[]>([])
|
||||
const nodePage = ref(1)
|
||||
const nodePageSize = 25
|
||||
const nodeTotal = ref(0)
|
||||
const messages = ref<TextMessage[]>([])
|
||||
const chatPageSize = 20
|
||||
const chatLoadingOlder = ref(false)
|
||||
const chatHasMore = ref(true)
|
||||
const chatInitialized = ref(false)
|
||||
const positions = ref<PositionRecord[]>([])
|
||||
let refreshTimer: number | undefined
|
||||
|
||||
const nodesById = computed<NodeInfoById>(() => {
|
||||
const map = new Map<string, NodeInfoMap>()
|
||||
for (const node of mapNodeSource.value) {
|
||||
const map = new Map<string, NodeInfo>()
|
||||
for (const node of nodeInfoSource.value) {
|
||||
map.set(node.node_id, node)
|
||||
}
|
||||
for (const node of pagedNodes.value) {
|
||||
for (const node of pagedNodeInfo.value) {
|
||||
map.set(node.node_id, node)
|
||||
}
|
||||
return Object.fromEntries(map)
|
||||
})
|
||||
|
||||
const mapNodes = computed<MapNode[]>(() => {
|
||||
return mapNodeSource.value
|
||||
.filter((node) => node.latitude != null && node.longitude != null)
|
||||
.map((node) => ({
|
||||
node_id: node.node_id,
|
||||
label: node.short_name || node.node_id,
|
||||
latitude: node.latitude as number,
|
||||
longitude: node.longitude as number,
|
||||
altitude: node.altitude,
|
||||
source: 'node',
|
||||
updated_at: node.updated_at,
|
||||
node,
|
||||
latest_position: null,
|
||||
}))
|
||||
return mapReportSource.value
|
||||
.filter((report) => report.latitude != null && report.longitude != null)
|
||||
.map((report) => {
|
||||
const nodeinfo = nodesById.value[report.node_id] ?? null
|
||||
return {
|
||||
node_id: report.node_id,
|
||||
label: report.short_name || report.long_name || nodeinfo?.short_name || nodeinfo?.long_name || report.node_id,
|
||||
latitude: report.latitude as number,
|
||||
longitude: report.longitude as number,
|
||||
altitude: report.altitude,
|
||||
source: 'map_report',
|
||||
updated_at: report.updated_at,
|
||||
nodeinfo,
|
||||
map_report: report,
|
||||
latest_position: null,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function toChronological(items: TextMessage[]): TextMessage[] {
|
||||
return [...items].reverse()
|
||||
}
|
||||
|
||||
function compareMessages(a: TextMessage, b: TextMessage): number {
|
||||
const timeDiff = Date.parse(a.created_at) - Date.parse(b.created_at)
|
||||
return timeDiff !== 0 ? timeDiff : a.id - b.id
|
||||
}
|
||||
|
||||
function mergeMessages(existing: TextMessage[], incoming: TextMessage[]): TextMessage[] {
|
||||
const byId = new Map<number, TextMessage>()
|
||||
for (const message of existing) {
|
||||
byId.set(message.id, message)
|
||||
}
|
||||
for (const message of incoming) {
|
||||
byId.set(message.id, message)
|
||||
}
|
||||
return Array.from(byId.values()).sort(compareMessages)
|
||||
}
|
||||
|
||||
async function loadInitialChatMessages() {
|
||||
const response = await getTextMessages(chatPageSize, 0)
|
||||
messages.value = toChronological(response.items)
|
||||
chatHasMore.value = response.items.length === chatPageSize
|
||||
chatInitialized.value = true
|
||||
}
|
||||
|
||||
async function loadOlderMessages() {
|
||||
if (chatLoadingOlder.value || !chatHasMore.value) {
|
||||
return
|
||||
}
|
||||
|
||||
chatLoadingOlder.value = true
|
||||
try {
|
||||
const response = await getTextMessages(chatPageSize, messages.value.length)
|
||||
messages.value = mergeMessages(messages.value, toChronological(response.items))
|
||||
chatHasMore.value = response.items.length === chatPageSize
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
chatLoadingOlder.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function pollLatestMessages() {
|
||||
const response = await getTextMessages(chatPageSize, 0)
|
||||
messages.value = mergeMessages(messages.value, toChronological(response.items))
|
||||
}
|
||||
|
||||
async function loadNodePage(page: number, showLoading = true) {
|
||||
if (showLoading) {
|
||||
nodePageLoading.value = true
|
||||
}
|
||||
try {
|
||||
const safePage = Math.max(1, page)
|
||||
const response = await getNodes(nodePageSize, (safePage - 1) * nodePageSize)
|
||||
pagedNodes.value = response.items
|
||||
const response = await getNodeInfo(nodePageSize, (safePage - 1) * nodePageSize)
|
||||
pagedNodeInfo.value = response.items
|
||||
nodeTotal.value = response.total ?? response.offset + response.items.length
|
||||
nodePage.value = safePage
|
||||
} catch (err) {
|
||||
@@ -72,17 +130,20 @@ async function refresh(showLoading = true) {
|
||||
}
|
||||
error.value = ''
|
||||
try {
|
||||
const [healthData, mapNodeData, messageData, positionData] = await Promise.all([
|
||||
const [healthData, nodeInfoData, mapReportData, positionData] = await Promise.all([
|
||||
getHealth(),
|
||||
getNodes(500, 0),
|
||||
getTextMessages(100),
|
||||
getNodeInfo(500, 0),
|
||||
getMapReports(500, 0),
|
||||
getPositions(500),
|
||||
])
|
||||
health.value = healthData
|
||||
mapNodeSource.value = mapNodeData.items
|
||||
messages.value = messageData.items
|
||||
nodeInfoSource.value = nodeInfoData.items
|
||||
mapReportSource.value = mapReportData.items
|
||||
positions.value = positionData.items
|
||||
await loadNodePage(nodePage.value, showLoading)
|
||||
await Promise.all([
|
||||
loadNodePage(nodePage.value, showLoading),
|
||||
chatInitialized.value ? pollLatestMessages() : loadInitialChatMessages(),
|
||||
])
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
@@ -115,7 +176,7 @@ onBeforeUnmount(() => {
|
||||
<span class="status-pill" :class="{ ok: health?.status === 'ok' }">
|
||||
{{ health?.status ?? 'unknown' }} / db {{ health?.database ?? 'unknown' }}
|
||||
</span>
|
||||
<span class="counter">节点 {{ nodeTotal }} · 消息 {{ messages.length }} · 坐标 {{ mapNodes.length }}</span>
|
||||
<span class="counter">节点 {{ nodeTotal }} · 已加载消息 {{ messages.length }} · 坐标 {{ mapNodes.length }}</span>
|
||||
<button @click="() => refresh()" :disabled="loading">{{ loading ? '刷新中...' : '刷新' }}</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -127,7 +188,10 @@ onBeforeUnmount(() => {
|
||||
:messages="messages"
|
||||
:nodes-by-id="nodesById"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:loading-older="chatLoadingOlder"
|
||||
:has-more-messages="chatHasMore"
|
||||
@select-node="selectedNodeId = $event"
|
||||
@load-older="loadOlderMessages"
|
||||
/>
|
||||
<MeshMap
|
||||
:nodes="mapNodes"
|
||||
@@ -138,7 +202,7 @@ onBeforeUnmount(() => {
|
||||
</section>
|
||||
|
||||
<NodeListPanel
|
||||
:nodes="pagedNodes"
|
||||
:nodes="pagedNodeInfo"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:page="nodePage"
|
||||
:page-size="nodePageSize"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HealthStatus, ListResponse, NodeInfoMap, PositionRecord, TextMessage } from './types'
|
||||
import type { HealthStatus, ListResponse, MapReport, NodeInfo, PositionRecord, TextMessage } from './types'
|
||||
|
||||
async function getJSON<T>(path: string): Promise<T> {
|
||||
const response = await fetch(path)
|
||||
@@ -12,12 +12,16 @@ export function getHealth(): Promise<HealthStatus> {
|
||||
return getJSON<HealthStatus>('/api/health')
|
||||
}
|
||||
|
||||
export function getNodes(limit = 500, offset = 0): Promise<ListResponse<NodeInfoMap>> {
|
||||
return getJSON<ListResponse<NodeInfoMap>>(`/api/nodes?limit=${limit}&offset=${offset}`)
|
||||
export function getNodeInfo(limit = 500, offset = 0): Promise<ListResponse<NodeInfo>> {
|
||||
return getJSON<ListResponse<NodeInfo>>(`/api/nodeinfo?limit=${limit}&offset=${offset}`)
|
||||
}
|
||||
|
||||
export function getTextMessages(limit = 100): Promise<ListResponse<TextMessage>> {
|
||||
return getJSON<ListResponse<TextMessage>>(`/api/text-messages?limit=${limit}`)
|
||||
export function getMapReports(limit = 500, offset = 0): Promise<ListResponse<MapReport>> {
|
||||
return getJSON<ListResponse<MapReport>>(`/api/map-reports?limit=${limit}&offset=${offset}`)
|
||||
}
|
||||
|
||||
export function getTextMessages(limit = 100, offset = 0): Promise<ListResponse<TextMessage>> {
|
||||
return getJSON<ListResponse<TextMessage>>(`/api/text-messages?limit=${limit}&offset=${offset}`)
|
||||
}
|
||||
|
||||
export function getPositions(limit = 500): Promise<ListResponse<PositionRecord>> {
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUpdate, onMounted, onUpdated, ref } from 'vue'
|
||||
import type { NodeInfoById, TextMessage } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
messages: TextMessage[]
|
||||
nodesById: NodeInfoById
|
||||
selectedNodeId: string | null
|
||||
loadingOlder: boolean
|
||||
hasMoreMessages: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-node': [nodeId: string]
|
||||
'load-older': []
|
||||
}>()
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const topThreshold = 8
|
||||
const bottomThreshold = 40
|
||||
|
||||
let didInitialScroll = false
|
||||
let shouldStickToBottom = true
|
||||
let restoreScrollHeight: number | null = null
|
||||
let restoreScrollTop = 0
|
||||
let restoreMessageCount = 0
|
||||
|
||||
function senderName(message: TextMessage): string {
|
||||
const node = props.nodesById[message.from_id]
|
||||
return node?.long_name || node?.short_name || message.from_id
|
||||
@@ -19,10 +33,79 @@ function senderName(message: TextMessage): string {
|
||||
function formatTime(value: string): string {
|
||||
return new Date(value).toLocaleString()
|
||||
}
|
||||
|
||||
function isNearBottom(el: HTMLElement): boolean {
|
||||
return el.scrollHeight - el.scrollTop - el.clientHeight <= bottomThreshold
|
||||
}
|
||||
|
||||
function clearRestoreState() {
|
||||
restoreScrollHeight = null
|
||||
restoreScrollTop = 0
|
||||
restoreMessageCount = 0
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const el = panelRef.value
|
||||
if (
|
||||
!el ||
|
||||
props.loadingOlder ||
|
||||
!props.hasMoreMessages ||
|
||||
props.messages.length === 0 ||
|
||||
restoreScrollHeight != null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (el.scrollTop <= topThreshold) {
|
||||
restoreScrollHeight = el.scrollHeight
|
||||
restoreScrollTop = el.scrollTop
|
||||
restoreMessageCount = props.messages.length
|
||||
emit('load-older')
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
const el = panelRef.value
|
||||
if (el) {
|
||||
shouldStickToBottom = isNearBottom(el)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
const el = panelRef.value
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight
|
||||
didInitialScroll = true
|
||||
}
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
const el = panelRef.value
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
|
||||
if (restoreScrollHeight != null) {
|
||||
if (props.messages.length > restoreMessageCount) {
|
||||
el.scrollTop = el.scrollHeight - restoreScrollHeight + restoreScrollTop
|
||||
clearRestoreState()
|
||||
return
|
||||
}
|
||||
if (!props.loadingOlder) {
|
||||
clearRestoreState()
|
||||
}
|
||||
}
|
||||
|
||||
if (!didInitialScroll || shouldStickToBottom) {
|
||||
el.scrollTop = el.scrollHeight
|
||||
didInitialScroll = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="chat-panel panel">
|
||||
<aside ref="panelRef" class="chat-panel panel" @scroll.passive="handleScroll">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">Chat</p>
|
||||
@@ -31,6 +114,8 @@ function formatTime(value: string): string {
|
||||
<span class="badge">{{ messages.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingOlder" class="chat-loading">正在加载更早消息...</div>
|
||||
<div v-else-if="!hasMoreMessages && messages.length > 0" class="chat-end">没有更多历史消息</div>
|
||||
<div v-if="messages.length === 0" class="empty">暂无聊天消息</div>
|
||||
<button
|
||||
v-for="message in messages"
|
||||
|
||||
@@ -91,21 +91,24 @@ function renderMarkers(forceFit: boolean) {
|
||||
}
|
||||
|
||||
function buildNodePopupHTML(node: MapNode): string {
|
||||
const info = node.node
|
||||
const info = node.nodeinfo
|
||||
const report = node.map_report
|
||||
return `
|
||||
<div class="node-popup">
|
||||
<strong>${escapeHTML(node.node_id)}</strong>
|
||||
<dl>
|
||||
<div><dt>长名称</dt><dd>${escapeHTML(info?.long_name || '-')}</dd></div>
|
||||
<div><dt>短名称</dt><dd>${escapeHTML(info?.short_name || '-')}</dd></div>
|
||||
<div><dt>硬件型号</dt><dd>${escapeHTML(info?.hw_model || '-')}</dd></div>
|
||||
<div><dt>角色</dt><dd>${escapeHTML(info?.role || '-')}</dd></div>
|
||||
<div><dt>固件版本</dt><dd>${escapeHTML(info?.firmware_version || '-')}</dd></div>
|
||||
<div><dt>长名称</dt><dd>${escapeHTML(report?.long_name || info?.long_name || '-')}</dd></div>
|
||||
<div><dt>短名称</dt><dd>${escapeHTML(report?.short_name || info?.short_name || '-')}</dd></div>
|
||||
<div><dt>硬件型号</dt><dd>${escapeHTML(report?.hw_model || info?.hw_model || '-')}</dd></div>
|
||||
<div><dt>角色</dt><dd>${escapeHTML(report?.role || info?.role || '-')}</dd></div>
|
||||
<div><dt>固件版本</dt><dd>${escapeHTML(report?.firmware_version || '-')}</dd></div>
|
||||
<div><dt>区域</dt><dd>${escapeHTML(report?.region || '-')}</dd></div>
|
||||
<div><dt>调制预设</dt><dd>${escapeHTML(report?.modem_preset || '-')}</dd></div>
|
||||
<div><dt>海拔</dt><dd>${node.altitude ?? '-'}</dd></div>
|
||||
<div><dt>经度</dt><dd>${node.longitude.toFixed(5)}</dd></div>
|
||||
<div><dt>纬度</dt><dd>${node.latitude.toFixed(5)}</dd></div>
|
||||
<div><dt>位置精度</dt><dd>${info?.position_precision ?? '-'}</dd></div>
|
||||
<div><dt>在线节点</dt><dd>${info?.num_online_local_nodes ?? '-'}</dd></div>
|
||||
<div><dt>位置精度</dt><dd>${report?.position_precision ?? '-'}</dd></div>
|
||||
<div><dt>在线节点</dt><dd>${report?.num_online_local_nodes ?? '-'}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { MapNode, NodeInfoMap, PositionRecord, TextMessage } from '../types'
|
||||
import type { MapNode, NodeInfo, PositionRecord, TextMessage } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
node: NodeInfoMap | null
|
||||
node: NodeInfo | null
|
||||
mapNode: MapNode | null
|
||||
messages: TextMessage[]
|
||||
positions: PositionRecord[]
|
||||
@@ -35,11 +35,12 @@ function formatTime(value: string | null | undefined): string {
|
||||
<div class="detail-main">
|
||||
<dl>
|
||||
<div><dt>Node ID</dt><dd>{{ node?.node_id || mapNode?.node_id }}</dd></div>
|
||||
<div><dt>Role</dt><dd>{{ node?.role || '-' }}</dd></div>
|
||||
<div><dt>Hardware</dt><dd>{{ node?.hw_model || '-' }}</dd></div>
|
||||
<div><dt>Latitude</dt><dd>{{ mapNode?.latitude ?? node?.latitude ?? '-' }}</dd></div>
|
||||
<div><dt>Longitude</dt><dd>{{ mapNode?.longitude ?? node?.longitude ?? '-' }}</dd></div>
|
||||
<div><dt>Altitude</dt><dd>{{ mapNode?.altitude ?? node?.altitude ?? '-' }}</dd></div>
|
||||
<div><dt>User ID</dt><dd>{{ node?.user_id || '-' }}</dd></div>
|
||||
<div><dt>Role</dt><dd>{{ node?.role || mapNode?.map_report?.role || '-' }}</dd></div>
|
||||
<div><dt>Hardware</dt><dd>{{ node?.hw_model || mapNode?.map_report?.hw_model || '-' }}</dd></div>
|
||||
<div><dt>Latitude</dt><dd>{{ mapNode?.latitude ?? '-' }}</dd></div>
|
||||
<div><dt>Longitude</dt><dd>{{ mapNode?.longitude ?? '-' }}</dd></div>
|
||||
<div><dt>Altitude</dt><dd>{{ mapNode?.altitude ?? '-' }}</dd></div>
|
||||
<div><dt>Updated</dt><dd>{{ formatTime(node?.updated_at || mapNode?.updated_at) }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { NodeInfoMap } from '../types'
|
||||
import type { NodeInfo } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: NodeInfoMap[]
|
||||
nodes: NodeInfo[]
|
||||
selectedNodeId: string | null
|
||||
page: number
|
||||
pageSize: number
|
||||
@@ -20,7 +20,7 @@ const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.page
|
||||
const canPrev = computed(() => props.page > 1)
|
||||
const canNext = computed(() => props.page < totalPages.value)
|
||||
|
||||
function nodeName(node: NodeInfoMap): string {
|
||||
function nodeName(node: NodeInfo): string {
|
||||
return node.long_name || node.short_name || node.node_id
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ function formatTime(value: string): string {
|
||||
<section class="node-list-panel panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">NodeInfo Map</p>
|
||||
<p class="eyebrow">NodeInfo</p>
|
||||
<h2>节点列表</h2>
|
||||
</div>
|
||||
<span class="badge">共 {{ total }} 条</span>
|
||||
@@ -45,10 +45,9 @@ function formatTime(value: string): string {
|
||||
<tr>
|
||||
<th>节点</th>
|
||||
<th>Node ID</th>
|
||||
<th>类型</th>
|
||||
<th>User ID</th>
|
||||
<th>角色</th>
|
||||
<th>硬件</th>
|
||||
<th>坐标</th>
|
||||
<th>更新时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -62,10 +61,9 @@ function formatTime(value: string): string {
|
||||
>
|
||||
<td>{{ nodeName(node) }}</td>
|
||||
<td>{{ node.node_id }}</td>
|
||||
<td>{{ node.latest_type }}</td>
|
||||
<td>{{ node.user_id || '-' }}</td>
|
||||
<td>{{ node.role || '-' }}</td>
|
||||
<td>{{ node.hw_model || '-' }}</td>
|
||||
<td>{{ node.latitude ?? '-' }}, {{ node.longitude ?? '-' }}</td>
|
||||
<td>{{ formatTime(node.updated_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -163,6 +163,16 @@ h3 {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.chat-loading,
|
||||
.chat-end {
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
@@ -10,20 +10,36 @@ export interface HealthStatus {
|
||||
database: string
|
||||
}
|
||||
|
||||
export interface NodeInfoMap {
|
||||
export interface NodeInfo {
|
||||
node_id: string
|
||||
node_num: number
|
||||
user_id: string | null
|
||||
long_name: string | null
|
||||
short_name: string | null
|
||||
hw_model: string | null
|
||||
role: string | null
|
||||
is_licensed: boolean | null
|
||||
public_key: string | null
|
||||
updated_at: string
|
||||
content_json: string
|
||||
}
|
||||
|
||||
export interface MapReport {
|
||||
node_id: string
|
||||
node_num: number
|
||||
latest_type: string
|
||||
long_name: string | null
|
||||
short_name: string | null
|
||||
hw_model: string | null
|
||||
role: string | null
|
||||
firmware_version: string | null
|
||||
region: string | null
|
||||
modem_preset: string | null
|
||||
latitude: number | null
|
||||
longitude: number | null
|
||||
altitude: number | null
|
||||
position_precision: number | null
|
||||
num_online_local_nodes: number | null
|
||||
has_opted_report_location: boolean | null
|
||||
updated_at: string
|
||||
content_json: string
|
||||
}
|
||||
@@ -56,10 +72,11 @@ export interface MapNode {
|
||||
latitude: number
|
||||
longitude: number
|
||||
altitude: number | null
|
||||
source: 'node' | 'position'
|
||||
source: 'map_report' | 'position'
|
||||
updated_at: string
|
||||
node: NodeInfoMap | null
|
||||
nodeinfo: NodeInfo | null
|
||||
map_report: MapReport | null
|
||||
latest_position: PositionRecord | null
|
||||
}
|
||||
|
||||
export type NodeInfoById = Record<string, NodeInfoMap>
|
||||
export type NodeInfoById = Record<string, NodeInfo>
|
||||
|
||||
Reference in New Issue
Block a user