From 9f5a82d8d3af0a8f3b792b89819f326e0cfbb281 Mon Sep 17 00:00:00 2001 From: Alfred Date: Tue, 6 Jan 2026 19:04:29 +0800 Subject: [PATCH 01/14] feat: Add model status monitoring dashboard and backend integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive model monitoring system to robOS dashboard: Backend (ESP32): - Extended agx_monitor to parse model_status_update WebSocket events - New data structures for model status (agx_model_status_t, agx_model_monitor_data_t) - API functions: agx_monitor_get_model_data(), agx_monitor_is_model_data_valid() - Thread-safe model data storage with mutex protection - New HTTP endpoint: /api/model_status with CORS support Frontend (Web Dashboard): - New "模型状态监控" (Model Status Monitor) section with collapsible UI - "模型拉起状态" (Model Loading Status): * Three progress bars for LLM, embedding, and reranker models * Real-time progress tracking (0%-100%) * Shimmer animation for loading states * Dynamic color coding (gray/green) based on status * Display model name and API port when complete - "模型运行日志" (Model Runtime Logs): * Three collapsible log windows (default collapsed) * Auto-refresh every 5 seconds when expanded * Fetch logs from http://10.10.99.98:58090/api/model_logs/ * HTML escaping for XSS prevention * Auto-scroll to bottom * Proper handling of disabled models - Collapsible sections for inference/application/model monitors (saved to localStorage) - Translation support (Chinese/English) for all new UI elements - Responsive design for mobile and desktop UI/UX Improvements: - Progress bars show "--" when no data available - "未启用" message for disabled models - "等待状态数据..." when waiting for backend connection - Right-aligned status text with proper contrast - Smooth transitions and animations - State persistence across page reloads Integration: - Connects to rm01OrinStatus backend for model status - WebSocket: model_status_update events (every 2s polling) - HTTP API: /api/model_logs/ (on-demand fetching) - Proper error handling and fallback states Documentation: - Add MODEL_MONITORING_IMPLEMENTATION.md for technical details --- components/agx_monitor/agx_monitor.c | 272 ++++++++- components/agx_monitor/include/agx_monitor.h | 47 ++ components/web_server/web_server.c | 81 +++ docs/MODEL_MONITORING_IMPLEMENTATION.md | 379 ++++++++++++ sdcard/web/index.htm | 575 ++++++++++++++++++- 5 files changed, 1340 insertions(+), 14 deletions(-) create mode 100644 docs/MODEL_MONITORING_IMPLEMENTATION.md diff --git a/components/agx_monitor/agx_monitor.c b/components/agx_monitor/agx_monitor.c index 5456a21..82a55fd 100644 --- a/components/agx_monitor/agx_monitor.c +++ b/components/agx_monitor/agx_monitor.c @@ -46,8 +46,9 @@ typedef struct { esp_websocket_client_handle_t ws_client; ///< WebSocket client handle // Data storage - agx_monitor_data_t latest_data; ///< Latest monitoring data - SemaphoreHandle_t data_mutex; ///< Data access mutex + agx_monitor_data_t latest_data; ///< Latest monitoring data + agx_model_monitor_data_t latest_model_data; ///< Latest model monitoring data + SemaphoreHandle_t data_mutex; ///< Data access mutex // Task management TaskHandle_t monitor_task_handle; ///< Monitor task handle @@ -103,6 +104,9 @@ static esp_err_t agx_monitor_parse_power_data(cJSON *power_json, agx_monitor_data_t *data); static esp_err_t agx_monitor_parse_gpu_data(cJSON *gpu_json, agx_monitor_data_t *data); +static esp_err_t agx_monitor_parse_model_data(const char *json_data, size_t data_len); +static esp_err_t agx_monitor_parse_model_status(cJSON *model_json, + agx_model_status_t *status); // Utility functions static void agx_monitor_update_status(agx_monitor_status_t new_status); @@ -210,9 +214,11 @@ esp_err_t agx_monitor_init(const agx_monitor_config_t *config) { return ESP_ERR_NO_MEM; } - // Initialize data structure + // Initialize data structures memset(&s_agx_monitor.latest_data, 0, sizeof(agx_monitor_data_t)); s_agx_monitor.latest_data.is_valid = false; + memset(&s_agx_monitor.latest_model_data, 0, sizeof(agx_model_monitor_data_t)); + s_agx_monitor.latest_model_data.is_valid = false; // Initialize status and timing s_agx_monitor.connection_status = AGX_MONITOR_STATUS_INITIALIZED; @@ -1010,7 +1016,7 @@ static void agx_monitor_websocket_event_handler(void *handler_args, ESP_LOGD(TAG, "Found JSON array start: %s", json_start); // Find the event name and data - // Expected format: 42["tegrastats_update",{data}] + // Expected format: 42["tegrastats_update",{data}] or 42["model_status_update",{data}] if (strstr(json_start, "tegrastats_update")) { ESP_LOGD(TAG, "*** TEGRASTATS_UPDATE EVENT DETECTED ***"); @@ -1045,9 +1051,38 @@ static void agx_monitor_websocket_event_handler(void *handler_args, } else { ESP_LOGW(TAG, "Could not find JSON object start"); } + } else if (strstr(json_start, "model_status_update")) { + ESP_LOGD(TAG, "*** MODEL_STATUS_UPDATE EVENT DETECTED ***"); + + const char *data_start = strchr(json_start, '{'); + if (data_start) { + // Find the end of the JSON object + const char *data_end = strrchr(message, '}'); + if (data_end) { + size_t json_len = data_end - data_start + 1; + + ESP_LOGD(TAG, "=== MODEL STATUS JSON DATA ==="); + ESP_LOGD(TAG, "JSON Length: %zu bytes", json_len); + ESP_LOGD(TAG, "JSON Content: %.*s", (int)json_len, + data_start); + ESP_LOGD(TAG, "=============================="); + + esp_err_t parse_ret = + agx_monitor_parse_model_data(data_start, json_len); + if (parse_ret == ESP_OK) { + ESP_LOGD(TAG, "✅ Processed model status data"); + } else { + ESP_LOGW(TAG, "❌ Failed to parse model status data: %s", + esp_err_to_name(parse_ret)); + } + } else { + ESP_LOGW(TAG, "Could not find JSON object end"); + } + } else { + ESP_LOGW(TAG, "Could not find JSON object start"); + } } else { - ESP_LOGD(TAG, "Socket.IO event (not tegrastats_update): %s", - json_start); + ESP_LOGD(TAG, "Socket.IO event (unknown): %s", json_start); } } else { ESP_LOGW(TAG, "Could not find JSON array in Socket.IO message"); @@ -1991,4 +2026,229 @@ static esp_err_t agx_monitor_unregister_commands(void) { ESP_LOGD(TAG, "Unregistered AGX monitor console command"); return ESP_OK; +} + +/* ============================================================================ + * Model Data Parsing Functions + * ============================================================================ + */ + +static esp_err_t agx_monitor_parse_model_status(cJSON *model_json, + agx_model_status_t *status) { + if (model_json == NULL || status == NULL) { + return ESP_ERR_INVALID_ARG; + } + + // Parse service name + cJSON *service = cJSON_GetObjectItem(model_json, "service"); + if (cJSON_IsString(service) && service->valuestring != NULL) { + strncpy(status->service_name, service->valuestring, + sizeof(status->service_name) - 1); + } + + // Parse model type + cJSON *model_type = cJSON_GetObjectItem(model_json, "model_type"); + if (cJSON_IsString(model_type) && model_type->valuestring != NULL) { + strncpy(status->model_type, model_type->valuestring, + sizeof(status->model_type) - 1); + } + + // Parse progress + cJSON *progress = cJSON_GetObjectItem(model_json, "progress"); + if (cJSON_IsNumber(progress)) { + status->progress = progress->valueint; + } + + // Parse status text + cJSON *status_text = cJSON_GetObjectItem(model_json, "status_text"); + if (cJSON_IsString(status_text) && status_text->valuestring != NULL) { + strncpy(status->status_text, status_text->valuestring, + sizeof(status->status_text) - 1); + } + + // Parse model name + cJSON *model_name = cJSON_GetObjectItem(model_json, "model_name"); + if (cJSON_IsString(model_name) && model_name->valuestring != NULL) { + strncpy(status->model_name, model_name->valuestring, + sizeof(status->model_name) - 1); + } + + // Parse API port + cJSON *api_port = cJSON_GetObjectItem(model_json, "api_port"); + if (cJSON_IsString(api_port) && api_port->valuestring != NULL) { + strncpy(status->api_port, api_port->valuestring, + sizeof(status->api_port) - 1); + } + + // Parse is_enabled + cJSON *is_enabled = cJSON_GetObjectItem(model_json, "is_enabled"); + if (cJSON_IsBool(is_enabled)) { + status->is_enabled = cJSON_IsTrue(is_enabled); + } + + // Parse startup_complete + cJSON *startup_complete = cJSON_GetObjectItem(model_json, "startup_complete"); + if (cJSON_IsBool(startup_complete)) { + status->startup_complete = cJSON_IsTrue(startup_complete); + } + + // Parse last_update + cJSON *last_update = cJSON_GetObjectItem(model_json, "last_update"); + if (cJSON_IsNumber(last_update)) { + status->last_update = (uint64_t)last_update->valuedouble; + } + + return ESP_OK; +} + +static esp_err_t agx_monitor_parse_model_data(const char *json_data, + size_t data_len) { + if (json_data == NULL || data_len == 0) { + ESP_LOGE(TAG, "Invalid model JSON data parameters"); + return ESP_ERR_INVALID_ARG; + } + + ESP_LOGD(TAG, "Parsing model JSON data (%zu bytes)", data_len); + + // Parse JSON data using cJSON + cJSON *root = cJSON_Parse(json_data); + if (root == NULL) { + const char *error_ptr = cJSON_GetErrorPtr(); + if (error_ptr != NULL) { + ESP_LOGE(TAG, "Model JSON parse error before: %s", error_ptr); + } else { + ESP_LOGE(TAG, "Failed to parse model JSON data"); + } + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = ESP_OK; + + // Acquire mutex for data update + if (xSemaphoreTake(s_agx_monitor.data_mutex, pdMS_TO_TICKS(1000))) { + // Clear previous data + memset(&s_agx_monitor.latest_model_data, 0, + sizeof(agx_model_monitor_data_t)); + s_agx_monitor.latest_model_data.update_time_us = esp_timer_get_time(); + + // Parse timestamp + cJSON *timestamp = cJSON_GetObjectItem(root, "timestamp"); + if (cJSON_IsString(timestamp) && (timestamp->valuestring != NULL)) { + strncpy(s_agx_monitor.latest_model_data.timestamp, + timestamp->valuestring, + sizeof(s_agx_monitor.latest_model_data.timestamp) - 1); + } + + // Parse LLM model status + cJSON *llm = cJSON_GetObjectItem(root, "llm"); + if (llm != NULL) { + esp_err_t llm_ret = agx_monitor_parse_model_status( + llm, &s_agx_monitor.latest_model_data.llm); + if (llm_ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to parse LLM status: %s", esp_err_to_name(llm_ret)); + if (ret == ESP_OK) + ret = llm_ret; + } + } + + // Parse embedding model status + cJSON *embedding = cJSON_GetObjectItem(root, "embedding"); + if (embedding != NULL) { + esp_err_t emb_ret = agx_monitor_parse_model_status( + embedding, &s_agx_monitor.latest_model_data.embedding); + if (emb_ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to parse embedding status: %s", + esp_err_to_name(emb_ret)); + if (ret == ESP_OK) + ret = emb_ret; + } + } + + // Parse reranker model status + cJSON *reranker = cJSON_GetObjectItem(root, "reranker"); + if (reranker != NULL) { + esp_err_t rerank_ret = agx_monitor_parse_model_status( + reranker, &s_agx_monitor.latest_model_data.reranker); + if (rerank_ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to parse reranker status: %s", + esp_err_to_name(rerank_ret)); + if (ret == ESP_OK) + ret = rerank_ret; + } + } + + // Mark data as valid if parsing was successful + if (ret == ESP_OK) { + s_agx_monitor.latest_model_data.is_valid = true; + ESP_LOGD(TAG, "Model JSON data parsing completed successfully"); + } else { + s_agx_monitor.latest_model_data.is_valid = false; + ESP_LOGW(TAG, "Model JSON data parsing completed with errors"); + } + + xSemaphoreGive(s_agx_monitor.data_mutex); + } else { + ESP_LOGE(TAG, "Failed to acquire mutex for model data update"); + ret = ESP_ERR_TIMEOUT; + } + + // Clean up JSON object + cJSON_Delete(root); + + return ret; +} + +/* ============================================================================ + * Public API for Model Data + * ============================================================================ + */ + +esp_err_t agx_monitor_get_model_data(agx_model_monitor_data_t *data) { + if (!s_agx_monitor.initialized) { + return ESP_ERR_INVALID_STATE; + } + + if (data == NULL) { + return ESP_ERR_INVALID_ARG; + } + + // Take mutex and copy data + if (xSemaphoreTake(s_agx_monitor.data_mutex, pdMS_TO_TICKS(1000))) { + memcpy(data, &s_agx_monitor.latest_model_data, + sizeof(agx_model_monitor_data_t)); + xSemaphoreGive(s_agx_monitor.data_mutex); + } else { + ESP_LOGW(TAG, "Failed to acquire mutex for model data access"); + return ESP_ERR_TIMEOUT; + } + + return ESP_OK; +} + +bool agx_monitor_is_model_data_valid(void) { + if (!s_agx_monitor.initialized) { + return false; + } + + bool is_valid = false; + if (xSemaphoreTake(s_agx_monitor.data_mutex, pdMS_TO_TICKS(100))) { + is_valid = s_agx_monitor.latest_model_data.is_valid; + + // Check if data is recent (within last 30 seconds) + if (is_valid) { + uint64_t current_time = esp_timer_get_time(); + uint64_t data_age = + current_time - s_agx_monitor.latest_model_data.update_time_us; + if (data_age > 30000000) { // 30 seconds in microseconds + ESP_LOGD(TAG, "Model data expired: age=%llu us", data_age); + is_valid = false; + } + } + + xSemaphoreGive(s_agx_monitor.data_mutex); + } else { + ESP_LOGD(TAG, "Failed to acquire mutex for model data validity check"); + } + + return is_valid; } \ No newline at end of file diff --git a/components/agx_monitor/include/agx_monitor.h b/components/agx_monitor/include/agx_monitor.h index a1bc072..f51c4d7 100644 --- a/components/agx_monitor/include/agx_monitor.h +++ b/components/agx_monitor/include/agx_monitor.h @@ -186,6 +186,33 @@ typedef struct { uint64_t update_time_us; ///< Update timestamp in microseconds } agx_monitor_data_t; +/** + * @brief Model status information structure + */ +typedef struct { + char service_name[64]; ///< Service name (e.g., "dev-llm.service") + char model_type[16]; ///< Model type ("llm", "embedding", "reranker") + int progress; ///< Startup progress (0-100) + char status_text[128]; ///< Status text or percentage + char model_name[64]; ///< Model name (e.g., "RM-01 LLM") + char api_port[8]; ///< API port number + bool is_enabled; ///< Whether model is enabled + bool startup_complete; ///< Whether startup is complete + uint64_t last_update; ///< Last update timestamp +} agx_model_status_t; + +/** + * @brief Complete model monitoring data structure + */ +typedef struct { + char timestamp[AGX_MONITOR_MAX_TIMESTAMP_LENGTH]; ///< ISO 8601 timestamp + agx_model_status_t llm; ///< LLM model status + agx_model_status_t embedding; ///< Embedding model status + agx_model_status_t reranker; ///< Reranker model status + bool is_valid; ///< Data validity flag + uint64_t update_time_us; ///< Update timestamp in microseconds +} agx_model_monitor_data_t; + /** * @brief AGX monitor configuration structure */ @@ -358,6 +385,26 @@ esp_err_t agx_monitor_register_callback(agx_monitor_event_callback_t callback, */ esp_err_t agx_monitor_unregister_callback(void); +/** + * @brief Get latest model monitoring data + * + * Retrieves the most recent model status data from all three models. + * This function is thread-safe. + * + * @param data Pointer to model data structure to fill + * @return esp_err_t ESP_OK on success, error code on failure + */ +esp_err_t agx_monitor_get_model_data(agx_model_monitor_data_t *data); + +/** + * @brief Check if model data is valid + * + * Checks if the latest model monitoring data is valid and recent. + * + * @return true if data is valid, false otherwise + */ +bool agx_monitor_is_model_data_valid(void); + /** * @brief Register console commands * diff --git a/components/web_server/web_server.c b/components/web_server/web_server.c index 7e55ec8..2948b3b 100644 --- a/components/web_server/web_server.c +++ b/components/web_server/web_server.c @@ -10,6 +10,7 @@ */ #include "web_server.h" +#include "agx_monitor.h" #include "cJSON.h" #include "esp_http_server.h" #include "esp_log.h" @@ -205,6 +206,80 @@ static esp_err_t api_system_handler(httpd_req_t *req) { return ESP_OK; } +/** + * @brief Model status API handler + */ +static esp_err_t api_model_status_handler(httpd_req_t *req) { + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + + agx_model_monitor_data_t model_data; + esp_err_t ret = agx_monitor_get_model_data(&model_data); + + if (ret != ESP_OK) { + cJSON *json = cJSON_CreateObject(); + cJSON_AddStringToObject(json, "error", "Failed to get model data"); + cJSON_AddBoolToObject(json, "is_valid", false); + + char *json_string = cJSON_Print(json); + if (json_string) { + httpd_resp_sendstr(req, json_string); + free(json_string); + } + cJSON_Delete(json); + return ESP_OK; + } + + // Create JSON response + cJSON *json = cJSON_CreateObject(); + cJSON_AddStringToObject(json, "timestamp", model_data.timestamp); + cJSON_AddBoolToObject(json, "is_valid", model_data.is_valid); + + // Add LLM model status + cJSON *llm = cJSON_CreateObject(); + cJSON_AddStringToObject(llm, "model_type", model_data.llm.model_type); + cJSON_AddNumberToObject(llm, "progress", model_data.llm.progress); + cJSON_AddStringToObject(llm, "status_text", model_data.llm.status_text); + cJSON_AddStringToObject(llm, "model_name", model_data.llm.model_name); + cJSON_AddStringToObject(llm, "api_port", model_data.llm.api_port); + cJSON_AddBoolToObject(llm, "is_enabled", model_data.llm.is_enabled); + cJSON_AddBoolToObject(llm, "startup_complete", model_data.llm.startup_complete); + cJSON_AddItemToObject(json, "llm", llm); + + // Add embedding model status + cJSON *embedding = cJSON_CreateObject(); + cJSON_AddStringToObject(embedding, "model_type", model_data.embedding.model_type); + cJSON_AddNumberToObject(embedding, "progress", model_data.embedding.progress); + cJSON_AddStringToObject(embedding, "status_text", model_data.embedding.status_text); + cJSON_AddStringToObject(embedding, "model_name", model_data.embedding.model_name); + cJSON_AddStringToObject(embedding, "api_port", model_data.embedding.api_port); + cJSON_AddBoolToObject(embedding, "is_enabled", model_data.embedding.is_enabled); + cJSON_AddBoolToObject(embedding, "startup_complete", model_data.embedding.startup_complete); + cJSON_AddItemToObject(json, "embedding", embedding); + + // Add reranker model status + cJSON *reranker = cJSON_CreateObject(); + cJSON_AddStringToObject(reranker, "model_type", model_data.reranker.model_type); + cJSON_AddNumberToObject(reranker, "progress", model_data.reranker.progress); + cJSON_AddStringToObject(reranker, "status_text", model_data.reranker.status_text); + cJSON_AddStringToObject(reranker, "model_name", model_data.reranker.model_name); + cJSON_AddStringToObject(reranker, "api_port", model_data.reranker.api_port); + cJSON_AddBoolToObject(reranker, "is_enabled", model_data.reranker.is_enabled); + cJSON_AddBoolToObject(reranker, "startup_complete", model_data.reranker.startup_complete); + cJSON_AddItemToObject(json, "reranker", reranker); + + char *json_string = cJSON_Print(json); + if (json_string) { + httpd_resp_sendstr(req, json_string); + free(json_string); + } else { + httpd_resp_send_500(req); + } + + cJSON_Delete(json); + return ESP_OK; +} + /** * @brief OPTIONS handler for CORS */ @@ -260,6 +335,12 @@ esp_err_t web_server_start(void) { .user_ctx = NULL}; httpd_register_uri_handler(server, &api_system_uri); + httpd_uri_t api_model_status_uri = {.uri = "/api/model_status", + .method = HTTP_GET, + .handler = api_model_status_handler, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &api_model_status_uri); + // Register OPTIONS handler for CORS httpd_uri_t options_uri = {.uri = "/*", .method = HTTP_OPTIONS, diff --git a/docs/MODEL_MONITORING_IMPLEMENTATION.md b/docs/MODEL_MONITORING_IMPLEMENTATION.md new file mode 100644 index 0000000..7fce0e1 --- /dev/null +++ b/docs/MODEL_MONITORING_IMPLEMENTATION.md @@ -0,0 +1,379 @@ +# 模型状态监控功能实施文档 + +## 📋 概述 + +本文档描述了为 robOS 和 rm01OrinStatus 项目添加的模型状态监控功能的完整实现。 + +## ✅ 已实现功能 + +### 1. 推理模组端 (rm01OrinStatus) + +#### 新增文件 +- `src/tegrastats_api/model_monitor.py` - 模型监控核心模块 + +#### 修改文件 +- `src/tegrastats_api/server.py` - 集成模型监控 +- `src/tegrastats_api/__init__.py` - 导出新模块 + +#### 功能特性 +- ✅ 监控 3 个 systemd 服务: + - `dev-llm.service` (主模型) + - `dev-embedding.service` (嵌入模型) + - `dev-reranker.service` (重排模型) +- ✅ 通过 `journalctl` 实时跟踪启动日志 +- ✅ 识别启动关键节点并计算进度(10%, 25%, 40%, 50%, 75%, 100%) +- ✅ 提取模型名称和 API 端口信息 +- ✅ 检测模型是否启用(10秒超时主模型,5秒超时其他模型) +- ✅ 通过 WebSocket 推送 `model_status_update` 事件 +- ✅ REST API 端点: + - `GET /api/model_status` - 获取所有模型状态 + - `GET /api/model_logs/?lines=100` - 获取模型日志 + +### 2. ESP32 端 (robOS) + +#### 修改文件 +- `components/agx_monitor/include/agx_monitor.h` - 添加模型数据结构 +- `components/agx_monitor/agx_monitor.c` - 实现模型数据解析和存储 +- `components/web_server/web_server.c` - 添加模型状态 API + +#### 功能特性 +- ✅ 接收并解析 `model_status_update` WebSocket 事件 +- ✅ 线程安全的模型数据存储 +- ✅ REST API 端点: + - `GET /api/model_status` - 代理模型状态数据 + +### 3. Web 界面 + +#### 修改文件 +- `sdcard/web/index.htm` + +#### 功能特性 + +**折叠功能** +- ✅ 推理模组监控板块可折叠 +- ✅ 应用模组监控板块可折叠 +- ✅ 模型状态监控板块可折叠 +- ✅ 折叠状态保存到 localStorage + +**模型拉起状态** +- ✅ 3 个进度条(主模型、嵌入模型、重排模型) +- ✅ 高度与 GPU 监控一致 +- ✅ 从左到右循环滚动光线动画(加载时) +- ✅ 显示进度百分比或状态文本 +- ✅ 100% 后 5 秒显示模型名称和 API 端口 +- ✅ 未启用时显示"未启用XXX模型" + +**模型运行日志** +- ✅ 3 个日志窗口(主模型、嵌入模型、重排模型) +- ✅ 每个窗口可独立折叠(默认折叠) +- ✅ 高度为温度监控高度的 2 倍 +- ✅ 支持滚动,自动滚动到底部 +- ✅ 终端风格显示(黑色背景,绿色文字) +- ✅ 未启用时显示"未启用XXX模型" + +**多语言支持** +- ✅ 中文和英文完整翻译 + +## 🚀 部署步骤 + +### 1. 部署 rm01OrinStatus(推理模组) + +```bash +# SSH 到推理模组 +ssh user@10.10.99.98 + +# 停止服务 +sudo systemctl stop tegrastats-api.service + +# 拉取最新代码 +cd rm01OrinStatus +git pull + +# 重启服务 +sudo systemctl start tegrastats-api.service + +# 查看状态 +sudo systemctl status tegrastats-api.service + +# 查看日志确认模型监控启动 +sudo journalctl -u tegrastats-api.service -f +``` + +### 2. 部署 robOS(ESP32) + +```bash +# 在开发机器上 +cd /Users/massif/robOS + +# 构建项目 +idf.py build + +# 烧录到 ESP32 +idf.py flash + +# 查看串口日志 +idf.py monitor +``` + +### 3. 更新 Web 界面 + +Web 界面文件 `sdcard/web/index.htm` 会在 ESP32 重启后自动从 SD 卡加载。 + +## 🧪 测试验证 + +### 1. 验证推理模组端 + +```bash +# 测试 API 端点 +curl http://10.10.99.98:58090/api/model_status | python -m json.tool + +# 预期响应 +{ + "timestamp": "2025-01-06T...", + "llm": { + "service": "dev-llm.service", + "model_type": "llm", + "progress": 100, + "status_text": "模型:RM-01 LLM | 端口:58000", + "model_name": "RM-01 LLM", + "api_port": "58000", + "is_enabled": true, + "startup_complete": true + }, + "embedding": { ... }, + "reranker": { ... } +} +``` + +### 2. 验证 ESP32 端 + +```bash +# 从 ESP32 获取模型状态 +curl http://10.10.99.97/api/model_status | python -m json.tool +``` + +### 3. 验证 Web 界面 + +1. 打开浏览器访问 `http://10.10.99.97` +2. 检查三个板块都有折叠按钮 +3. 点击折叠按钮验证折叠功能 +4. 滚动到"模型状态监控"板块 +5. 验证三个进度条显示正确 +6. 点击日志板块标题展开日志窗口 +7. 切换语言验证翻译 + +## 📊 数据流架构 + +``` +推理模组 (10.10.99.98) + ├─ ModelMonitor + │ ├─ journalctl监控 → 解析日志 → 识别关键点 + │ └─ 每1秒更新状态 + └─ TegrastatsServer + ├─ WebSocket: model_status_update 事件 + └─ REST API: /api/model_status + + ↓ (WebSocket) + +ESP32 (10.10.99.97) + ├─ agx_monitor + │ ├─ 接收 model_status_update + │ ├─ 解析并存储数据 + │ └─ 线程安全访问 + └─ web_server + └─ REST API: /api/model_status + + ↓ (HTTP) + +Web 界面 (浏览器) + ├─ 每2秒轮询 /api/model_status + ├─ 更新进度条 + └─ 更新日志显示 +``` + +## 🔧 配置说明 + +### 进度检查点 + +在 `rm01OrinStatus/src/tegrastats_api/model_monitor.py` 中定义: + +```python +CHECKPOINTS = [ + (r"Initializing a V1 LLM engine", 10), + (r"Loading safetensors checkpoint shards:.*100%.*Completed", 25), + (r"Available KV cache memory", 40), + (r"Capturing CUDA graphs", 50), + (r"Graph capturing finished", 75), + (r"Application startup complete", 100), +] +``` + +### 超时设置 + +- 主模型 (llm): 10 秒无日志显示"未启用" +- 嵌入模型 (embedding): 5 秒 +- 重排模型 (reranker): 5 秒 + +### 更新频率 + +- 推理模组 WebSocket 推送:1 秒 +- Web 界面轮询:2 秒 +- 日志更新:3 秒 + +## 🐛 故障排查 + +### 问题:模型状态不更新 + +**检查步骤**: +1. 验证推理模组服务运行:`sudo systemctl status tegrastats-api.service` +2. 检查模型服务状态: + ```bash + sudo systemctl status dev-llm.service + sudo systemctl status dev-embedding.service + sudo systemctl status dev-reranker.service + ``` +3. 查看推理模组日志:`sudo journalctl -u tegrastats-api.service -f` +4. 验证 WebSocket 连接:查看浏览器控制台 + +### 问题:进度条卡在某个百分比 + +**原因**:日志关键词不匹配 + +**解决**: +1. 查看实际的服务日志:`sudo journalctl -u dev-llm.service -f` +2. 对比 `CHECKPOINTS` 中的正则表达式 +3. 调整正则表达式以匹配实际日志 + +### 问题:折叠状态不保存 + +**原因**:浏览器 localStorage 问题 + +**解决**: +1. 清除浏览器缓存 +2. 检查浏览器控制台是否有错误 +3. 验证 `localStorage` 是否可用 + +## 📝 API 参考 + +### rm01OrinStatus API + +#### GET /api/model_status + +返回所有模型的状态。 + +**响应示例**: +```json +{ + "timestamp": "2025-01-06T12:00:00.000Z", + "llm": { + "service": "dev-llm.service", + "model_type": "llm", + "progress": 100, + "status_text": "模型:RM-01 LLM | 端口:58000", + "model_name": "RM-01 LLM", + "api_port": "58000", + "is_enabled": true, + "startup_complete": true, + "last_update": 1704542400 + }, + "embedding": { ... }, + "reranker": { ... } +} +``` + +#### GET /api/model_logs/{model_type}?lines=100 + +获取指定模型的日志。 + +**参数**: +- `model_type`: llm | embedding | reranker +- `lines`: 返回行数(默认100,最大1000) + +**响应示例**: +```json +{ + "model_type": "llm", + "logs": [ + "[12:00:00] Initializing a V1 LLM engine", + "[12:00:05] Loading safetensors checkpoint shards: 100% Completed", + ... + ], + "count": 50, + "timestamp": "2025-01-06T12:00:00.000Z" +} +``` + +### robOS API + +#### GET /api/model_status + +代理推理模组的模型状态数据。 + +格式与 rm01OrinStatus 相同。 + +## 🎨 UI 设计说明 + +### 进度条动画 + +使用 CSS 关键帧动画实现光线滚动效果: + +```css +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} + +.model-status-progress-fill.loading { + background: linear-gradient(...); + background-size: 1000px 100%; + animation: shimmer 2s infinite linear; +} +``` + +### 折叠动画 + +使用 `max-height` 过渡实现平滑折叠: + +```css +.collapsible-content { + max-height: 5000px; + transition: max-height 0.5s ease, opacity 0.3s ease; +} + +.collapsible-content.collapsed { + max-height: 0; + opacity: 0; +} +``` + +## 📚 相关文件清单 + +### rm01OrinStatus +- `src/tegrastats_api/model_monitor.py` ⭐ 新建 +- `src/tegrastats_api/server.py` ✏️ 修改 +- `src/tegrastats_api/__init__.py` ✏️ 修改 + +### robOS +- `components/agx_monitor/include/agx_monitor.h` ✏️ 修改 +- `components/agx_monitor/agx_monitor.c` ✏️ 修改 +- `components/web_server/web_server.c` ✏️ 修改 +- `sdcard/web/index.htm` ✏️ 修改 + +## 🎯 未来改进建议 + +1. **实时日志流**:通过 WebSocket 实时推送日志,而不是轮询 +2. **日志搜索**:添加日志搜索和过滤功能 +3. **历史记录**:记录模型启动历史和失败次数 +4. **告警功能**:模型启动失败时发送告警 +5. **性能优化**:使用虚拟滚动处理大量日志 + +--- + +**实施日期**:2025-01-06 +**实施者**:AI Assistant +**版本**:1.0.0 + diff --git a/sdcard/web/index.htm b/sdcard/web/index.htm index 0635b10..21f9f56 100644 --- a/sdcard/web/index.htm +++ b/sdcard/web/index.htm @@ -692,6 +692,168 @@ margin-left: 0px !important; } + /* 折叠功能样式 */ + .collapsible-header { + cursor: pointer; + user-select: none; + position: relative; + padding-right: 40px !important; + } + + .collapse-icon { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + font-size: 1.2rem; + transition: transform 0.3s ease; + } + + .collapse-icon.collapsed { + transform: translateY(-50%) rotate(-90deg); + } + + .collapsible-content { + max-height: 5000px; + overflow: hidden; + transition: max-height 0.5s ease, opacity 0.3s ease; + opacity: 1; + } + + .collapsible-content.collapsed { + max-height: 0; + opacity: 0; + } + + /* 模型状态监控样式 */ + .model-status-progress { + position: relative; + width: 100%; + height: 20px; + background-color: #e0e0e0; + border-radius: 10px; + overflow: hidden; + margin: 10px 0; + } + + .model-status-progress-fill { + height: 100%; + background: linear-gradient(90deg, #4CAF50, #45a049); + border-radius: 10px; + transition: width 0.5s ease; + position: relative; + overflow: hidden; + } + + /* 进度条光线滚动动画 */ + @keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } + } + + .model-status-progress-fill.loading { + background: linear-gradient( + 90deg, + #4CAF50 0%, + #81C784 50%, + #4CAF50 100% + ); + background-size: 1000px 100%; + animation: shimmer 2s infinite linear; + } + + .model-status-text { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + color: white; + font-size: 0.85rem; + font-weight: bold; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); + white-space: nowrap; + z-index: 10; + min-width: 40px; + text-align: right; + } + + /* 当进度条宽度很小时,文字显示在进度条外部 */ + .model-status-progress-fill[style*="width: 0%"] .model-status-text, + .model-status-progress-fill[style*="width: 1"] .model-status-text { + color: #666; + text-shadow: none; + } + + .model-log-window { + background: #1e1e1e; + color: #d4d4d4; + font-family: 'Courier New', monospace; + font-size: 0.75rem; + padding: 15px; + border-radius: 8px; + max-height: 0; + overflow-y: auto; + transition: max-height 0.3s ease; + margin-top: 10px; + line-height: 1.4; + } + + .model-log-window.expanded { + max-height: 400px; /* 温度监控高度的2倍 */ + } + + .model-log-window::-webkit-scrollbar { + width: 8px; + } + + .model-log-window::-webkit-scrollbar-track { + background: #2d2d2d; + border-radius: 4px; + } + + .model-log-window::-webkit-scrollbar-thumb { + background: #555; + border-radius: 4px; + } + + .model-log-window::-webkit-scrollbar-thumb:hover { + background: #666; + } + + .model-log-line { + padding: 2px 0; + word-wrap: break-word; + } + + .model-log-timestamp { + color: #858585; + } + + .model-section-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background: rgba(255,255,255,0.05); + border-radius: 8px; + margin: 10px 0; + cursor: pointer; + user-select: none; + } + + .model-section-header:hover { + background: rgba(255,255,255,0.1); + } + + .model-section-title { + font-weight: 600; + font-size: 1rem; + } + /* 版权信息样式 */ .copyright { text-align: center; @@ -753,11 +915,15 @@

