dsh-recency-guard 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -61,6 +61,8 @@ var zh_default = {
61
61
  emptyHint: "\u5B88\u62A4\u63D2\u4EF6\u5C1A\u672A\u8BB0\u5F55\u4EFB\u4F55\u542F\u52A8\u4E8B\u5B9E\u3002",
62
62
  snapshotAt: "\u5FEB\u7167\u65F6\u523B {time}",
63
63
  pollingOn: "\u6BCF {seconds} \u79D2\u8F6E\u8BE2",
64
+ pollingBackoff: "\u6570\u636E\u7A33\u5B9A\uFF0C\u5DF2\u653E\u6162\u5237\u65B0",
65
+ pollingHint: "\u95F4\u9694\u6309\u5185\u5BB9\u7A33\u5B9A\u6027\u81EA\u9002\u5E94\uFF083\u201330 \u79D2\uFF09\uFF1B\u6570\u636E\u7A33\u5B9A\u65F6\u653E\u6162\uFF0C\u53D8\u5316\u65F6\u7ACB\u523B\u56DE\u5230 5 \u79D2\u3002",
64
66
  pollingPaused: "\u8F6E\u8BE2\u5DF2\u6682\u505C\uFF08\u753B\u9762\u505C\u5728\u6682\u505C\u65F6\u523B\uFF09",
65
67
  restore: "\u6062\u590D\u975E\u5ACC\u7591\u63D2\u4EF6",
66
68
  stale: "\u6570\u636E\u5DF2\u505C\u7559 {minutes} \u5206\u949F\uFF08\u8F6E\u8BE2\u5DF2\u6682\u505C\uFF09",
@@ -98,6 +100,7 @@ var zh_default = {
98
100
  markov: "\u8F6C\u79FB\u77E9\u9635",
99
101
  dataflow: "\u6570\u636E\u6D41",
100
102
  diagnostics: "\u7CFB\u7EDF\u8BCA\u65AD",
103
+ preventive: "\u9884\u8B66\uFF08\u53EA\u89C2\u6D4B\uFF09",
101
104
  cooccurrence: "\u5171\u73B0\u9A8C\u8BC1",
102
105
  calibration: "\u8BEF\u4F24\u6821\u51C6",
103
106
  bayes: "\u8D1D\u53F6\u65AF\u8BFB\u6570"
@@ -224,6 +227,32 @@ var zh_default = {
224
227
  note: "\u5907\u6CE8"
225
228
  }
226
229
  },
230
+ preventive: {
231
+ title: "\u9884\u9632\u6027\u9884\u8B66",
232
+ hint: "\u53EA\u9884\u8B66\uFF0C\u4E0D\u884C\u52A8",
233
+ alertOnly: "\u53EA\u9884\u8B66\uFF0C\u4E0D\u5173\u95ED\u4EFB\u4F55\u63D2\u4EF6 \u2014\u2014 \u52A8\u4F5C\u4ECD\u8D70\u53D6\u8BC1\u6D41\u7A0B\uFF08RQ1/RQ2\uFF09\u3002",
234
+ unavailable: "\u8BE5\u89C6\u56FE\u4E0D\u63D0\u4F9B\u9884\u9632\u6027\u8BFB\u6570\uFF08\u4E0D\u53EF\u5F97 \u2260 \u4E00\u5207\u6B63\u5E38\uFF09\u3002",
235
+ loadTime: "\u542F\u52A8\u9891\u7387\uFF08\u52A0\u8F7D\u8017\u65F6\u4EE3\u7406\uFF09",
236
+ configChanges: "\u914D\u7F6E\u53D8\u66F4\u9891\u7387\uFF08CUSUM\uFF09",
237
+ current: "\u5F53\u524D\u6C34\u5E73",
238
+ baseline: "\u4E2D\u5FC3\u7EBF",
239
+ upper: "\u4E0A\u63A7\u5236\u9650\uFF08\u8D8A\u754C\u9884\u8B66\uFF09",
240
+ lower: "\u4E0B\u63A7\u5236\u9650\uFF08\u5BF9\u7167\uFF09",
241
+ sigma: "\u7A33\u5065 \u03C3\u0302",
242
+ samples: "\u6837\u672C\u6570",
243
+ perMinute: "\u6B21/\u5206\u949F",
244
+ statistic: "\u7EDF\u8BA1\u91CF S",
245
+ threshold: "\u5224\u5B9A\u9608\u503C h",
246
+ reference: "\u53C2\u8003\u503C k",
247
+ baselineGap: "\u57FA\u7EBF\u95F4\u9694",
248
+ intervals: "\u95F4\u9694\u6570",
249
+ breach: "\u6F02\u79FB\u5224\u5B9A",
250
+ noBreach: "\u672A\u8D8A\u754C",
251
+ breachAt: "\u7B2C {index} \u4E2A\u95F4\u9694\u9996\u6B21\u8D8A\u754C",
252
+ alertYes: "\u9884\u8B66\uFF1A\u8D8A\u754C",
253
+ alertNo: "\u672A\u8D8A\u754C",
254
+ source: "\u6765\u6E90\uFF1A{boots} \u6B21\u542F\u52A8 / {changes} \u6B21\u53D8\u66F4 \xB7 \u5355\u4F4D {units}"
255
+ },
227
256
  graphStruct: {
228
257
  title: "\u7ED3\u6784\u6D1E\u5BDF",
229
258
  hubs: "\u67A2\u7EBD",
@@ -479,6 +508,8 @@ var en_default = {
479
508
  emptyHint: "The guard has not recorded any startup facts yet.",
480
509
  snapshotAt: "Snapshot at {time}",
481
510
  pollingOn: "polling every {seconds}s",
511
+ pollingBackoff: "data is stable, refresh slowed down",
512
+ pollingHint: "The interval adapts to content stability (3\u201330s); slower when stable, back to 5s on change.",
482
513
  pollingPaused: "polling paused (the view is frozen at the pause moment)",
483
514
  restore: "Restore non-suspects",
484
515
  stale: "Data is {minutes} min old (polling paused)",
@@ -516,6 +547,7 @@ var en_default = {
516
547
  markov: "Transition matrix",
517
548
  dataflow: "Data flow",
518
549
  diagnostics: "Diagnostics",
550
+ preventive: "Warnings (observe only)",
519
551
  cooccurrence: "Co-occurrence",
520
552
  calibration: "False-positive calibration",
521
553
  bayes: "Bayesian reading"
@@ -642,6 +674,32 @@ var en_default = {
642
674
  note: "note"
643
675
  }
644
676
  },
677
+ preventive: {
678
+ title: "Preventive warnings",
679
+ hint: "warn only, never act",
680
+ alertOnly: "Warnings only \u2014 no plugin is ever disabled here; actions still go through evidence (RQ1/RQ2).",
681
+ unavailable: "This view does not provide preventive readings (unavailable \u2260 all clear).",
682
+ loadTime: "Startup frequency (load-time proxy)",
683
+ configChanges: "Config change frequency (CUSUM)",
684
+ current: "Current level",
685
+ baseline: "Centre line",
686
+ upper: "Upper control limit (alert)",
687
+ lower: "Lower control limit (reference)",
688
+ sigma: "Robust \u03C3\u0302",
689
+ samples: "Samples",
690
+ perMinute: "per minute",
691
+ statistic: "Statistic S",
692
+ threshold: "Threshold h",
693
+ reference: "Reference k",
694
+ baselineGap: "Baseline gap",
695
+ intervals: "Intervals",
696
+ breach: "Drift verdict",
697
+ noBreach: "not breached",
698
+ breachAt: "first breached at interval {index}",
699
+ alertYes: "Alert: breached",
700
+ alertNo: "not breached",
701
+ source: "Source: {boots} startups / {changes} changes \xB7 units {units}"
702
+ },
645
703
  graphStruct: {
646
704
  title: "Structure",
647
705
  hubs: "Hubs",
@@ -3044,6 +3102,96 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
3044
3102
  // src/client/panel/PanelCards.tsx
3045
3103
  var import_react3 = require("react");
3046
3104
 
3105
+ // src/client/panel/preventive-model.ts
3106
+ function finite2(value) {
3107
+ return typeof value === "number" && Number.isFinite(value);
3108
+ }
3109
+ function percentOrValue(value, digits = 2) {
3110
+ return finite2(value) ? value.toFixed(digits) : t("metrics.undefined");
3111
+ }
3112
+ function preMinute(value) {
3113
+ return finite2(value) ? `${value.toFixed(2)} ${t("preventive.perMinute")}` : t("metrics.undefined");
3114
+ }
3115
+ function secondsOfMs(value, digits = 1) {
3116
+ return finite2(value) ? `${(value / 1e3).toFixed(digits)} s` : t("metrics.undefined");
3117
+ }
3118
+ function usable(metric, required) {
3119
+ return metric.defined === true && metric.value !== null && required(metric.value);
3120
+ }
3121
+ function preventiveSummary(snapshot) {
3122
+ const note = t("preventive.alertOnly");
3123
+ const preventive = snapshot.preventive;
3124
+ if (preventive === null || preventive === void 0) {
3125
+ return { available: false, reads: [], source: null, note, anyAlert: false };
3126
+ }
3127
+ const load = preventive.loadTime;
3128
+ const loadDefined = usable(
3129
+ load,
3130
+ (value) => finite2(value.current) && finite2(value.baseline) && finite2(value.upper) && finite2(value.lower) && finite2(value.sigma) && finite2(value.ratio)
3131
+ );
3132
+ const loadRead = {
3133
+ key: "loadTime",
3134
+ title: t("preventive.loadTime"),
3135
+ defined: loadDefined,
3136
+ reason: loadDefined ? null : load.reason ?? t("metrics.valueUndefined"),
3137
+ label: load.label,
3138
+ samples: finite2(load.samples) ? load.samples : 0,
3139
+ alert: loadDefined ? load.value.alert === true : false,
3140
+ rows: loadDefined ? [
3141
+ { key: "current", label: t("preventive.current"), value: preMinute(load.value.current) },
3142
+ { key: "baseline", label: t("preventive.baseline"), value: preMinute(load.value.baseline) },
3143
+ { key: "upper", label: t("preventive.upper"), value: preMinute(load.value.upper) },
3144
+ { key: "lower", label: t("preventive.lower"), value: preMinute(load.value.lower) },
3145
+ {
3146
+ key: "sigma",
3147
+ label: t("preventive.sigma"),
3148
+ value: `${percentOrValue(load.value.sigma)} (${load.value.sigmaEstimator})`
3149
+ },
3150
+ { key: "samples", label: t("preventive.samples"), value: String(load.samples) }
3151
+ ] : []
3152
+ };
3153
+ const config = preventive.configChanges;
3154
+ const configDefined = usable(
3155
+ config,
3156
+ (value) => finite2(value.statistic) && finite2(value.threshold) && finite2(value.reference) && finite2(value.intervals) && finite2(value.baselineMs)
3157
+ );
3158
+ const configRead = {
3159
+ key: "configChanges",
3160
+ title: t("preventive.configChanges"),
3161
+ defined: configDefined,
3162
+ reason: configDefined ? null : config.reason ?? t("metrics.valueUndefined"),
3163
+ label: config.label,
3164
+ samples: finite2(config.samples) ? config.samples : 0,
3165
+ alert: configDefined ? config.value.alert === true : false,
3166
+ rows: configDefined ? [
3167
+ { key: "statistic", label: t("preventive.statistic"), value: percentOrValue(config.value.statistic, 2) },
3168
+ { key: "threshold", label: t("preventive.threshold"), value: percentOrValue(config.value.threshold, 2) },
3169
+ { key: "reference", label: t("preventive.reference"), value: percentOrValue(config.value.reference, 2) },
3170
+ { key: "baseline", label: t("preventive.baselineGap"), value: secondsOfMs(config.value.baselineMs) },
3171
+ { key: "sigma", label: t("preventive.sigma"), value: secondsOfMs(config.value.sigmaMs) },
3172
+ { key: "intervals", label: t("preventive.intervals"), value: String(config.value.intervals) },
3173
+ {
3174
+ key: "breach",
3175
+ label: t("preventive.breach"),
3176
+ value: config.value.breachIndex === null ? t("preventive.noBreach") : t("preventive.breachAt", { index: config.value.breachIndex })
3177
+ }
3178
+ ] : []
3179
+ };
3180
+ const source = preventive.source;
3181
+ return {
3182
+ available: true,
3183
+ reads: [loadRead, configRead],
3184
+ source: source === void 0 || source === null ? null : {
3185
+ bootSamples: finite2(source.bootSamples) ? source.bootSamples : 0,
3186
+ changeSamples: finite2(source.changeSamples) ? source.changeSamples : 0,
3187
+ units: source.units,
3188
+ note: source.note
3189
+ },
3190
+ note,
3191
+ anyAlert: loadRead.alert || configRead.alert
3192
+ };
3193
+ }
3194
+
3047
3195
  // src/client/panel/panel-model.ts
3048
3196
  var STAGE_ORDER = ["idle", "monitoring", "crashed", "deciding", "stabilizing", "recovered"];
3049
3197
  function stageLabelOf(stage, fallback) {
@@ -3172,14 +3320,14 @@ function metricRows(snapshot) {
3172
3320
  const rows = [];
3173
3321
  const push = (key, label, metric, format, bar) => {
3174
3322
  const raw = metric.value;
3175
- const usable = metric.defined && raw !== null;
3323
+ const usable2 = metric.defined && raw !== null;
3176
3324
  rows.push({
3177
3325
  key,
3178
3326
  label,
3179
- value: usable ? format(raw) : t("metrics.undefined"),
3180
- defined: usable,
3181
- reason: usable ? null : metric.reason ?? (metric.defined ? t("metrics.valueUndefined") : null),
3182
- bar: usable ? bar : null
3327
+ value: usable2 ? format(raw) : t("metrics.undefined"),
3328
+ defined: usable2,
3329
+ reason: usable2 ? null : metric.reason ?? (metric.defined ? t("metrics.valueUndefined") : null),
3330
+ bar: usable2 ? bar : null
3183
3331
  });
3184
3332
  };
3185
3333
  push("crashRate", t("metrics.crashRate"), metrics.crashRate, (value) => pctText(value), metrics.crashRate.value ?? null);
@@ -3864,6 +4012,34 @@ function CooccurrenceBody({ snapshot }) {
3864
4012
  ] })
3865
4013
  ] });
3866
4014
  }
