Update architecture
Some checks failed
CI/CD / Lint & Test (push) Failing after 51s
CI/CD / Build & Push Docker image (hub.p42.ru) (push) Has been skipped

This commit is contained in:
admin 2026-02-09 17:31:21 +07:00
parent 8217818036
commit f4f3062352
4 changed files with 63 additions and 70 deletions

View File

@ -1,46 +0,0 @@
# Варианты favicon для NATS UI
В проекте две группы иконок (32×32 PNG): дизайн «два облачка + линия» в разных цветах и альтернативные дизайны.
## Цветовые варианты (два облачка и изогнутая линия)
Один и тот же дизайн — разные фоны и акценты.
| Файл | Цвета |
|------|--------|
| `favicon_variant0_original.png` | Исходный: приглушённый синий фон, белые облачка и линия |
| `favicon_variant1_purple.png` | Тёмно-фиолетовый фон, белые формы |
| `favicon_variant2_navy_cyan.png` | Тёмно-синий фон, голубые (cyan) формы |
| `favicon_variant3_teal.png` | Бирюзовый фон, белые формы |
| `favicon_variant4_black.png` | Чёрный фон, белые формы |
| `favicon_variant5_coral.png` | Кораллово-оранжевый фон, белые формы |
| `favicon_variant6_violet.png` | Фиолетовый фон, белые формы |
| `favicon_variant7_green.png` | Тёмно-зелёный фон, белые формы |
## Другие дизайны
| Файл | Описание |
|------|----------|
| `favicon_option1.png` | Молния/стрелка на фиолетовом |
| `favicon_option2.png` | Синий круг, три точки |
| `favicon_option3.png` | Стилизованная буква N |
| `favicon_option4.png` | Два перекрывающихся облачка/конверта |
| `favicon_option5.png` | Узлы и связи |
## Как выбрать иконку
1. Откройте файлы в проводнике или в браузере (если раздаёте статику).
2. Выберите понравившийся вариант.
3. Замените текущий favicon, например:
```bash
cp favicon_variant3_teal.png favicon.ico
# или
cp favicon_option4.png favicon.ico
```
4. Пересоберите приложение:
```bash
go build -o nats-ui .
```
Или пересоберите Docker-образ.
Браузеры принимают PNG в качестве favicon (файл может называться `favicon.ico`).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -39,13 +39,30 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.header-info {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin-top: 12px;
font-size: 0.95em;
}
.header .status { .header .status {
display: inline-block; display: inline-block;
padding: 8px 16px; padding: 8px 16px;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
border-radius: 20px; border-radius: 20px;
font-size: 0.9em; font-size: 0.9em;
margin-top: 10px; }
.header-sep {
opacity: 0.7;
}
.header-info strong {
font-weight: 600;
} }
.status.connected { .status.connected {
@ -326,7 +343,11 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>🚀 NATS Queue Visualizer</h1> <h1>🚀 NATS Queue Visualizer</h1>
<div id="status" class="status disconnected">Отключено</div> <div class="header-info">
<span id="status" class="status disconnected">Отключено</span>
<span class="header-sep"></span>
<span>Подписки: <strong id="subscribedList"></strong></span>
</div>
</div> </div>
<div class="controls"> <div class="controls">
@ -565,6 +586,19 @@
updateMessages(); updateMessages();
}); });
function loadSubscribed() {
fetch('/api/subscribed')
.then(res => res.json())
.then(data => {
const list = data.subscribed || [];
const el = document.getElementById('subscribedList');
el.textContent = list.length ? list.join(', ') : '—';
})
.catch(() => {
document.getElementById('subscribedList').textContent = '—';
});
}
function loadConnections() { function loadConnections() {
fetch('/api/connections') fetch('/api/connections')
.then(res => res.json()) .then(res => res.json())
@ -613,6 +647,7 @@
// Инициализация - только WebSocket, все сообщения придут автоматически // Инициализация - только WebSocket, все сообщения придут автоматически
connectWebSocket(); connectWebSocket();
loadSubscribed();
loadConnections(); loadConnections();
setInterval(loadConnections, 5000); setInterval(loadConnections, 5000);
</script> </script>

46
main.go
View File

@ -146,10 +146,11 @@ type Config struct {
type connzResponse struct { type connzResponse struct {
Connections []connInfo `json:"connections"` Connections []connInfo `json:"connections"`
Conns []connInfo `json:"conns"`
} }
type connInfo struct { type connInfo struct {
CID int `json:"cid"` CID uint64 `json:"cid"`
IP string `json:"ip"` IP string `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
Name string `json:"name"` Name string `json:"name"`
@ -170,6 +171,7 @@ func connectionsHandler(w http.ResponseWriter, _ *http.Request, cfg *Config) {
return return
} }
url := strings.TrimSuffix(cfg.NatsMonitorURL, "/") + "/connz" url := strings.TrimSuffix(cfg.NatsMonitorURL, "/") + "/connz"
log.Printf("Fetching NATS connections from %s", url)
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -181,7 +183,7 @@ func connectionsHandler(w http.ResponseWriter, _ *http.Request, cfg *Config) {
client := &http.Client{Timeout: 5 * time.Second} client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Printf("Failed to fetch NATS connz: %v", err) log.Printf("Failed to fetch NATS connz from %s: %v", url, err)
if err := json.NewEncoder(w).Encode(connzResponse{Connections: []connInfo{}}); err != nil { if err := json.NewEncoder(w).Encode(connzResponse{Connections: []connInfo{}}); err != nil {
log.Printf("Failed to encode connections: %v", err) log.Printf("Failed to encode connections: %v", err)
} }
@ -194,7 +196,7 @@ func connectionsHandler(w http.ResponseWriter, _ *http.Request, cfg *Config) {
return return
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
log.Printf("NATS connz returned status %d", resp.StatusCode) log.Printf("NATS connz %s returned status %d, body: %s", url, resp.StatusCode, truncate(string(body), 300))
if err := json.NewEncoder(w).Encode(connzResponse{Connections: []connInfo{}}); err != nil { if err := json.NewEncoder(w).Encode(connzResponse{Connections: []connInfo{}}); err != nil {
log.Printf("Failed to encode connections: %v", err) log.Printf("Failed to encode connections: %v", err)
} }
@ -202,15 +204,22 @@ func connectionsHandler(w http.ResponseWriter, _ *http.Request, cfg *Config) {
} }
var connz connzResponse var connz connzResponse
if err := json.Unmarshal(body, &connz); err != nil { if err := json.Unmarshal(body, &connz); err != nil {
log.Printf("Failed to parse connz: %v", err) log.Printf("Failed to parse NATS connz from %s: %v, body sample: %s", url, err, truncate(string(body), 200))
if err := json.NewEncoder(w).Encode(connzResponse{Connections: []connInfo{}}); err != nil { }
if len(connz.Connections) == 0 && len(connz.Conns) > 0 {
connz.Connections = connz.Conns
}
out := connzResponse{Connections: connz.Connections}
if err := json.NewEncoder(w).Encode(out); err != nil {
log.Printf("Failed to encode connections: %v", err) log.Printf("Failed to encode connections: %v", err)
} }
return
} }
if err := json.NewEncoder(w).Encode(connz); err != nil {
log.Printf("Failed to encode connections: %v", err) func truncate(s string, max int) string {
if len(s) <= max {
return s
} }
return s[:max] + "..."
} }
func extractProducerFromPayload(data []byte) string { func extractProducerFromPayload(data []byte) string {
@ -264,7 +273,6 @@ func main() {
} }
log.Fatalf("Failed to load config: %v", err) log.Fatalf("Failed to load config: %v", err)
} }
log.Printf("Loaded configuration from %s", configFile)
store := NewMessageStore(config.MaxMessages) store := NewMessageStore(config.MaxMessages)
hub := NewHub() hub := NewHub()
go hub.Run() go hub.Run()
@ -272,17 +280,14 @@ func main() {
opts = append(opts, nats.Name("nats-ui")) opts = append(opts, nats.Name("nats-ui"))
if config.Token != "" { if config.Token != "" {
opts = append(opts, nats.Token(config.Token)) opts = append(opts, nats.Token(config.Token))
log.Printf("Using token authentication")
} else if config.Username != "" || config.Password != "" { } else if config.Username != "" || config.Password != "" {
opts = append(opts, nats.UserInfo(config.Username, config.Password)) opts = append(opts, nats.UserInfo(config.Username, config.Password))
log.Printf("Using username/password authentication")
} }
nc, err := nats.Connect(config.NatsURL, opts...) nc, err := nats.Connect(config.NatsURL, opts...)
if err != nil { if err != nil {
log.Fatalf("Failed to connect to NATS: %v", err) log.Fatalf("Failed to connect to NATS: %v", err)
} }
defer nc.Close() defer nc.Close()
log.Printf("Connected to NATS at %s", config.NatsURL)
subjectsList := parseSubjects(config.Subjects) subjectsList := parseSubjects(config.Subjects)
for _, subject := range subjectsList { for _, subject := range subjectsList {
subj := subject subj := subject
@ -299,12 +304,10 @@ func main() {
} }
store.Add(message) store.Add(message)
hub.broadcast <- message hub.broadcast <- message
log.Printf("Received message on %s: %d bytes", msg.Subject, len(msg.Data))
}) })
if err != nil { if err != nil {
log.Fatalf("Failed to subscribe to %s: %v", subj, err) log.Fatalf("Failed to subscribe to %s: %v", subj, err)
} }
log.Printf("Subscribed to: %s", subj)
defer func() { defer func() {
if err := sub.Unsubscribe(); err != nil { if err := sub.Unsubscribe(); err != nil {
log.Printf("Failed to unsubscribe from %s: %v", subj, err) log.Printf("Failed to unsubscribe from %s: %v", subj, err)
@ -347,6 +350,13 @@ func main() {
log.Printf("Failed to encode subjects: %v", err) log.Printf("Failed to encode subjects: %v", err)
} }
}) })
http.HandleFunc("/api/subscribed", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
list := parseSubjects(config.Subjects)
if err := json.NewEncoder(w).Encode(map[string]interface{}{"subscribed": list}); err != nil {
log.Printf("Failed to encode subscribed: %v", err)
}
})
http.HandleFunc("/api/connections", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/api/connections", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
connectionsHandler(w, r, config) connectionsHandler(w, r, config)
@ -358,18 +368,13 @@ func main() {
return return
} }
hub.register <- conn hub.register <- conn
// Отправляем все существующие сообщения одним массивом при подключении
go func() { go func() {
messages := store.GetAll() messages := store.GetAll()
// Отправляем массив всех сообщений
if err := conn.WriteJSON(messages); err != nil { if err := conn.WriteJSON(messages); err != nil {
log.Printf("Error sending initial messages: %v", err) log.Printf("Error sending initial messages: %v", err)
return return
} }
}() }()
// Читаем сообщения от клиента (для keep-alive)
go func() { go func() {
for { for {
_, _, err := conn.ReadMessage() _, _, err := conn.ReadMessage()
@ -380,8 +385,7 @@ func main() {
} }
}() }()
}) })
log.Printf("Starting HTTP server on port %s", config.Port) log.Printf("NATS UI: http://0.0.0.0:%s", config.Port)
log.Printf("Open http://localhost:%s in your browser", config.Port)
addr := ":" + config.Port addr := ":" + config.Port
log.Fatal(http.ListenAndServe(addr, nil)) log.Fatal(http.ListenAndServe(addr, nil))
} }