feat: show provider gateway versions in webui
This commit is contained in:
@@ -296,6 +296,43 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.node-version-line, .gateway-cell {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 7px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.gateway-cell {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.version-chip, .mode-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid rgba(78, 183, 168, 0.48);
|
||||
color: #c9f3ec;
|
||||
background: rgba(78, 183, 168, 0.1);
|
||||
font-family: "Cascadia Mono", "IBM Plex Mono", "Liberation Mono", monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.version-chip.unknown {
|
||||
color: var(--warn);
|
||||
border-color: rgba(215, 161, 58, 0.45);
|
||||
background: rgba(215, 161, 58, 0.08);
|
||||
}
|
||||
.mode-chip.schedule {
|
||||
color: #ffd7cf;
|
||||
border-color: rgba(207, 106, 84, 0.5);
|
||||
background: rgba(207, 106, 84, 0.1);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -652,6 +689,80 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
color: #bcd2d7;
|
||||
}
|
||||
|
||||
.gateway-page {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.gateway-version-table { min-width: 1120px; }
|
||||
.gateway-version-table-wrap { max-height: 340px; }
|
||||
.capability-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
min-width: 180px;
|
||||
}
|
||||
.latest-upgrade-cell {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 220px;
|
||||
}
|
||||
.latest-upgrade-cell small, .gateway-record-meta, .upgrade-outcome {
|
||||
color: var(--muted);
|
||||
}
|
||||
.upgrade-outcome {
|
||||
display: block;
|
||||
min-width: 210px;
|
||||
max-width: 420px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.upgrade-outcome.failed { color: #ffd7cf; }
|
||||
.upgrade-outcome.succeeded { color: #d8f6dd; }
|
||||
.gateway-record-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(520px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.gateway-record-card {
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line-soft);
|
||||
background: var(--panel-3);
|
||||
}
|
||||
.gateway-record-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.gateway-record-head code {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: #bcd2d7;
|
||||
}
|
||||
.gateway-record-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.gateway-record-meta span {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--line-soft);
|
||||
background: rgba(255,255,255,0.025);
|
||||
}
|
||||
.upgrade-record-table { min-width: 1080px; }
|
||||
.upgrade-record-table-wrap {
|
||||
max-height: 300px;
|
||||
border: 1px solid var(--line-soft);
|
||||
}
|
||||
.upgrade-record-table-wrap.compact {
|
||||
max-height: 230px;
|
||||
}
|
||||
.provider-upgrade-records-panel .panel-body {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.chip-row, .summary-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -887,6 +998,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
.dispatch-form { grid-template-columns: 1fr 1fr; }
|
||||
.dispatch-actions { align-items: center; }
|
||||
.page-grid, .docker-layout, .monitor-layout { grid-template-columns: 1fr; }
|
||||
.gateway-record-grid { grid-template-columns: 1fr; }
|
||||
.overview-grid .panel:nth-child(n+3), .dispatch-grid .panel:first-child, .topology-grid .panel:nth-child(3) { grid-column: 1; }
|
||||
}
|
||||
|
||||
@@ -982,7 +1094,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
padding: 4px 9px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.metric-grid, .policy-grid, .security-board, .dispatch-form, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid { grid-template-columns: 1fr; }
|
||||
.metric-grid, .policy-grid, .security-board, .dispatch-form, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .gateway-record-grid { grid-template-columns: 1fr; }
|
||||
.compact-row, .heartbeat-row, .log-row, .endpoint-list article, .volume-route { grid-template-columns: 1fr; align-items: start; }
|
||||
.docker-hero, .monitor-hero { flex-direction: column; }
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ const MODULES = [
|
||||
{ id: "list", label: "节点清单" },
|
||||
{ id: "monitor", label: "资源监控" },
|
||||
{ id: "docker", label: "Docker 状态" },
|
||||
{ id: "gateway", label: "网关版本" },
|
||||
{ id: "labels", label: "资源标签" },
|
||||
{ id: "heartbeats", label: "心跳状态" },
|
||||
] },
|
||||
@@ -155,6 +156,82 @@ function safeId(value: any): string {
|
||||
return String(value).replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
function recordValue(data: any, key: string): any {
|
||||
return data && typeof data === "object" && !Array.isArray(data) ? data[key] : undefined;
|
||||
}
|
||||
|
||||
function labelString(node: any, key: string, fallback = "未知"): string {
|
||||
const value = recordValue(node?.labels, key);
|
||||
return typeof value === "string" && value.length > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
function nodeGatewayVersion(node: any): string {
|
||||
return labelString(node, "providerGatewayVersion");
|
||||
}
|
||||
|
||||
function nodeGatewayPolicy(node: any): string {
|
||||
return labelString(node, "providerGatewayUpgradePolicy");
|
||||
}
|
||||
|
||||
function nodeGatewayStartedAt(node: any): string {
|
||||
return labelString(node, "providerGatewayStartedAt", "");
|
||||
}
|
||||
|
||||
function nodeCapabilities(node: any): string[] {
|
||||
const value = recordValue(node?.labels, "unideskCapabilities");
|
||||
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
||||
}
|
||||
|
||||
function fmtGatewayVersion(value: any): string {
|
||||
const text = typeof value === "string" && value.length > 0 ? value : "未知";
|
||||
if (text === "未知") return "版本未知";
|
||||
return text.startsWith("v") ? text : `v${text}`;
|
||||
}
|
||||
|
||||
function taskPayload(task: any): AnyRecord {
|
||||
return task?.payload && typeof task.payload === "object" && !Array.isArray(task.payload) ? task.payload as AnyRecord : {};
|
||||
}
|
||||
|
||||
function taskResult(task: any): AnyRecord {
|
||||
return task?.result && typeof task.result === "object" && !Array.isArray(task.result) ? task.result as AnyRecord : {};
|
||||
}
|
||||
|
||||
function taskUpgradeMode(task: any): string {
|
||||
const payload = taskPayload(task);
|
||||
const result = taskResult(task);
|
||||
const mode = payload.mode ?? result.mode;
|
||||
return mode === "schedule" ? "schedule" : "plan";
|
||||
}
|
||||
|
||||
function taskUpgradeSource(task: any): string {
|
||||
const source = taskPayload(task).source;
|
||||
return typeof source === "string" && source.length > 0 ? source : "unknown";
|
||||
}
|
||||
|
||||
function taskUpgradePolicy(task: any): string {
|
||||
const result = taskResult(task);
|
||||
const plan = result.plan && typeof result.plan === "object" && !Array.isArray(result.plan) ? result.plan as AnyRecord : {};
|
||||
const policy = plan.policy;
|
||||
return typeof policy === "string" && policy.length > 0 ? policy : "--";
|
||||
}
|
||||
|
||||
function taskUpgradeOutcome(task: any): string {
|
||||
const status = String(task?.status || "").toLowerCase();
|
||||
if (status === "failed") return taskFailureReason(task);
|
||||
if (isPendingTask(task)) return "等待 provider 回传升级终态";
|
||||
const result = taskResult(task);
|
||||
if (typeof result.updaterContainerId === "string" && result.updaterContainerId.length > 0) return `updater ${result.updaterContainerId.slice(0, 18)}`;
|
||||
if (typeof result.message === "string" && result.message.length > 0) return result.message;
|
||||
if (result.plan) return "升级计划已生成";
|
||||
return "无升级结果摘要";
|
||||
}
|
||||
|
||||
function providerUpgradeTasks(tasks: any[], providerId: string): any[] {
|
||||
return tasks
|
||||
.filter((task) => task?.providerId === providerId && task?.command === "provider.upgrade")
|
||||
.sort((left, right) => (timeMs(right.updatedAt) ?? 0) - (timeMs(left.updatedAt) ?? 0));
|
||||
}
|
||||
|
||||
async function requestJson(path: string, options: AnyRecord = {}): Promise<any> {
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (options.body && !headers.has("content-type")) headers.set("content-type", "application/json");
|
||||
@@ -247,6 +324,14 @@ function LabelChips({ labels, limit = 8 }: AnyRecord) {
|
||||
);
|
||||
}
|
||||
|
||||
function GatewayVersionBadge({ node }: AnyRecord) {
|
||||
const version = nodeGatewayVersion(node);
|
||||
return h("span", {
|
||||
className: `version-chip ${version === "未知" ? "unknown" : ""}`,
|
||||
"data-testid": `gateway-version-${safeId(node?.providerId || "unknown")}`,
|
||||
}, fmtGatewayVersion(version));
|
||||
}
|
||||
|
||||
function DataSummary({ data, empty = "无数据" }: AnyRecord) {
|
||||
if (data === null || data === undefined) return h("span", { className: "muted" }, empty);
|
||||
if (typeof data !== "object") return h("span", { className: "summary-value" }, summarizeValue(data));
|
||||
@@ -374,6 +459,10 @@ function NodeCard({ node, onRaw }: AnyRecord) {
|
||||
h("div", null, h("strong", null, node.name), h("code", null, node.providerId)),
|
||||
h(StatusBadge, { status: node.status }),
|
||||
),
|
||||
h("div", { className: "node-version-line" },
|
||||
h(GatewayVersionBadge, { node }),
|
||||
h("span", null, `升级策略 ${nodeGatewayPolicy(node)}`),
|
||||
),
|
||||
h(LabelChips, { labels: node.labels, limit: 6 }),
|
||||
h("div", { className: "node-card-foot" },
|
||||
h("span", null, `心跳 ${fmtDate(node.lastHeartbeat)}`),
|
||||
@@ -416,10 +505,11 @@ function NodeListPage({ nodes, onRaw }: AnyRecord) {
|
||||
return h(Panel, { title: "节点清单", eyebrow: `${nodes.length} Providers` },
|
||||
nodes.length === 0 ? h(EmptyState, { title: "暂无 Provider 节点", text: "确认 provider-gateway 已连接 provider ingress" }) :
|
||||
h("div", { className: "table-wrap" }, h("table", null,
|
||||
h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "Provider"), h("th", null, "资源标签"), h("th", null, "连接时间"), h("th", null, "最后心跳"), h("th", null, "操作"))),
|
||||
h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "Provider"), h("th", null, "网关版本"), h("th", null, "资源标签"), h("th", null, "连接时间"), h("th", null, "最后心跳"), h("th", null, "操作"))),
|
||||
h("tbody", null, nodes.map((node: any) => h("tr", { key: node.providerId },
|
||||
h("td", null, h(StatusBadge, { status: node.status })),
|
||||
h("td", null, h("strong", null, node.name), h("code", null, node.providerId)),
|
||||
h("td", null, h("div", { className: "gateway-cell" }, h(GatewayVersionBadge, { node }), h("span", null, nodeGatewayPolicy(node)))),
|
||||
h("td", null, h(LabelChips, { labels: node.labels, limit: 5 })),
|
||||
h("td", null, fmtDate(node.connectedAt)),
|
||||
h("td", null, fmtDate(node.lastHeartbeat)),
|
||||
@@ -459,7 +549,7 @@ function HeartbeatPage({ nodes }: AnyRecord) {
|
||||
);
|
||||
}
|
||||
|
||||
function NodeMonitorPage({ nodes, systemStatuses, onRaw, refresh }: AnyRecord) {
|
||||
function NodeMonitorPage({ nodes, systemStatuses, tasks, onRaw, refresh }: AnyRecord) {
|
||||
const [selectedProvider, setSelectedProvider] = useState("");
|
||||
const merged = useMemo(() => nodes.map((node: any) => {
|
||||
const status = systemStatuses.find((item: any) => item.providerId === node.providerId);
|
||||
@@ -535,6 +625,7 @@ function NodeMonitorPage({ nodes, systemStatuses, onRaw, refresh }: AnyRecord) {
|
||||
),
|
||||
h("div", { className: "monitor-side-stack" },
|
||||
h(UpgradeControl, { provider: active, refresh, onRaw }),
|
||||
h(ProviderUpgradeRecordsPanel, { provider: active, tasks, onRaw, limit: 5 }),
|
||||
h(Panel, { title: "采样说明", eyebrow: "Retention" },
|
||||
h("div", { className: "monitor-note-list" },
|
||||
h("article", null, h("b", null, "CPU"), h("span", null, "从 /proc/stat 计算相邻采样差值,首个采样用 load/cores 近似")),
|
||||
@@ -611,6 +702,112 @@ function UpgradeControl({ provider, refresh, onRaw }: AnyRecord) {
|
||||
);
|
||||
}
|
||||
|
||||
function UpgradeRecordsTable({ records, onRaw, compact = false }: AnyRecord) {
|
||||
if (records.length === 0) {
|
||||
return h(EmptyState, { title: "暂无自动更新记录", text: "该节点还没有 provider.upgrade 任务;执行预检或升级后会在这里形成结构化记录" });
|
||||
}
|
||||
return h("div", { className: `upgrade-record-table-wrap table-wrap ${compact ? "compact" : ""}` }, h("table", { className: "upgrade-record-table" },
|
||||
h("thead", null, h("tr", null,
|
||||
h("th", null, "状态"),
|
||||
h("th", null, "模式"),
|
||||
h("th", null, "任务"),
|
||||
h("th", null, "来源"),
|
||||
h("th", null, "耗时"),
|
||||
h("th", null, "策略"),
|
||||
h("th", null, "结果记录"),
|
||||
h("th", null, "更新时间"),
|
||||
h("th", null, "操作"),
|
||||
)),
|
||||
h("tbody", null, records.map((task: any) => h("tr", { key: task.id, "data-testid": `gateway-upgrade-record-${safeId(task.id)}` },
|
||||
h("td", null, h(StatusBadge, { status: task.status })),
|
||||
h("td", null, h("span", { className: `mode-chip ${taskUpgradeMode(task)}` }, taskUpgradeMode(task) === "schedule" ? "执行升级" : "预检")),
|
||||
h("td", null, h("strong", null, "provider.upgrade"), h("code", null, task.id)),
|
||||
h("td", null, taskUpgradeSource(task)),
|
||||
h("td", null, h(TaskDurationCell, { task })),
|
||||
h("td", null, taskUpgradePolicy(task)),
|
||||
h("td", null, h("span", { className: `upgrade-outcome ${String(task.status || "").toLowerCase()}` }, taskUpgradeOutcome(task))),
|
||||
h("td", null, fmtDate(task.updatedAt)),
|
||||
h("td", null, h(RawButton, { title: `Provider Upgrade Task ${task.id}`, data: task, onOpen: onRaw })),
|
||||
))),
|
||||
));
|
||||
}
|
||||
|
||||
function ProviderUpgradeRecordsPanel({ provider, tasks, onRaw, limit = 5 }: AnyRecord) {
|
||||
const records = providerUpgradeTasks(tasks, provider.providerId).slice(0, limit);
|
||||
return h(Panel, {
|
||||
title: "自动更新记录",
|
||||
eyebrow: provider.providerId,
|
||||
actions: h(GatewayVersionBadge, { node: provider }),
|
||||
className: "provider-upgrade-records-panel",
|
||||
},
|
||||
h("div", { "data-testid": `provider-upgrade-records-${safeId(provider.providerId)}` },
|
||||
h(UpgradeRecordsTable, { records, onRaw, compact: true }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function GatewayVersionPage({ nodes, tasks, onRaw }: AnyRecord) {
|
||||
const rows = useMemo(() => nodes.map((node: any) => {
|
||||
const records = providerUpgradeTasks(tasks, node.providerId);
|
||||
return { node, records, latest: records[0] || null, capabilities: nodeCapabilities(node) };
|
||||
}), [nodes, tasks]);
|
||||
const totalRecords = rows.reduce((sum: number, row: any) => sum + row.records.length, 0);
|
||||
|
||||
return h("div", { className: "gateway-page", "data-testid": "gateway-version-page" },
|
||||
h(Panel, { title: "Provider Gateway 版本", eyebrow: `${nodes.length} Providers / ${totalRecords} 更新记录` },
|
||||
nodes.length === 0 ? h(EmptyState, { title: "暂无 Provider 节点", text: "等待 provider-gateway 注册后显示版本号和升级记录" }) :
|
||||
h("div", { className: "table-wrap gateway-version-table-wrap" }, h("table", { className: "gateway-version-table" },
|
||||
h("thead", null, h("tr", null,
|
||||
h("th", null, "状态"),
|
||||
h("th", null, "Provider"),
|
||||
h("th", null, "Gateway 版本"),
|
||||
h("th", null, "升级策略"),
|
||||
h("th", null, "运行时间"),
|
||||
h("th", null, "能力"),
|
||||
h("th", null, "最近自动更新"),
|
||||
h("th", null, "操作"),
|
||||
)),
|
||||
h("tbody", null, rows.map((row: any) => h("tr", { key: row.node.providerId },
|
||||
h("td", null, h(StatusBadge, { status: row.node.status })),
|
||||
h("td", null, h("strong", null, row.node.name), h("code", null, row.node.providerId)),
|
||||
h("td", null, h(GatewayVersionBadge, { node: row.node })),
|
||||
h("td", null, nodeGatewayPolicy(row.node)),
|
||||
h("td", null, nodeGatewayStartedAt(row.node) ? fmtDate(nodeGatewayStartedAt(row.node)) : "待新版上报"),
|
||||
h("td", null, h("div", { className: "capability-row" },
|
||||
row.capabilities.length === 0 ? h("span", { className: "muted" }, "未声明") :
|
||||
row.capabilities.slice(0, 5).map((item: string) => h("span", { key: item, className: "data-chip" }, item)),
|
||||
)),
|
||||
h("td", null, row.latest ? h("div", { className: "latest-upgrade-cell" },
|
||||
h(StatusBadge, { status: row.latest.status }),
|
||||
h("span", null, `${taskUpgradeMode(row.latest) === "schedule" ? "执行升级" : "预检"} / ${fmtDate(row.latest.updatedAt)}`),
|
||||
h("small", null, taskUpgradeOutcome(row.latest)),
|
||||
) : h("span", { className: "muted" }, "暂无记录")),
|
||||
h("td", null, h(RawButton, { title: `Provider ${row.node.providerId}`, data: row.node, onOpen: onRaw })),
|
||||
))),
|
||||
)),
|
||||
),
|
||||
h(Panel, { title: "自动更新记录", eyebrow: "Structured provider.upgrade records" },
|
||||
nodes.length === 0 ? h(EmptyState, { title: "暂无记录", text: "没有 provider 节点时不会生成自动更新记录" }) :
|
||||
h("div", { className: "gateway-record-grid" }, rows.map((row: any) => h("article", {
|
||||
key: row.node.providerId,
|
||||
className: "gateway-record-card",
|
||||
"data-testid": `gateway-records-${safeId(row.node.providerId)}`,
|
||||
},
|
||||
h("div", { className: "gateway-record-head" },
|
||||
h("div", null, h("strong", null, row.node.name), h("code", null, row.node.providerId)),
|
||||
h(GatewayVersionBadge, { node: row.node }),
|
||||
),
|
||||
h("div", { className: "gateway-record-meta" },
|
||||
h("span", null, `心跳 ${fmtDate(row.node.lastHeartbeat)}`),
|
||||
h("span", null, `策略 ${nodeGatewayPolicy(row.node)}`),
|
||||
h("span", null, `${row.records.length} 条记录`),
|
||||
),
|
||||
h(UpgradeRecordsTable, { records: row.records.slice(0, 8), onRaw, compact: true }),
|
||||
))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function dockerStateTone(state: string): string {
|
||||
if (state === "running") return "online";
|
||||
if (state === "paused" || state === "restarting") return "warn";
|
||||
@@ -1017,8 +1214,9 @@ function WorkArea({ activeModule, activeTab, data, session, refresh, onRaw, onNa
|
||||
if (activeModule === "ops" && activeTab === "events") return h(EventsPage, { events: data.events, onRaw });
|
||||
if (activeModule === "ops" && activeTab === "logs") return h(LogsPage, { logs: data.logs, onRaw });
|
||||
if (activeModule === "nodes" && activeTab === "list") return h(NodeListPage, { nodes: data.nodes, onRaw });
|
||||
if (activeModule === "nodes" && activeTab === "monitor") return h(NodeMonitorPage, { nodes: data.nodes, systemStatuses: data.systemStatuses, onRaw, refresh });
|
||||
if (activeModule === "nodes" && activeTab === "monitor") return h(NodeMonitorPage, { nodes: data.nodes, systemStatuses: data.systemStatuses, tasks: data.tasks, onRaw, refresh });
|
||||
if (activeModule === "nodes" && activeTab === "docker") return h(DockerStatusPage, { nodes: data.nodes, dockerStatuses: data.dockerStatuses, onRaw });
|
||||
if (activeModule === "nodes" && activeTab === "gateway") return h(GatewayVersionPage, { nodes: data.nodes, tasks: data.tasks, onRaw });
|
||||
if (activeModule === "nodes" && activeTab === "labels") return h(LabelsPage, { nodes: data.nodes });
|
||||
if (activeModule === "nodes" && activeTab === "heartbeats") return h(HeartbeatPage, { nodes: data.nodes });
|
||||
if (activeModule === "tasks" && activeTab === "dispatch") return h(DispatchPage, { nodes: data.nodes, onDispatched: refresh, onRaw });
|
||||
@@ -1051,7 +1249,7 @@ function Shell({ session, onLogout }: AnyRecord) {
|
||||
requestJson(`${cfg.apiBaseUrl}/nodes/system-status?limit=120`),
|
||||
requestJson(`${cfg.apiBaseUrl}/nodes/docker-status`),
|
||||
requestJson(`${cfg.apiBaseUrl}/events?limit=100`),
|
||||
requestJson(`${cfg.apiBaseUrl}/tasks?limit=100`),
|
||||
requestJson(`${cfg.apiBaseUrl}/tasks?limit=300`),
|
||||
requestJson(`${cfg.apiBaseUrl}/tasks?status=pending&limit=100`),
|
||||
requestJson("/logs?limit=100"),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user