diff --git a/.gitignore b/.gitignore
index 394984f..679187f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,4 +36,12 @@ specs
.omc
.playwright-mcp
.jqwik-database
-.DS_Store
\ No newline at end of file
+.DS_Store
+
+# Eclipse / m2e IDE files
+.classpath
+.project
+.settings/
+backend/.classpath
+backend/.project
+backend/.settings/
\ No newline at end of file
diff --git a/backend/src/main/java/org/apache/tsfile/viewer/config/WebConfig.java b/backend/src/main/java/org/apache/tsfile/viewer/config/WebConfig.java
index 448f5d5..f6fa3f3 100644
--- a/backend/src/main/java/org/apache/tsfile/viewer/config/WebConfig.java
+++ b/backend/src/main/java/org/apache/tsfile/viewer/config/WebConfig.java
@@ -20,10 +20,12 @@
package org.apache.tsfile.viewer.config;
import java.io.IOException;
+import java.time.Duration;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
+import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -44,8 +46,18 @@ public class WebConfig implements WebMvcConfigurer {
* Configure resource handlers for serving static frontend assets.
*
*
Maps /view/** requests to classpath:/static/view/ directory. Implements SPA routing by
- * falling back to index.html for all non-existent resources (client-side routes). Enables caching
- * with 1-hour cache period.
+ * falling back to index.html for all non-existent resources (client-side routes).
+ *
+ *
Caching strategy follows the standard hashed-SPA pattern:
+ *
+ *
+ *
{@code /view/assets/**} — build output with content-hashed filenames (e.g. {@code
+ * index-BXJsXIsM.js}). Safe to cache forever ({@code max-age=1y, immutable}); a content
+ * change produces a new filename, so it can never go stale.
+ *
{@code index.html} and SPA routes — the entry point that references those hashes. Served
+ * {@code no-cache} (revalidate every load) so a new deployment is picked up immediately
+ * instead of being masked by a cached entry point for up to the cache period.
+ *
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
@@ -55,11 +67,17 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
.addResourceLocations("classpath:/static/view/")
.setCachePeriod(86400); // 24 hours cache
- // Handle frontend SPA routes
+ // Content-hashed build assets: immutable, cache for a year.
+ registry
+ .addResourceHandler("/view/assets/**")
+ .addResourceLocations("classpath:/static/view/assets/")
+ .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic().immutable());
+
+ // Frontend SPA entry + client-side routes: always revalidate so deploys take effect at once.
registry
.addResourceHandler("/view", "/view/", "/view/**")
.addResourceLocations("classpath:/static/view/")
- .setCachePeriod(3600) // 1 hour cache
+ .setCacheControl(CacheControl.noCache())
.resourceChain(true)
.addResolver(
new PathResourceResolver() {
diff --git a/backend/src/main/java/org/apache/tsfile/viewer/service/DataService.java b/backend/src/main/java/org/apache/tsfile/viewer/service/DataService.java
index b73f7a7..50362c9 100644
--- a/backend/src/main/java/org/apache/tsfile/viewer/service/DataService.java
+++ b/backend/src/main/java/org/apache/tsfile/viewer/service/DataService.java
@@ -65,6 +65,14 @@ public class DataService {
private static final Logger logger = LoggerFactory.getLogger(DataService.class);
private static final int DEFAULT_MAX_POINTS = 1000;
+ /**
+ * Hard upper bound on the number of chart series built per request. Acts as a performance safety
+ * net so a request that selects thousands of measurements (e.g. a wide table model file) cannot
+ * make the backend build and serialize an unbounded number of series. The frontend additionally
+ * refuses to render beyond its own (lower) display threshold.
+ */
+ private static final int MAX_CHART_SERIES = 200;
+
private final TsFileProperties tsFileProperties;
private final FileService fileService;
private final TsFileDataReader dataReader;
@@ -229,11 +237,19 @@ public ChartDataResponse queryChartData(ChartDataRequest request)
int maxPoints = request.getMaxPoints() != null ? request.getMaxPoints() : DEFAULT_MAX_POINTS;
+ boolean seriesCapped = false;
+ outer:
for (Map.Entry> deviceEntry : deviceGroups.entrySet()) {
String device = deviceEntry.getKey();
List deviceData = deviceEntry.getValue();
for (String measurement : request.getMeasurements()) {
+ // Performance safety net: never build more than MAX_CHART_SERIES series.
+ if (series.size() >= MAX_CHART_SERIES) {
+ seriesCapped = true;
+ break outer;
+ }
+
List dataPoints = extractMeasurementData(deviceData, measurement);
if (dataPoints.isEmpty()) continue;
@@ -260,6 +276,15 @@ public ChartDataResponse queryChartData(ChartDataRequest request)
}
}
+ if (seriesCapped) {
+ logger.warn(
+ "Chart series capped at {} for fileId={} (requested {} measurements across {} devices)",
+ MAX_CHART_SERIES,
+ request.getFileId(),
+ request.getMeasurements().size(),
+ deviceGroups.size());
+ }
+
// Calculate time range
TimeRange timeRange = calculateTimeRange(allData);
diff --git a/frontend/src/components/tsfile/ChartPanel.vue b/frontend/src/components/tsfile/ChartPanel.vue
index 905e4fc..c4756b4 100644
--- a/frontend/src/components/tsfile/ChartPanel.vue
+++ b/frontend/src/components/tsfile/ChartPanel.vue
@@ -66,6 +66,10 @@ const props = withDefaults(defineProps(), {
const { t } = useI18n();
+// 单图最多渲染的序列数,超过则拒绝绘制并提示(防止数千条折线卡死浏览器)
+const MAX_CHART_SERIES = 50;
+const tooManySeries = computed(() => props.series.length > MAX_CHART_SERIES);
+
const chartRef = ref(null);
let chartInstance: echarts.ECharts | null = null;
const chartHeight = ref(500);
@@ -138,6 +142,7 @@ const chartOption = computed(() => {
function initChart() {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
+ if (tooManySeries.value) return;
chartInstance.setOption(chartOption.value);
}
@@ -147,6 +152,11 @@ function updateChart() {
initChart();
return;
}
+ // 序列过多时不渲染,仅清空,交由模板展示告警
+ if (tooManySeries.value) {
+ chartInstance.clear();
+ return;
+ }
chartInstance.setOption(chartOption.value, true);
nextTick(() => chartInstance?.resize());
}
@@ -215,15 +225,23 @@ onUnmounted(() => {
show-icon
class="mb-2 flex-shrink-0"
/>
+