4015
+ function PreventiveBody({ snapshot }) {
4016
+ const summary = preventiveSummary(snapshot);
4017
+ if (!summary.available) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "rg-empty", children: t("preventive.unavailable") });
4018
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
4019
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "rg-notice rg-preventive-alertonly", children: summary.note }),
4020
+ summary.reads.map((read) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "rg-preventive-read", children: [
4021
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("h4", { className: "rg-card-subtitle", children: [
4022
+ read.title,
4023
+ read.defined && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `rg-badge ${read.alert ? "rg-badge-warn" : "rg-badge-neutral"}`, children: read.alert ? t("preventive.alertYes") : t("preventive.alertNo") })
4024
+ ] }),
4025
+ read.defined ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
4026
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "rg-card-note", title: read.label, children: read.label }),
4027
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dl", { className: "rg-dl rg-dl-compact", children: read.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "rg-dl-pair", children: [
4028
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: row.label }),
4029
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dd", { children: row.value })
4030
+ ] }, row.key)) })
4031
+ ] }) : (
4032
+ // 不可判定 ⇒ 只说原因;绝不渲染 0 值(也绝不显示「一切正常」)。
4033
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "rg-empty", children: read.reason ?? t("metrics.undefined") })
4034
+ )
4035
+ ] }, read.key)),
4036
+ summary.source !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "rg-card-note", title: summary.source.note, children: t("preventive.source", {
4037
+ boots: summary.source.bootSamples,
4038
+ changes: summary.source.changeSamples,
4039
+ units: summary.source.units
4040
+ }) })
4041
+ ] });
4042
+ }
3867
4043
  function PartitionsBody({
3868
4044
  snapshot,
3869
4045
  selectedKey,
@@ -4287,9 +4463,105 @@ var PanelErrorBoundary = class extends import_react5.Component {
4287
4463
  }
4288
4464
  };
4289
4465
 
4466
+ // src/client/panel/poll-adaptive.ts
4467
+ var POLL_BASE_MS = 5e3;
4468
+ var POLL_MIN_MS = 3e3;
4469
+ var POLL_MAX_MS = 3e4;
4470
+ var POLL_BACKOFF_MULTIPLIER = 3;
4471
+ function positive(value, fallback) {
4472
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
4473
+ }
4474
+ function nextPollDelay(input) {
4475
+ const base = positive(input.baseMs, POLL_BASE_MS);
4476
+ let min = positive(input.minMs, POLL_MIN_MS);
4477
+ let max = positive(input.maxMs, POLL_MAX_MS);
4478
+ if (min > max) {
4479
+ const swapped = min;
4480
+ min = max;
4481
+ max = swapped;
4482
+ }
4483
+ const clamp = (value) => Math.round(Math.min(max, Math.max(min, value)));
4484
+ if (input.changed === true) return clamp(base);
4485
+ const rounds = typeof input.sameRounds === "number" && Number.isFinite(input.sameRounds) && input.sameRounds > 0 ? Math.floor(input.sameRounds) : 0;
4486
+ const cap = Math.min(max, base * POLL_BACKOFF_MULTIPLIER);
4487
+ const raw = base * (1 + rounds);
4488
+ return clamp(Number.isFinite(raw) ? Math.min(cap, raw) : cap);
4489
+ }
4490
+ function stableStringify(value) {
4491
+ return canonicalize(value, /* @__PURE__ */ new Set());
4492
+ }
4493
+ function canonicalize(value, stack) {
4494
+ if (value === null) return "null";
4495
+ switch (typeof value) {
4496
+ case "number":
4497
+ return Number.isFinite(value) ? JSON.stringify(value) : "null";
4498
+ case "string":
4499
+ return JSON.stringify(value);
4500
+ case "boolean":
4501
+ return value ? "true" : "false";
4502
+ case "bigint":
4503
+ return JSON.stringify(`${value.toString()}n`);
4504
+ case "undefined":
4505
+ case "function":
4506
+ case "symbol":
4507
+ return "null";
4508
+ default:
4509
+ break;
4510
+ }
4511
+ if (Array.isArray(value)) {
4512
+ if (stack.has(value)) return JSON.stringify("[Circular]");
4513
+ stack.add(value);
4514
+ const out = `[${value.map((entry) => canonicalize(entry, stack)).join(",")}]`;
4515
+ stack.delete(value);
4516
+ return out;
4517
+ }
4518
+ const object = value;
4519
+ if (stack.has(object)) return JSON.stringify("[Circular]");
4520
+ stack.add(object);
4521
+ const parts = [];
4522
+ for (const key of Object.keys(object).sort()) {
4523
+ const entry = object[key];
4524
+ if (entry === void 0 || typeof entry === "function" || typeof entry === "symbol") continue;
4525
+ parts.push(`${JSON.stringify(key)}:${canonicalize(entry, stack)}`);
4526
+ }
4527
+ stack.delete(object);
4528
+ return `{${parts.join(",")}}`;
4529
+ }
4530
+ function fnv1a32(text) {
4531
+ let hash = 2166136261;
4532
+ for (let index = 0; index < text.length; index += 1) {
4533
+ hash ^= text.charCodeAt(index);
4534
+ hash = Math.imul(hash, 16777619);
4535
+ }
4536
+ return hash >>> 0;
4537
+ }
4538
+ var OMIT_TOP_LEVEL = /* @__PURE__ */ new Set(["at"]);
4539
+ var OMIT_DIAGNOSTIC_KEYS = /* @__PURE__ */ new Set(["snapshotMs"]);
4540
+ function stripVolatile(snapshot) {
4541
+ if (snapshot === null || typeof snapshot !== "object" || Array.isArray(snapshot)) return snapshot;
4542
+ const copy = { ...snapshot };
4543
+ for (const key of OMIT_TOP_LEVEL) delete copy[key];
4544
+ const diagnostics = copy.diagnostics;
4545
+ if (diagnostics !== null && typeof diagnostics === "object" && !Array.isArray(diagnostics)) {
4546
+ const cleaned = { ...diagnostics };
4547
+ for (const key of OMIT_DIAGNOSTIC_KEYS) delete cleaned[key];
4548
+ copy.diagnostics = cleaned;
4549
+ }
4550
+ return copy;
4551
+ }
4552
+ function snapshotFingerprint(snapshot) {
4553
+ return fnv1a32(stableStringify(stripVolatile(snapshot))).toString(16).padStart(8, "0");
4554
+ }
4555
+ function graphFingerprint(graph) {
4556
+ return fnv1a32(stableStringify(graph ?? null)).toString(16).padStart(8, "0");
4557
+ }
4558
+ function pollContentFingerprint(snapshot, graph) {
4559
+ return `${snapshotFingerprint(snapshot)}|${graphFingerprint(graph)}`;
4560
+ }
4561
+
4290
4562
  // src/client/panel/GuardPanel.tsx
4291
4563
  var import_jsx_runtime6 = require("react/jsx-runtime");
4292
- var POLL_INTERVAL = 5e3;
4564
+ var POLL_BASE = POLL_BASE_MS;
4293
4565
  var COLLAPSE_KEY = "recency-guard-collapsed";
4294
4566
  function loadCollapsed() {
4295
4567
  try {
@@ -4313,6 +4585,8 @@ function GuardPanel(props) {
4313
4585
  const [failure, setFailure] = (0, import_react6.useState)(null);
4314
4586
  const [busy, setBusy] = (0, import_react6.useState)(false);
4315
4587
  const [polling, setPolling] = (0, import_react6.useState)(true);
4588
+ const [pollDelay, setPollDelay] = (0, import_react6.useState)(POLL_BASE);
4589
+ const [stableRounds, setStableRounds] = (0, import_react6.useState)(0);
4316
4590
  const [collapsed, setCollapsed] = (0, import_react6.useState)(() => loadCollapsed());
4317
4591
  const [form, setForm] = (0, import_react6.useState)("wide");
4318
4592
  const [selectedNodeId, setSelectedNodeId] = (0, import_react6.useState)(null);
@@ -4323,6 +4597,8 @@ function GuardPanel(props) {
4323
4597
  const [conflictNotice, setConflictNotice] = (0, import_react6.useState)(false);
4324
4598
  const rootRef = (0, import_react6.useRef)(null);
4325
4599
  const mounted = (0, import_react6.useRef)(true);
4600
+ const fingerprintRef = (0, import_react6.useRef)(null);
4601
+ const sameRoundsRef = (0, import_react6.useRef)(0);
4326
4602
  const load = (0, import_react6.useCallback)(
4327
4603
  async (partitionKey) => {
4328
4604
  const [snapshotResult, graphResult, archivesResult] = await Promise.all([
@@ -4332,7 +4608,7 @@ function GuardPanel(props) {
4332
4608
  // 面板据此隐藏归档卡片 —— 不假装「归档为空」。
4333
4609
  props.fetchArchives ? props.fetchArchives(partitionKey ?? void 0) : Promise.resolve({ ok: false, reason: "archives-absent" })
4334
4610
  ]);
4335
- if (!mounted.current) return;
4611
+ if (!mounted.current) return { changed: true, sameRounds: 0 };
4336
4612
  if (snapshotResult.ok) {
4337
4613
  setSnapshot(snapshotResult.data);
4338
4614
  setFailure(null);
@@ -4351,21 +4627,50 @@ function GuardPanel(props) {
4351
4627
  archivesInjected: props.fetchArchives !== void 0
4352
4628
  })
4353
4629
  );
4630
+ if (!snapshotResult.ok) {
4631
+ sameRoundsRef.current = 0;
4632
+ return { changed: true, sameRounds: 0 };
4633
+ }
4634
+ const fingerprint = pollContentFingerprint(
4635
+ snapshotResult.data,
4636
+ graphResult.ok ? graphResult.data : null
4637
+ );
4638
+ const previous = fingerprintRef.current;
4639
+ const changed = previous === null || previous !== fingerprint;
4640
+ const sameRounds = changed ? 0 : Math.min(sameRoundsRef.current + 1, 999);
4641
+ fingerprintRef.current = fingerprint;
4642
+ sameRoundsRef.current = sameRounds;
4643
+ return { changed, sameRounds };
4354
4644
  },
4355
4645
  [props.fetchSnapshot, props.fetchGraph, props.fetchArchives]
4356
4646
  );
4357
4647
  (0, import_react6.useEffect)(() => {
4358
4648
  mounted.current = true;
4359
- void load(requestedKey);
4360
- if (!polling) {
4361
- return () => {
4362
- mounted.current = false;
4363
- };
4364
- }
4365
- const timer = setInterval(() => void load(requestedKey), POLL_INTERVAL);
4649
+ let cancelled = false;
4650
+ let timer = null;
4651
+ const afterLoad = (outcome) => {
4652
+ if (cancelled) return;
4653
+ const delay = nextPollDelay({
4654
+ sameRounds: outcome.sameRounds,
4655
+ changed: outcome.changed,
4656
+ baseMs: POLL_BASE
4657
+ });
4658
+ setPollDelay(delay);
4659
+ setStableRounds(outcome.sameRounds);
4660
+ if (!polling) return;
4661
+ timer = setTimeout(() => {
4662
+ void run();
4663
+ }, delay);
4664
+ };
4665
+ const run = async () => {
4666
+ const outcome = await load(requestedKey);
4667
+ afterLoad(outcome);
4668
+ };
4669
+ void run();
4366
4670
  return () => {
4671
+ cancelled = true;
4672
+ if (timer !== null) clearTimeout(timer);
4367
4673
  mounted.current = false;
4368
- clearInterval(timer);
4369
4674
  };
4370
4675
  }, [load, polling, requestedKey]);
4371
4676
  (0, import_react6.useEffect)(() => {
@@ -4455,6 +4760,13 @@ function GuardPanel(props) {
4455
4760
  currentPartition && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "rg-subtitle rg-partition-workspace-full", title: currentPartition.workspace, children: shortenPath(currentPartition.workspace, 72) })
4456
4761
  ] }),
4457
4762
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "rg-actions", children: [
4763
+ polling && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "rg-poll-chip", role: "status", title: t("panel.pollingHint"), children: [
4764
+ t("panel.pollingOn", { seconds: Math.max(1, Math.round(pollDelay / 1e3)) }),
4765
+ stableRounds > 0 && pollDelay > POLL_BASE && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4766
+ " \xB7 ",
4767
+ t("panel.pollingBackoff")
4768
+ ] })
4769
+ ] }),
4458
4770
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4459
4771
  "button",
