up
This commit is contained in:
+116
-17
@@ -36,9 +36,19 @@ func New(cfg *config.AdminConfig) *Server {
|
||||
{
|
||||
admin.GET("/api/stats", handleStats)
|
||||
admin.GET("/api/clients", handleClients)
|
||||
admin.GET("/api/client/:id", handleClientDetail)
|
||||
admin.GET("/api/client/:id/subs", handleClientSubs)
|
||||
}
|
||||
r.GET("/admin/mqtt", serveAdmin)
|
||||
r.GET("/admin/mqtt/client/:id", serveClientDetail)
|
||||
r.GET("/", serveIndex)
|
||||
|
||||
// Meshtastic 消息查看
|
||||
meshtastic := r.Group("/admin/meshtastic")
|
||||
{
|
||||
meshtastic.GET("/", serveMeshtastic)
|
||||
meshtastic.GET("/api/messages", handleMessages)
|
||||
}
|
||||
r.GET("/admin/mqtt", serveIndex)
|
||||
r.GET("/", serveHome)
|
||||
|
||||
addr := cfg.Port
|
||||
if addr == "" {
|
||||
@@ -90,23 +100,76 @@ func (s *Server) Enabled() bool { return s.enabled }
|
||||
// serveIndex 返回管理页面
|
||||
func serveIndex(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, indexHTML)
|
||||
c.String(http.StatusOK, homeHTML)
|
||||
}
|
||||
|
||||
// serveHome 主页(meshmap 占位)
|
||||
func serveHome(c *gin.Context) {
|
||||
// serveAdmin MQTT 管理页面
|
||||
func serveAdmin(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="UTF-8"><title>meshgo</title></head>
|
||||
<body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f0f2f5">
|
||||
<div style="text-align:center">
|
||||
<h1 style="font-size:48px;color:#1677ff;margin-bottom:8px">meshmap</h1>
|
||||
<p style="color:#888;margin-bottom:24px">Mesh Network Map · 网格拓扑可视化</p>
|
||||
<a href="/admin/mqtt" style="display:inline-block;padding:10px 24px;background:#1677ff;color:#fff;text-decoration:none;border-radius:6px;font-size:14px">MQTT 管理面板</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>`)
|
||||
c.String(http.StatusOK, adminHTML)
|
||||
}
|
||||
|
||||
// serveClientDetail 客户端详情页面
|
||||
func serveClientDetail(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, clientDetailHTML)
|
||||
}
|
||||
|
||||
// serveMeshtastic Meshtastic 消息查看页面
|
||||
func serveMeshtastic(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, meshtasticHTML)
|
||||
}
|
||||
|
||||
// handleMessages 返回 msh 消息列表(JSON)
|
||||
func handleMessages(c *gin.Context) {
|
||||
messages := stats.GetMessages()
|
||||
pskIndex := stats.GetDefaultPSKIndex()
|
||||
|
||||
for i := range messages {
|
||||
if isJsonTopic(messages[i].Topic) {
|
||||
continue
|
||||
}
|
||||
|
||||
dec, err := stats.TryDecryptMessage(messages[i].Payload, pskIndex)
|
||||
if err != nil {
|
||||
// 打印详细错误
|
||||
env, parseErr := stats.ParseServiceEnvelopeDebug(messages[i].Payload)
|
||||
if parseErr != nil {
|
||||
log.Printf("[decrypt] FAIL psk=%d topic=%s parse_err=%v decrypt_err=%v payload_len=%d",
|
||||
pskIndex, messages[i].Topic, parseErr, err, len(messages[i].Payload))
|
||||
} else {
|
||||
packetId := uint64(0)
|
||||
from := uint32(0)
|
||||
variant := 0
|
||||
encryptedLen := 0
|
||||
if env.Packet != nil {
|
||||
packetId = env.Packet.Id
|
||||
from = env.Packet.From
|
||||
variant = env.Packet.WhichPayloadVariant
|
||||
encryptedLen = len(env.Packet.Encrypted)
|
||||
}
|
||||
log.Printf("[decrypt] FAIL psk=%d topic=%s channel=%s packetId=%d from=0x%x variant=%d encrypted_len=%d err=%v",
|
||||
pskIndex, messages[i].Topic, env.ChannelId, packetId, from, variant, encryptedLen, err)
|
||||
}
|
||||
messages[i].Decrypted = &stats.DecryptedMessage{
|
||||
GatewayId: err.Error(),
|
||||
}
|
||||
} else if dec != nil {
|
||||
messages[i].Decrypted = dec
|
||||
log.Printf("[decrypt] OK topic=%s channel=%s packetId=%d from=0x%x port=%d payload_len=%d",
|
||||
messages[i].Topic, dec.ChannelId, dec.PacketId, dec.From, dec.PortNum, len(dec.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": messages,
|
||||
})
|
||||
}
|
||||
|
||||
func isJsonTopic(topic string) bool {
|
||||
return len(topic) > 0 && (topic[len(topic)-1] == 'n' || topic[len(topic)-5:len(topic)] == "/json")
|
||||
}
|
||||
|
||||
// handleStats 返回实时统计(JSON)
|
||||
@@ -126,6 +189,33 @@ func handleClients(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleClientDetail 返回指定客户端详情(JSON)
|
||||
func handleClientDetail(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
info := stats.GetClient(id)
|
||||
if info == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 404,
|
||||
"msg": "客户端不存在或已离线",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": info,
|
||||
})
|
||||
}
|
||||
|
||||
// handleClientSubs 返回指定客户端的订阅主题列表(JSON)
|
||||
func handleClientSubs(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
subs := stats.GetClientSubs(id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": subs,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 认证中间件(预留扩展点)
|
||||
// AuthMiddleware 根据 cfg.AuthType 选择对应认证策略
|
||||
@@ -168,4 +258,13 @@ func basicAuth(user, pass string) gin.HandlerFunc {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
//go:embed index.html
|
||||
var indexHTML string
|
||||
var homeHTML string
|
||||
|
||||
//go:embed admin_mqtt.html
|
||||
var adminHTML string
|
||||
|
||||
//go:embed client_detail.html
|
||||
var clientDetailHTML string
|
||||
|
||||
//go:embed meshtastic.html
|
||||
var meshtasticHTML string
|
||||
|
||||
Reference in New Issue
Block a user