feat(web-sentinel): add dashboard manual trigger

This commit is contained in:
Codex
2026-07-01 11:42:24 +00:00
parent a071d1e33e
commit 4830410e68
9 changed files with 464 additions and 7 deletions
@@ -152,6 +152,44 @@ select {
padding: 0 12px;
}
.toolbar-button:disabled {
cursor: progress;
opacity: 0.62;
}
.trigger-button {
border-color: #9fbbb4;
background: #eff7f4;
font-weight: 700;
}
.trigger-status {
display: inline-flex;
align-items: center;
max-width: 220px;
min-height: 30px;
padding: 0 9px;
border: 1px solid var(--line);
border-radius: 8px;
background: #ffffff;
color: var(--muted);
font-size: 12px;
overflow-wrap: anywhere;
}
.trigger-status[data-trigger-state="triggered"],
.trigger-status[data-trigger-state="already-active"] {
border-color: #b9ddc9;
background: var(--green-soft);
color: #17633f;
}
.trigger-status[data-trigger-state="failed"] {
border-color: #efb3ad;
background: var(--red-soft);
color: #8b1f1a;
}
select {
padding: 0 28px 0 10px;
}
@@ -20,6 +20,9 @@ createApp({
const initialDeepLink = readMonitorDeepLink();
const loading = ref(true);
const error = ref("");
const manualTriggering = ref(false);
const manualTriggerResult = ref(null);
const manualTriggerError = ref("");
const overview = ref(null);
const runs = ref([]);
const findings = ref([]);
@@ -189,6 +192,20 @@ createApp({
text: `${key} ${value?.ok === false ? "异常" : "ok"}`,
}));
});
const manualTriggerState = computed(() => {
if (manualTriggering.value) return "triggering";
if (manualTriggerError.value) return "failed";
return String(manualTriggerResult.value?.status || "idle");
});
const manualTriggerJobName = computed(() => String(manualTriggerResult.value?.jobName || ""));
const manualTriggerStatusText = computed(() => {
if (manualTriggering.value) return "触发中";
if (manualTriggerError.value) return manualTriggerError.value;
const result = manualTriggerResult.value || {};
if (result.status === "triggered") return `已触发 ${shortId(result.jobName)}`;
if (result.status === "already-active") return `运行中 ${shortId(result.jobName)}`;
return "";
});
async function loadAll(options = {}) {
if (!options.silent) loading.value = true;
@@ -254,6 +271,27 @@ createApp({
void loadAll();
}
async function triggerQuickVerify() {
if (manualTriggering.value) return;
manualTriggering.value = true;
manualTriggerError.value = "";
manualTriggerResult.value = null;
try {
const payload = await postJson("/api/quick-verify/trigger", {
source: "monitor-web",
reason: "manual-web-ui",
valuesRedacted: true,
});
manualTriggerResult.value = payload;
autoRefresh.value = true;
await loadAll({ silent: true });
} catch (cause) {
manualTriggerError.value = String(cause?.message || cause).slice(0, 160);
} finally {
manualTriggering.value = false;
}
}
function selectCheckRun() {
const run = runs.value.find((item) => item.id === checkRunId.value);
if (run) void selectRun(run);
@@ -429,6 +467,9 @@ createApp({
bootstrap,
loading,
error,
manualTriggering,
manualTriggerResult,
manualTriggerError,
overview,
runs,
findings,
@@ -480,12 +521,16 @@ createApp({
checkScopeText,
cadence,
healthChecks,
manualTriggerState,
manualTriggerJobName,
manualTriggerStatusText,
loadAll,
selectRun,
selectCheckRun,
openCheckDetail,
closeCheckDetail,
refreshNow,
triggerQuickVerify,
currentHref,
showTrendTooltip,
hideTrendTooltip,
@@ -532,7 +577,13 @@ createApp({
};
},
template: `
<div class="monitor-shell" data-monitor-shell="true" :data-monitor-status="currentStatus">
<div
class="monitor-shell"
data-monitor-shell="true"
:data-monitor-status="currentStatus"
:data-manual-trigger-state="manualTriggerState"
:data-manual-trigger-job="manualTriggerJobName"
>
<header class="topbar">
<div class="title-block">
<div class="mark" aria-hidden="true"></div>
@@ -554,6 +605,26 @@ createApp({
</select>
<button class="toolbar-button" type="button" @click="autoRefresh = !autoRefresh">{{ autoRefresh ? "自动" : "手动" }}</button>
<button class="toolbar-button" type="button" @click="refreshNow">刷新</button>
<button
class="toolbar-button trigger-button"
data-monitor-manual-trigger="true"
type="button"
:disabled="manualTriggering"
:data-trigger-state="manualTriggerState"
:data-trigger-job="manualTriggerJobName"
@click="triggerQuickVerify"
>
{{ manualTriggering ? "触发中" : "触发巡检" }}
</button>
<span
v-if="manualTriggerStatusText"
class="trigger-status"
data-monitor-manual-trigger-status="true"
:data-trigger-state="manualTriggerState"
:data-trigger-job="manualTriggerJobName"
>
{{ manualTriggerStatusText }}
</span>
</div>
</header>
@@ -1049,6 +1120,21 @@ async function fetchJson(path) {
return await response.json();
}
async function postJson(path, body) {
const response = await fetch(apiUrl(path), {
method: "POST",
cache: "no-store",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify(body || {}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload.ok === false) {
const reason = payload.reason || payload.error || `${path} HTTP ${response.status}`;
throw new Error(String(reason));
}
return payload;
}
function apiUrl(path) {
const prefix = bootstrap.basePath || "";
const suffix = path.startsWith("/") ? path : `/${path}`;