4460
4772
  {
@@ -4623,6 +4935,17 @@ function GuardPanel(props) {
4623
4935
  }
4624
4936
  ),
4625
4937
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "rg-group-title", children: t("groups.observation") }),
4938
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4939
+ Card,
4940
+ {
4941
+ id: "preventive",
4942
+ title: t("sections.preventive"),
4943
+ hint: t("preventive.hint"),
4944
+ collapsed: collapsed.has("preventive"),
4945
+ onToggle: () => toggleCollapse("preventive"),
4946
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PreventiveBody, { snapshot })
4947
+ }
4948
+ ),
4626
4949
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4627
4950
  Card,
4628
4951
  {
@@ -4734,7 +5057,13 @@ function GuardPanel(props) {
4734
5057
  failure !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "rg-error", role: "alert", children: failure }),
4735
5058
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("footer", { className: "rg-foot", children: [
4736
5059
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: t("panel.snapshotAt", { time: snapshot.at.replace("T", " ").slice(0, 19) }) }),
4737
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: polling ? t("panel.pollingOn", { seconds: POLL_INTERVAL / 1e3 }) : t("panel.stale", { minutes: staleMinutes }) })
5060
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
5061
+ polling ? t("panel.pollingOn", { seconds: Math.max(1, Math.round(pollDelay / 1e3)) }) : t("panel.stale", { minutes: staleMinutes }),
5062
+ polling && stableRounds > 0 && pollDelay > POLL_BASE && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5063
+ " \xB7 ",
5064
+ t("panel.pollingBackoff")
5065
+ ] })
5066
+ ] })
4738
5067
  ] })