RM - 01 Dashboard

-
-

推理模组监控

-

NVIDIA 推理服务器系统状态

+
+
+

推理模组监控

+

NVIDIA 推理服务器系统状态

+
+
+
@@ -838,15 +1004,20 @@

推理模组监控

+
-
-

应用模组监控

-

Intel X86 应用服务器系统状态

+
+
+

应用模组监控

+

Intel X86 应用服务器系统状态

+
+
+
@@ -925,7 +1096,100 @@

应用模组监控

+
+ + + +
+
+
+

模型状态监控

+

推理模型加载和运行状态

+
+ +
+ +
+
+ +
+
+
🚀
+
模型拉起状态
+
+
+ +
+
+ 主模型 +
+
+
+ -- +
+
+
+ + +
+
+ 嵌入模型 +
+
+
+ -- +
+
+
+ + +
+
+ 重排模型 +
+
+
+ -- +
+
+
+
+
+ + +
+
+
📝
+
模型运行日志
+
+
+ +
+ 主模型 + +
+
+ + +
+ 嵌入模型 + +
+
+ + +
+ 重排模型 + +
+
+
+
+
+
+
+ @@ -1640,6 +1904,276 @@

应用模组监控

} } + // 折叠功能 + function toggleSection(sectionId) { + const content = document.getElementById(sectionId); + const icon = document.getElementById(sectionId + '-icon'); + + if (content && icon) { + content.classList.toggle('collapsed'); + icon.classList.toggle('collapsed'); + + // 保存折叠状态 + const isCollapsed = content.classList.contains('collapsed'); + localStorage.setItem(sectionId + '-collapsed', isCollapsed); + } + } + + // 恢复折叠状态 + function restoreCollapseStates() { + const sections = ['inference-section', 'application-section', 'model-section']; + sections.forEach(sectionId => { + const isCollapsed = localStorage.getItem(sectionId + '-collapsed') === 'true'; + if (isCollapsed) { + const content = document.getElementById(sectionId); + const icon = document.getElementById(sectionId + '-icon'); + if (content && icon) { + content.classList.add('collapsed'); + icon.classList.add('collapsed'); + } + } + }); + } + + // 切换模型日志窗口 + function toggleModelLog(modelType) { + const logWindow = document.getElementById(modelType + '-log-window'); + const icon = document.getElementById(modelType + '-log-icon'); + + if (logWindow && icon) { + logWindow.classList.toggle('expanded'); + icon.classList.toggle('collapsed'); + + // 如果展开,立即获取日志并滚动到底部 + if (logWindow.classList.contains('expanded')) { + // 检查模型是否已启用 + const modelData = modelDataCache[modelType]; + if (modelData && modelData.is_enabled) { + fetchModelLogs(modelType); + } + + setTimeout(() => { + logWindow.scrollTop = logWindow.scrollHeight; + }, 300); + } + } + } + + // 模型状态监控 + let modelStatusCheckInterval = null; + let modelDataCache = {}; + + function startModelStatusMonitoring() { + // 立即检查一次 + checkModelStatus(); + + // 每2秒检查一次 + modelStatusCheckInterval = setInterval(checkModelStatus, 2000); + } + + function stopModelStatusMonitoring() { + if (modelStatusCheckInterval) { + clearInterval(modelStatusCheckInterval); + modelStatusCheckInterval = null; + } + } + + async function checkModelStatus() { + try { + // 使用相对路径,自动适配当前访问的服务器地址 + const response = await fetch('/api/model_status'); + if (response.ok) { + const data = await response.json(); + if (data.is_valid) { + updateModelStatus(data); + } else { + // 数据无效时显示默认状态 + console.debug('模型状态数据无效'); + } + } + } catch (error) { + console.error('获取模型状态失败:', error); + } + } + + function updateModelStatus(data) { + // 更新三个模型的状态 + updateModelProgress('llm', data.llm); + updateModelProgress('embedding', data.embedding); + updateModelProgress('reranker', data.reranker); + + // 缓存数据用于日志显示 + modelDataCache = data; + } + + function updateModelProgress(modelType, modelData) { + const progressBar = document.getElementById(modelType + '-progress'); + const statusText = document.getElementById(modelType + '-status-text'); + const progressContainer = progressBar?.parentElement; + + if (!progressBar || !statusText) return; + + // 如果没有有效数据,保持初始状态 + if (!modelData) { + progressBar.style.width = '100%'; // 使用满宽以显示文字 + statusText.textContent = '--'; + progressBar.classList.remove('loading'); + progressBar.style.background = 'linear-gradient(90deg, #E0E0E0, #BDBDBD)'; + statusText.style.color = '#666'; + return; + } + + // 计算显示进度 + const actualProgress = modelData.progress || 0; + // 确保进度条至少有一定宽度以显示文字 + const displayProgress = actualProgress > 0 ? Math.max(actualProgress, 20) : 100; + progressBar.style.width = displayProgress + '%'; + + // 更新状态文本 + statusText.textContent = modelData.status_text || '--'; + + // 根据状态决定是否显示加载动画和颜色 + const isLoading = actualProgress > 0 && actualProgress < 100 && modelData.is_enabled; + + if (isLoading) { + // 正在加载 - 使用 CSS 动画,清除 inline background 让 CSS 类生效 + progressBar.classList.add('loading'); + progressBar.style.background = ''; // 清除 inline style,让 CSS .loading 类生效 + statusText.style.color = 'white'; + } else { + progressBar.classList.remove('loading'); + + // 设置进度条颜色和文字颜色 + if (!modelData.is_enabled) { + // 未启用 - 灰色背景 + progressBar.style.background = 'linear-gradient(90deg, #E0E0E0, #BDBDBD)'; + statusText.style.color = '#666'; + } else if (modelData.startup_complete) { + // 启动完成 - 绿色 + progressBar.style.background = 'linear-gradient(90deg, #4CAF50, #45a049)'; + statusText.style.color = 'white'; + } else if (actualProgress === 0) { + // 等待中 - 浅灰色 + progressBar.style.background = 'linear-gradient(90deg, #E0E0E0, #BDBDBD)'; + statusText.style.color = '#666'; + } else { + // 其他状态 - 默认灰色 + progressBar.style.background = 'linear-gradient(90deg, #BDBDBD, #9E9E9E)'; + statusText.style.color = 'white'; + } + } + } + + // 模型类型到翻译键的映射 + const modelTypeToTranslationKey = { + 'llm': 'main-model', + 'embedding': 'embedding-model', + 'reranker': 'reranker-model' + }; + + // 推理服务器地址(用于获取日志) + const INFERENCE_SERVER_URL = 'http://10.10.99.98:58090'; + + // 日志获取状态 + const logFetchState = { + llm: { lastFetch: 0, enabled: false }, + embedding: { lastFetch: 0, enabled: false }, + reranker: { lastFetch: 0, enabled: false } + }; + + // 更新日志窗口显示 + function updateModelLogs() { + const models = ['llm', 'embedding', 'reranker']; + models.forEach(modelType => { + const logWindow = document.getElementById(modelType + '-log-window'); + if (!logWindow) return; + + const modelData = modelDataCache[modelType]; + + // 获取正确的翻译键 + const translationKey = modelTypeToTranslationKey[modelType] || modelType; + const modelName = getTranslation(translationKey, modelType); + + // 如果数据还未加载,显示等待状态 + if (!modelData) { + if (logWindow.innerHTML === '') { + logWindow.innerHTML = `
[--:--:--] 等待 ${modelName} 状态数据...
`; + } + return; + } + + // 如果未启用,显示提示 + if (!modelData.is_enabled) { + logFetchState[modelType].enabled = false; + const translation = getTranslation('model-not-enabled', '未启用'); + logWindow.innerHTML = `
[--:--:--] ${translation}${modelName}
`; + } else { + // 模型已启用,检查是否需要获取日志 + const isExpanded = logWindow.classList.contains('expanded'); + + if (isExpanded && !logFetchState[modelType].enabled) { + // 刚展开,开始获取日志 + logFetchState[modelType].enabled = true; + fetchModelLogs(modelType); + } else if (!isExpanded) { + logFetchState[modelType].enabled = false; + } else if (isExpanded && logFetchState[modelType].enabled) { + // 已展开且已启用,定期刷新 + const now = Date.now(); + if (now - logFetchState[modelType].lastFetch > 5000) { // 每5秒刷新 + fetchModelLogs(modelType); + } + } + + // 如果日志窗口为空或显示等待状态,显示初始提示 + if (logWindow.innerHTML === '' || logWindow.innerHTML.includes('未启用') || logWindow.innerHTML.includes('等待')) { + const now = new Date(); + const timestamp = now.toTimeString().split(' ')[0]; + logWindow.innerHTML = `
[${timestamp}] 点击展开查看 ${modelName} 日志...
`; + } + } + }); + } + + // 从推理服务器获取日志 + async function fetchModelLogs(modelType) { + const logWindow = document.getElementById(modelType + '-log-window'); + if (!logWindow) return; + + try { + logFetchState[modelType].lastFetch = Date.now(); + const response = await fetch(`${INFERENCE_SERVER_URL}/api/model_logs/${modelType}?lines=100`); + + if (response.ok) { + const data = await response.json(); + if (data.logs && data.logs.length > 0) { + // 渲染日志 + const logsHtml = data.logs.map(line => + `
${escapeHtml(line)}
` + ).join(''); + logWindow.innerHTML = logsHtml; + + // 滚动到底部 + logWindow.scrollTop = logWindow.scrollHeight; + } else { + const translationKey = modelTypeToTranslationKey[modelType]; + const modelName = getTranslation(translationKey, modelType); + logWindow.innerHTML = `
[${new Date().toTimeString().split(' ')[0]}] ${modelName} 暂无日志输出
`; + } + } + } catch (error) { + console.debug(`获取 ${modelType} 日志失败:`, error); + // 不覆盖现有内容,静默失败 + } + } + + // HTML 转义函数 + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + // 页面加载完成后初始化监控 document.addEventListener('DOMContentLoaded', () => { // 初始化监控类并保存为全局变量以便语言切换时使用 @@ -1651,6 +2185,15 @@

应用模组监控

// 初始化语言系统 initLanguageSystem(); + + // 恢复折叠状态 + restoreCollapseStates(); + + // 启动模型状态监控 + startModelStatusMonitoring(); + + // 定期更新日志显示 + setInterval(updateModelLogs, 3000); }); // 语言翻译系统 @@ -1686,7 +2229,15 @@

应用模组监控

'system-5v': '系统 5V', 'gpu-freq': '3D GPU 频率', 'no-gpu-data': '暂无 GPU 数据', - 'no-memory-data': '暂无内存数据' + 'no-memory-data': '暂无内存数据', + 'model-monitoring': '模型状态监控', + 'model-status-desc': '推理模型加载和运行状态', + 'model-loading-status': '模型拉起状态', + 'model-runtime-logs': '模型运行日志', + 'main-model': '主模型', + 'embedding-model': '嵌入模型', + 'reranker-model': '重排模型', + 'model-not-enabled': '未启用' }, 'en': { 'title': 'RM - 01 Dashboard', @@ -1719,7 +2270,15 @@

应用模组监控

'system-5v': 'System 5V', 'gpu-freq': '3D GPU Frequency', 'no-gpu-data': 'No GPU Data Available', - 'no-memory-data': 'No Memory Data Available' + 'no-memory-data': 'No Memory Data Available', + 'model-monitoring': 'Model Status Monitor', + 'model-status-desc': 'Inference Model Loading and Runtime Status', + 'model-loading-status': 'Model Loading Status', + 'model-runtime-logs': 'Model Runtime Logs', + 'main-model': 'Main Model', + 'embedding-model': 'Embedding Model', + 'reranker-model': 'Reranker Model', + 'model-not-enabled': 'Not Enabled' } }; From f96b2df2c3f1020eb0eaaae216415eae91615807 Mon Sep 17 00:00:00 2001 From: Alfred Date: Tue, 6 Jan 2026 20:41:04 +0800 Subject: [PATCH 02/14] fix: Correct model monitoring UI issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move model status monitor section above inference module monitor - Fix log section collapse icons overlapping (add position: relative) - Remove emojis from card headers - Update subtitle to '模型加载进度和运行日志监控' - Fix HTML structure (model section was outside main-panel) - Fix model section header collapse icon CSS for proper flex layout --- sdcard/web/index.htm | 195 +++++++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 92 deletions(-) diff --git a/sdcard/web/index.htm b/sdcard/web/index.htm index 21f9f56..6e45204 100644 --- a/sdcard/web/index.htm +++ b/sdcard/web/index.htm @@ -843,6 +843,7 @@ margin: 10px 0; cursor: pointer; user-select: none; + position: relative; } .model-section-header:hover { @@ -854,6 +855,17 @@ font-size: 1rem; } + /* 覆盖模型日志折叠图标的定位,使其在flex布局中正常工作 */ + .model-section-header .collapse-icon { + position: static; + transform: none; + transition: transform 0.3s ease; + } + + .model-section-header .collapse-icon.collapsed { + transform: rotate(-90deg); + } + /* 版权信息样式 */ .copyright { text-align: center; @@ -913,6 +925,96 @@

RM - 01 Dashboard

+ +
+
+
+

模型状态监控

+

模型加载进度和运行日志监控

+
+ +
+ +
+
+ +
+
+
+
模型拉起状态
+
+
+ +
+
+ 主模型 +
+
+
+ -- +
+
+
+ + +
+
+ 嵌入模型 +
+
+
+ -- +
+
+
+ + +
+
+ 重排模型 +
+
+
+ -- +
+
+
+
+
+ + +
+
+
+
模型运行日志
+
+
+ +
+ 主模型 + +
+
+ + +
+ 嵌入模型 + +
+
+ + +
+ 重排模型 + +
+
+
+
+
+
+
+
@@ -1098,97 +1200,6 @@

应用模组监控

-
- - -
-
-
-

模型状态监控

-

推理模型加载和运行状态

-
- -
- -
-
- -
-
-
🚀
-
模型拉起状态
-
-
- -
-
- 主模型 -
-
-
- -- -
-
-
- - -
-
- 嵌入模型 -
-
-
- -- -
-
-
- - -
-
- 重排模型 -
-
-
- -- -
-
-
-
-
- - -
-
-
📝
-
模型运行日志
-
-
- -
- 主模型 - -
-
- - -
- 嵌入模型 - -
-
- - -
- 重排模型 - -
-
-
-
-
-
-
@@ -2272,7 +2283,7 @@

模型状态监控

'no-gpu-data': 'No GPU Data Available', 'no-memory-data': 'No Memory Data Available', 'model-monitoring': 'Model Status Monitor', - 'model-status-desc': 'Inference Model Loading and Runtime Status', + 'model-status-desc': 'Model Loading Progress and Runtime Logs', 'model-loading-status': 'Model Loading Status', 'model-runtime-logs': 'Model Runtime Logs', 'main-model': 'Main Model', From 8d5f2122ead49dbc1a3716d85ad58783affcaafb Mon Sep 17 00:00:00 2001 From: Alfred Date: Tue, 6 Jan 2026 20:47:03 +0800 Subject: [PATCH 03/14] style: Update model log UI to match dashboard theme - Change collapse icons color to green (#4CAF50) to match progress bars - Change log window background from dark (#1e1e1e) to light gray (#f8f9fa) - Change log text color from light gray to dark gray (#333) - Update scrollbar colors to match light theme - Add border to log windows for consistency with other cards --- sdcard/web/index.htm | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sdcard/web/index.htm b/sdcard/web/index.htm index 6e45204..10ca4ef 100644 --- a/sdcard/web/index.htm +++ b/sdcard/web/index.htm @@ -789,8 +789,8 @@ } .model-log-window { - background: #1e1e1e; - color: #d4d4d4; + background: #f8f9fa; + color: #333; font-family: 'Courier New', monospace; font-size: 0.75rem; padding: 15px; @@ -800,6 +800,7 @@ transition: max-height 0.3s ease; margin-top: 10px; line-height: 1.4; + border: 1px solid #e9ecef; } .model-log-window.expanded { @@ -811,17 +812,17 @@ } .model-log-window::-webkit-scrollbar-track { - background: #2d2d2d; + background: #e9ecef; border-radius: 4px; } .model-log-window::-webkit-scrollbar-thumb { - background: #555; + background: #bbb; border-radius: 4px; } .model-log-window::-webkit-scrollbar-thumb:hover { - background: #666; + background: #999; } .model-log-line { @@ -830,7 +831,7 @@ } .model-log-timestamp { - color: #858585; + color: #666; } .model-section-header { @@ -860,6 +861,7 @@ position: static; transform: none; transition: transform 0.3s ease; + color: #4CAF50; } .model-section-header .collapse-icon.collapsed { From 5512bea2711c14d3d702832c7a70739e066743f3 Mon Sep 17 00:00:00 2001 From: Alfred Date: Tue, 6 Jan 2026 20:50:17 +0800 Subject: [PATCH 04/14] chore: Update copyright information to 2026 - Change year from 2025 to 2026 - Update company name to 'RMinte (Chengdu) Artificial Intelligence Technology Co., Ltd.' - Add website URL: www.RMinte.com - Remove 'Panidea' from copyright text --- sdcard/web/index.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdcard/web/index.htm b/sdcard/web/index.htm index 10ca4ef..c1232ef 100644 --- a/sdcard/web/index.htm +++ b/sdcard/web/index.htm @@ -1208,7 +1208,7 @@

应用模组监控