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/README.md +23 -9
- package/bin/rescue.mjs +59 -6
- package/dist/client.js +364 -21
- package/dist/index.js +501 -131
- package/dsh-plugin.json +1 -1
- package/locale/en.json +29 -0
- package/locale/zh.json +29 -0
- package/package.json +1 -1
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
|
|
3323
|
+
const usable2 = metric.defined && raw !== null;
|
|
3176
3324
|
rows.push({
|
|
3177
3325
|
key,
|
|
3178
3326
|
label,
|
|
3179
|
-
value:
|
|
3180
|
-
defined:
|
|
3181
|
-
reason:
|
|
3182
|
-
bar:
|
|
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
|
|
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
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
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.
|
|
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
|
-
/* \
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
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:
|
|
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 */
|