4739
5068
  ] }) })
4740
5069
  ] });
@@ -5251,12 +5580,26 @@ var GRAPH_CSS = `
5251
5580
  flex: 0 0 auto;
5252
5581
  }
5253
5582
 
5254
- /* \u6570\u636E\u6D41\u5361\uFF1A4 \u4E2A\u6B65\u9AA4\u5FC5\u987B\u5B8C\u6574\u53EF\u8BFB\uFF08\u771F\u673A\u7B2C 4 \u6B65\u88AB\u88C1\u6389\u4E00\u534A\uFF09\u3002
5255
- \u5361\u7247 body \u7684\u9ED8\u8BA4\u9650\u9AD8\u662F\u7ED9\u957F\u6E05\u5355\u7528\u7684\uFF1B\u6570\u636E\u6D41\u662F\u56FA\u5B9A 4 \u884C\uFF0C\u6309\u5185\u5BB9\u4F38\u5C55
5256
- \uFF08\u4ECD\u7559 max-height \u515C\u5E95\uFF0C\u8D85\u51FA\u65F6\u5185\u90E8\u6EDA\u52A8\u662F\u663E\u5F0F\u7684\uFF09\u3002 */
5257
- .rg-card-body:has(.rg-dataflow) {
5583
+ /* \u8F6E\u8BE2\u95F4\u9694\u5FBD\u6807\uFF08\u64CD\u4F5C\u533A\uFF09\uFF1A\u6BD4\u6309\u94AE\u5F31\u3001\u4F46\u4E0D\u7528\u6EDA\u5230\u9875\u811A\u5C31\u80FD\u8BFB\u5230\u3002 */
5584
+ .rg-poll-chip {
5585
+ align-self: center;
5586
+ padding: 2px 8px;
5587
+ border: 1px solid var(--dsw-alias-border-l1);
5588
+ border-radius: var(--dsw-radius-full, 999px);
5589
+ font-size: var(--dsw-font-xxxs-11, 11px);
5590
+ color: var(--dsw-alias-label-secondary);
5591
+ white-space: nowrap;
5592
+ }
5593
+
5594
+ /* \u6570\u636E\u6D41 / \u9884\u8B66\u5361\uFF1A\u5185\u5BB9\u5FC5\u987B\u5B8C\u6574\u53EF\u8BFB\uFF08v0.16 \u6570\u636E\u6D41\u7B2C 4 \u6B65\u3001v0.17 \u9884\u8B66\u5361
5595
+ CUSUM \u8BF4\u660E\u884C\u4E24\u6B21\u88AB\u56FA\u5B9A 240px body \u88C1\u6389\uFF09\u3002\u8FD9\u4E24\u5F20\u5361\u5185\u5BB9\u957F\u5EA6\u6709\u9650\u4E14\u7528\u6237
5596
+ \u5FC5\u987B\u8BFB\u5168\uFF0C\u56E0\u6B64\u6309\u5185\u5BB9\u4F38\u5C55\uFF1B\u4ECD\u4FDD\u7559 max-height + **\u663E\u5F0F** overflow-y:auto
5597
+ \uFF08\u8D85\u51FA\u65F6\u6EDA\u52A8\u6761\u53EF\u89C1\uFF0C\u4E0D\u662F\u9759\u9ED8\u88C1\u5207\uFF09\u3002 */
5598
+ .rg-card-body:has(.rg-dataflow),
5599
+ .rg-card[data-card-id='preventive'] .rg-card-body {
5258
5600
  height: auto;
5259
- max-height: 320px;
5601
+ max-height: 360px;
5602
+ overflow-y: auto;
5260
5603
  }
5261
5604
 
5262
5605
  /* \u51CF\u5C11\u52A8\u6001\u6548\u679C\uFF1A\u8282\u70B9\u62D6\u52A8\u3001\u5E03\u5C40\u8FC7\u6E21\u4E0E\u56FE\u4F8B\u90FD\u4E0D\u5F97\u64AD\u653E\u52A8\u753B\u3002 */