This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 36126024c chore(studio): low-risk production hardening and code 
cleanup (#2650)
36126024c is described below

commit 36126024cbf75ae2e0f27cc074b2f39ceb6c8b6e
Author: zhaohai <[email protected]>
AuthorDate: Fri Sep 4 16:41:04 2026 +0800

    chore(studio): low-risk production hardening and code cleanup (#2650)
    
    * chore(studio): low-risk production hardening and code cleanup
    
    - Remove empty SecurityConfig placeholder (no Spring Security on classpath)
    - Enable in-process caching for settings datasource endpoints
    - Tune HikariCP pool size and logback rotation limits
    - Add nginx hashed-asset caching and pin Docker base images
    - Promote no-explicit-any to error; dedupe format helpers in topic.tsx
    
    Verified: frontend tsc -b clean, backend test-compile clean, targeted
    settings/message service+controller tests pass.
    
    * remove md
---
 deploy/nginx.conf                                  | 13 +++++++++++
 server/Dockerfile                                  |  3 +++
 .../apache/rocketmq/studio/StudioApplication.java  |  2 ++
 .../rocketmq/studio/auth/SecurityConfig.java       | 25 ----------------------
 .../studio/instance/message/MessageService.java    |  5 +++++
 .../rocketmq/studio/settings/SettingsService.java  | 11 ++++++++++
 server/src/main/resources/application.yml          | 11 ++++++++++
 server/src/main/resources/logback-spring.xml       |  5 +++--
 web/Dockerfile                                     |  6 ++++--
 web/eslint.config.js                               |  5 ++++-
 web/src/pages/instance/topic.tsx                   | 12 +----------
 web/src/pages/settings/GeneralSettingsTab.tsx      |  6 ++++--
 12 files changed, 61 insertions(+), 43 deletions(-)

diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 9d9723352..2e23acc93 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -9,6 +9,19 @@ server {
         try_files $uri $uri/ /index.html;
     }
 
+    # Hashed asset bundles are content-addressable: long-cache them with
+    # `immutable` so repeat visits skip re-downloading the ~1.78 MB JS payload
+    # (antd vendor + app entry) entirely. The HTML entry stays short-lived so
+    # the next deploy reaches users immediately.
+    location /assets/ {
+        root /usr/share/nginx/html;
+        expires 1y;
+        add_header Cache-Control "public, max-age=31536000, immutable";
+    }
+    location = /index.html {
+        add_header Cache-Control "no-cache";
+    }
+
     location /api/ {
         proxy_pass http://rocketmq-server:8888;
         proxy_set_header Host $host;
diff --git a/server/Dockerfile b/server/Dockerfile
index b4f380efc..2e2b1b1c9 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -1,4 +1,7 @@
 # Shared runtime dependencies
+# Note: a pinned dragonwell patch tag (e.g. 21.0.10-anolis) would be 
preferable for
+# reproducibility, but such tags are not reachable through the deployment 
registry
+# mirrors in use, so keep the rolling :21 tag until a verifiable pin is 
available.
 FROM alibabadragonwell/dragonwell:21 AS runtime-base
 WORKDIR /app
 # Node.js + agent CLIs for the claude-code / qoder agent providers.
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java 
b/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
index 38a537af1..1ea2baee9 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
@@ -19,10 +19,12 @@ package org.apache.rocketmq.studio;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
 @EnableScheduling
+@EnableCaching
 public class StudioApplication {
     public static void main(String[] args) {
         SpringApplication.run(StudioApplication.class, args);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/SecurityConfig.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/SecurityConfig.java
deleted file mode 100644
index 927d098d6..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/SecurityConfig.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.rocketmq.studio.auth;
-
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class SecurityConfig {
-    // TODO: Add Spring Security configuration when Spring Security dependency 
is added
-}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
index 6b391c943..0e29caf91 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
@@ -67,6 +67,11 @@ public class MessageService {
         if (page < 1 || pageSize < 1 || pageSize > MAX_PAGE_SIZE) {
             throw new BusinessException(400, "page must be positive and 
pageSize must be between 1 and 200");
         }
+        // The Apache / Aliyun / Tencent providers all cap topic-keyed scans 
at a fixed
+        // broker-side limit (see 
RocketMQMessageProvider#DEFAULT_TOPIC_LIMIT). RocketMQ has
+        // no server-side "skip the first N messages" API, so the in-memory 
subList below is
+        // the page slice within that cap; the total / resultMayBeTruncated 
fields let the UI
+        // tell the user when deeper pages may be empty.
         MessageQueryResult queryResult = queryMessagesDetailed(
                 instanceId, topic, msgId, tag, key, startTime, endTime, page 
== 1);
         List<MessageRecordVO> result = queryResult.messages();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
index 4b9177c50..7847297d4 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
@@ -29,6 +29,8 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -206,11 +208,17 @@ public class SettingsService {
     }
 
 
+    // The full-list endpoint is hit by every metrics tab on first paint and
+    // every time the datasource dropdown re-fetches. Caching it with the
+    // write paths evicted below keeps the user-visible list correct while
+    // removing a per-tab DB round trip.
+    @Cacheable("data-sources")
     public List<DataSourceVO> listDataSources() {
         log.debug("Listing all data sources");
         return settingsRepository.findAllDataSources();
     }
 
+    @Cacheable(value = "data-sources", key = "'page:' + #search + ':' + #type 
+ ':' + #page + ':' + #pageSize")
     public PageResult<DataSourceVO> listDataSources(String search, String 
type, int page, int pageSize) {
         if (page < 1) {
             throw new BusinessException(400, "page must be greater than zero");
@@ -224,6 +232,7 @@ public class SettingsService {
     }
 
 
+    @CacheEvict(value = "data-sources", allEntries = true)
     public DataSourceVO createDataSource(DataSourceVO dataSource) {
         if (dataSource == null) {
             throw new BusinessException(400, "Data source request is 
required");
@@ -236,6 +245,7 @@ public class SettingsService {
     }
 
 
+    @CacheEvict(value = "data-sources", allEntries = true)
     public DataSourceVO updateDataSource(DataSourceVO dataSource) {
         if (dataSource == null) {
             throw new BusinessException(400, "Data source request is 
required");
@@ -260,6 +270,7 @@ public class SettingsService {
     }
 
 
+    @CacheEvict(value = "data-sources", allEntries = true)
     public void deleteDataSource(String key) {
         String normalizedKey = normalizeDataSourceKey(key);
         log.info("Deleting data source: {}", normalizedKey);
diff --git a/server/src/main/resources/application.yml 
b/server/src/main/resources/application.yml
index 6efbf2bb5..0b248c8c5 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -11,6 +11,17 @@ spring:
     username: ${SPRING_DATASOURCE_USERNAME:root}
     password: ${SPRING_DATASOURCE_PASSWORD:studio123}
     driver-class-name: com.mysql.cj.jdbc.Driver
+    hikari:
+      # Studio is polled by multiple tabs / dashboards; the Spring Boot 
default of 10
+      # connections saturates fast under concurrent filters + alerting. 30 
leaves
+      # headroom without overwhelming a default MySQL max-connections=151.
+      maximum-pool-size: 30
+      minimum-idle: 5
+      connection-timeout: 5000
+      idle-timeout: 300000
+      max-lifetime: 1200000
+      pool-name: studio-pool
+      leak-detection-threshold: 30000
   mail:
     host: ${STUDIO_ALERTING_SMTP_HOST:}
     port: ${STUDIO_ALERTING_SMTP_PORT:587}
diff --git a/server/src/main/resources/logback-spring.xml 
b/server/src/main/resources/logback-spring.xml
index 6342b99ef..8178b497e 100644
--- a/server/src/main/resources/logback-spring.xml
+++ b/server/src/main/resources/logback-spring.xml
@@ -11,8 +11,9 @@
         <file>logs/rocketmq-studio/studio.log</file>
         <rollingPolicy 
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
             
<fileNamePattern>logs/rocketmq-studio/otherdays/studio.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
-            <maxFileSize>1GB</maxFileSize>
-            <maxHistory>10</maxHistory>
+            <maxFileSize>100MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>5GB</totalSizeCap>
         </rollingPolicy>
         <encoder>
             <pattern>${FILE_LOG_PATTERN}</pattern>
diff --git a/web/Dockerfile b/web/Dockerfile
index a42e7bd7a..f5beaefce 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,5 +1,7 @@
 # Stage 1: Build
-FROM node:20-alpine AS build
+# Pin Node and nginx base images to a specific minor so CI builds are
+# reproducible (the bare :20-alpine and :alpine tags are rolling).
+FROM node:20.19.0-alpine AS build
 ARG VITE_GIT_COMMIT
 ENV VITE_GIT_COMMIT=${VITE_GIT_COMMIT}
 WORKDIR /app
@@ -9,7 +11,7 @@ COPY . .
 RUN npm run build
 
 # Stage 2: Serve
-FROM nginx:alpine
+FROM nginx:1.27.2-alpine
 ENV NGINX_ENVSUBST_FILTER=^RESOLVER$
 COPY --from=build /app/dist /usr/share/nginx/html
 COPY nginx.conf /etc/nginx/templates/default.conf.template
diff --git a/web/eslint.config.js b/web/eslint.config.js
index cb4f6fceb..46a73af7d 100644
--- a/web/eslint.config.js
+++ b/web/eslint.config.js
@@ -20,7 +20,10 @@ export default tseslint.config(
       ...reactHooks.configs.recommended.rules,
       'react-refresh/only-export-components': ['warn', { allowConstantExport: 
true }],
       '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' 
}],
-      '@typescript-eslint/no-explicit-any': 'warn',
+      // The codebase had 0 explicit `any` at the 2026-08 cleanup; keep it 
that way
+      // by making the rule an error so any new escape hatch shows up in CI 
instead
+      // of slipping through as a warning.
+      '@typescript-eslint/no-explicit-any': 'error',
     },
   },
 );
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index e029b5da7..df927c046 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -86,6 +86,7 @@ import {
   type ResourceImportRow,
 } from '../../utils/resourceCsvImport';
 import { downloadCsv } from '../../utils/download';
+import { formatDateTime, formatNumber } from '../../utils/format';
 import { tableScrollX } from '../../utils/table';
 import {
   analyzeTopicRoutes,
@@ -287,17 +288,6 @@ const RANDOM_BODY_GENERATORS = [
   { label: '监控指标', fn: randomMetricsBody },
 ];
 
-// ─── Format helpers ───────────────────────────────────────────────
-const formatNumber = (n: number) => n.toLocaleString('zh-CN');
-
-const formatDateTime = (iso?: string): string => {
-  if (!iso) return '-';
-  const d = new Date(iso);
-  if (Number.isNaN(d.getTime())) return '-';
-  const pad = (n: number) => String(n).padStart(2, '0');
-  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 
${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
-};
-
 const ROUTE_STATUS_META: Record<
   RouteDiagnosticStatus,
   { color: string; label: string; icon: React.ReactNode }
diff --git a/web/src/pages/settings/GeneralSettingsTab.tsx 
b/web/src/pages/settings/GeneralSettingsTab.tsx
index facafb052..e7ce47fc3 100644
--- a/web/src/pages/settings/GeneralSettingsTab.tsx
+++ b/web/src/pages/settings/GeneralSettingsTab.tsx
@@ -187,8 +187,10 @@ export const GeneralSettingsTab = () => {
       if (!(await mergeAndSave(values))) return;
       await testNotification(channel);
       message.success(t('settings.testMessageSent'));
-    } catch (error: any) {
-      message.error(error?.response?.data?.message ?? 
t('settings.testMessageFailed'));
+    } catch (error) {
+      const apiMessage = (error as { response?: { data?: { message?: unknown } 
} })?.response?.data
+        ?.message;
+      message.error(typeof apiMessage === 'string' ? apiMessage : 
t('settings.testMessageFailed'));
     } finally {
       setTestingChannel(undefined);
     }

Reply via email to