the-android-mcp 3.19.0 → 3.20.1
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/web-ui.d.ts.map +1 -1
- package/dist/web-ui.js +419 -0
- package/dist/web-ui.js.map +1 -1
- package/package.json +1 -1
package/dist/web-ui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-ui.d.ts","sourceRoot":"","sources":["../src/web-ui.ts"],"names":[],"mappings":";AAKA,OAAO,IAAyC,MAAM,MAAM,CAAC;AAgB7D,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAC/C,eAAO,MAAM,mBAAmB,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"web-ui.d.ts","sourceRoot":"","sources":["../src/web-ui.ts"],"names":[],"mappings":";AAKA,OAAO,IAAyC,MAAM,MAAM,CAAC;AAgB7D,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAC/C,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AA65XzC,wBAAgB,gBAAgB,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CAAC,MAAM,CA8B5F;AAED,wBAAsB,qBAAqB,CAAC,OAAO,GAAE;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACV,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBhF"}
|
package/dist/web-ui.js
CHANGED
|
@@ -38,6 +38,7 @@ const WATCHDOG_PROFILES_FILE = path_1.default.join(APP_STATE_DIR, 'web-ui-watchd
|
|
|
38
38
|
const OPS_MISSIONS_FILE = path_1.default.join(APP_STATE_DIR, 'web-ui-ops-missions.json');
|
|
39
39
|
const OPS_MISSION_SCHEDULES_FILE = path_1.default.join(APP_STATE_DIR, 'web-ui-ops-mission-schedules.json');
|
|
40
40
|
const OPS_MISSION_POLICY_FILE = path_1.default.join(APP_STATE_DIR, 'web-ui-ops-mission-policy.json');
|
|
41
|
+
const OPS_MISSION_RUNS_FILE = path_1.default.join(APP_STATE_DIR, 'web-ui-ops-mission-runs.json');
|
|
41
42
|
const SNAPSHOT_KINDS = [
|
|
42
43
|
'radio',
|
|
43
44
|
'display',
|
|
@@ -93,6 +94,7 @@ let opsGatePolicy = {
|
|
|
93
94
|
};
|
|
94
95
|
const opsMissionRuns = [];
|
|
95
96
|
let opsMissionRunSeq = 1;
|
|
97
|
+
let opsMissionRunsLoaded = false;
|
|
96
98
|
const opsMissionSchedules = {};
|
|
97
99
|
const opsMissionScheduleTimers = {};
|
|
98
100
|
const opsMissionScheduleRunning = {};
|
|
@@ -1263,6 +1265,81 @@ function saveOpsMissions() {
|
|
|
1263
1265
|
function listOpsMissions() {
|
|
1264
1266
|
return Object.values(opsMissions).sort((a, b) => a.name.localeCompare(b.name));
|
|
1265
1267
|
}
|
|
1268
|
+
function loadOpsMissionRunsFromDisk() {
|
|
1269
|
+
try {
|
|
1270
|
+
ensureAppStateDir();
|
|
1271
|
+
if (!fs_1.default.existsSync(OPS_MISSION_RUNS_FILE)) {
|
|
1272
|
+
fs_1.default.writeFileSync(OPS_MISSION_RUNS_FILE, JSON.stringify([], null, 2), 'utf8');
|
|
1273
|
+
opsMissionRuns.splice(0, opsMissionRuns.length);
|
|
1274
|
+
opsMissionRunSeq = 1;
|
|
1275
|
+
opsMissionRunsLoaded = true;
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const raw = fs_1.default.readFileSync(OPS_MISSION_RUNS_FILE, 'utf8');
|
|
1279
|
+
const parsed = JSON.parse(raw);
|
|
1280
|
+
if (!Array.isArray(parsed)) {
|
|
1281
|
+
opsMissionRuns.splice(0, opsMissionRuns.length);
|
|
1282
|
+
opsMissionRunSeq = 1;
|
|
1283
|
+
opsMissionRunsLoaded = true;
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const restored = [];
|
|
1287
|
+
let maxId = 0;
|
|
1288
|
+
for (const value of parsed) {
|
|
1289
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
const item = value;
|
|
1293
|
+
const id = Number(item.id);
|
|
1294
|
+
const mission = typeof item.mission === 'string' ? item.mission.trim() : '';
|
|
1295
|
+
const startedAt = typeof item.startedAt === 'string' ? item.startedAt : nowIso();
|
|
1296
|
+
const finishedAt = typeof item.finishedAt === 'string' ? item.finishedAt : startedAt;
|
|
1297
|
+
if (!Number.isFinite(id) || id <= 0 || !mission) {
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const steps = Array.isArray(item.steps)
|
|
1301
|
+
? item.steps.filter(step => step && typeof step === 'object' && !Array.isArray(step))
|
|
1302
|
+
: [];
|
|
1303
|
+
const run = {
|
|
1304
|
+
id: Math.trunc(id),
|
|
1305
|
+
mission,
|
|
1306
|
+
startedAt,
|
|
1307
|
+
finishedAt,
|
|
1308
|
+
durationMs: clampInt(item.durationMs, 0, 0, 7 * 24 * 60 * 60 * 1000),
|
|
1309
|
+
ok: item.ok === true,
|
|
1310
|
+
steps,
|
|
1311
|
+
gate: item.gate && typeof item.gate === 'object' && !Array.isArray(item.gate)
|
|
1312
|
+
? item.gate
|
|
1313
|
+
: undefined,
|
|
1314
|
+
error: typeof item.error === 'string' ? item.error : undefined,
|
|
1315
|
+
};
|
|
1316
|
+
restored.push(run);
|
|
1317
|
+
if (run.id > maxId) {
|
|
1318
|
+
maxId = run.id;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
restored.sort((a, b) => a.id - b.id);
|
|
1322
|
+
const limited = restored.slice(-MAX_JOBS);
|
|
1323
|
+
opsMissionRuns.splice(0, opsMissionRuns.length, ...limited);
|
|
1324
|
+
opsMissionRunSeq = Math.max(1, maxId + 1);
|
|
1325
|
+
opsMissionRunsLoaded = true;
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
opsMissionRuns.splice(0, opsMissionRuns.length);
|
|
1329
|
+
opsMissionRunSeq = 1;
|
|
1330
|
+
opsMissionRunsLoaded = true;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function saveOpsMissionRunsToDisk() {
|
|
1334
|
+
ensureAppStateDir();
|
|
1335
|
+
fs_1.default.writeFileSync(OPS_MISSION_RUNS_FILE, JSON.stringify(opsMissionRuns, null, 2), 'utf8');
|
|
1336
|
+
}
|
|
1337
|
+
function ensureOpsMissionRunsLoaded() {
|
|
1338
|
+
if (opsMissionRunsLoaded) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
loadOpsMissionRunsFromDisk();
|
|
1342
|
+
}
|
|
1266
1343
|
function loadOpsMissionSchedules() {
|
|
1267
1344
|
try {
|
|
1268
1345
|
ensureAppStateDir();
|
|
@@ -1503,6 +1580,192 @@ function missionSchedulePolicyForecast(horizonMsRaw) {
|
|
|
1503
1580
|
items: items.slice(0, 400),
|
|
1504
1581
|
};
|
|
1505
1582
|
}
|
|
1583
|
+
function buildOpsMissionScheduleStatus() {
|
|
1584
|
+
const policy = currentOpsMissionPolicyState();
|
|
1585
|
+
const nowMs = Date.now();
|
|
1586
|
+
const schedules = listOpsMissionSchedules().map(schedule => {
|
|
1587
|
+
const baseMsRaw = schedule.lastRunAt
|
|
1588
|
+
? Date.parse(schedule.lastRunAt)
|
|
1589
|
+
: Date.parse(schedule.updatedAt || schedule.createdAt);
|
|
1590
|
+
const baseMs = Number.isFinite(baseMsRaw) ? baseMsRaw : nowMs;
|
|
1591
|
+
const nextRunMs = baseMs + Math.max(5000, schedule.everyMs);
|
|
1592
|
+
const due = schedule.active &&
|
|
1593
|
+
!policy.suspended &&
|
|
1594
|
+
nowMs >= nextRunMs &&
|
|
1595
|
+
opsMissionScheduleRunning[schedule.id] !== true;
|
|
1596
|
+
const overdueMs = due ? nowMs - nextRunMs : 0;
|
|
1597
|
+
return {
|
|
1598
|
+
id: schedule.id,
|
|
1599
|
+
name: schedule.name,
|
|
1600
|
+
mission: schedule.mission,
|
|
1601
|
+
active: schedule.active,
|
|
1602
|
+
running: opsMissionScheduleRunning[schedule.id] === true,
|
|
1603
|
+
nextRunAt: new Date(nextRunMs).toISOString(),
|
|
1604
|
+
due,
|
|
1605
|
+
overdueMs,
|
|
1606
|
+
runs: schedule.runs,
|
|
1607
|
+
failures: schedule.failures,
|
|
1608
|
+
lastRunAt: schedule.lastRunAt,
|
|
1609
|
+
lastError: schedule.lastError,
|
|
1610
|
+
laneId: schedule.laneId,
|
|
1611
|
+
deviceId: schedule.deviceId,
|
|
1612
|
+
};
|
|
1613
|
+
});
|
|
1614
|
+
const dueCount = schedules.filter(item => item.due).length;
|
|
1615
|
+
const activeCount = schedules.filter(item => item.active).length;
|
|
1616
|
+
return {
|
|
1617
|
+
ok: true,
|
|
1618
|
+
generatedAt: nowIso(),
|
|
1619
|
+
suspended: policy.suspended,
|
|
1620
|
+
suspendedReason: policy.suspendedReason,
|
|
1621
|
+
resumeAt: policy.resumeAt,
|
|
1622
|
+
total: schedules.length,
|
|
1623
|
+
activeCount,
|
|
1624
|
+
dueCount,
|
|
1625
|
+
schedules,
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function buildOpsMissionAnalytics(limitRaw) {
|
|
1629
|
+
ensureOpsMissionRunsLoaded();
|
|
1630
|
+
const limit = clampInt(limitRaw, 300, 1, 5000);
|
|
1631
|
+
const runs = opsMissionRuns.slice(-limit);
|
|
1632
|
+
const durations = runs
|
|
1633
|
+
.map(item => Number(item.durationMs))
|
|
1634
|
+
.filter(value => Number.isFinite(value) && value >= 0)
|
|
1635
|
+
.sort((a, b) => a - b);
|
|
1636
|
+
const passCount = runs.filter(item => item.ok).length;
|
|
1637
|
+
const failCount = runs.length - passCount;
|
|
1638
|
+
const avgDurationMs = durations.length > 0
|
|
1639
|
+
? Math.round((durations.reduce((sum, value) => sum + value, 0) / durations.length) * 100) / 100
|
|
1640
|
+
: 0;
|
|
1641
|
+
const p95DurationMs = durations.length > 0
|
|
1642
|
+
? durations[Math.max(0, Math.min(durations.length - 1, Math.ceil(durations.length * 0.95) - 1))]
|
|
1643
|
+
: 0;
|
|
1644
|
+
const byMission = new Map();
|
|
1645
|
+
const failureReasons = new Map();
|
|
1646
|
+
for (const run of runs) {
|
|
1647
|
+
const bucket = byMission.get(run.mission) || {
|
|
1648
|
+
mission: run.mission,
|
|
1649
|
+
total: 0,
|
|
1650
|
+
pass: 0,
|
|
1651
|
+
fail: 0,
|
|
1652
|
+
durationTotal: 0,
|
|
1653
|
+
durations: [],
|
|
1654
|
+
gatePass: 0,
|
|
1655
|
+
gateFail: 0,
|
|
1656
|
+
lastAt: undefined,
|
|
1657
|
+
lastOk: undefined,
|
|
1658
|
+
lastError: undefined,
|
|
1659
|
+
};
|
|
1660
|
+
bucket.total += 1;
|
|
1661
|
+
if (run.ok) {
|
|
1662
|
+
bucket.pass += 1;
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
bucket.fail += 1;
|
|
1666
|
+
const reason = run.error && run.error.trim().length > 0 ? run.error.trim() : 'unknown';
|
|
1667
|
+
failureReasons.set(reason, (failureReasons.get(reason) || 0) + 1);
|
|
1668
|
+
bucket.lastError = reason;
|
|
1669
|
+
}
|
|
1670
|
+
if (typeof run.durationMs === 'number' && Number.isFinite(run.durationMs) && run.durationMs >= 0) {
|
|
1671
|
+
bucket.durationTotal += run.durationMs;
|
|
1672
|
+
bucket.durations.push(run.durationMs);
|
|
1673
|
+
}
|
|
1674
|
+
const gate = run.gate && typeof run.gate === 'object' && !Array.isArray(run.gate)
|
|
1675
|
+
? run.gate
|
|
1676
|
+
: undefined;
|
|
1677
|
+
if (gate) {
|
|
1678
|
+
if (gate.pass === true) {
|
|
1679
|
+
bucket.gatePass += 1;
|
|
1680
|
+
}
|
|
1681
|
+
else if (gate.pass === false) {
|
|
1682
|
+
bucket.gateFail += 1;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
bucket.lastAt = run.finishedAt;
|
|
1686
|
+
bucket.lastOk = run.ok;
|
|
1687
|
+
byMission.set(run.mission, bucket);
|
|
1688
|
+
}
|
|
1689
|
+
const missions = Array.from(byMission.values())
|
|
1690
|
+
.map(item => {
|
|
1691
|
+
const sorted = item.durations.slice().sort((a, b) => a - b);
|
|
1692
|
+
const avg = item.durations.length > 0 ? Math.round((item.durationTotal / item.durations.length) * 100) / 100 : 0;
|
|
1693
|
+
const p95 = sorted.length > 0
|
|
1694
|
+
? sorted[Math.max(0, Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1))]
|
|
1695
|
+
: 0;
|
|
1696
|
+
return {
|
|
1697
|
+
mission: item.mission,
|
|
1698
|
+
total: item.total,
|
|
1699
|
+
pass: item.pass,
|
|
1700
|
+
fail: item.fail,
|
|
1701
|
+
passRate: item.total > 0 ? Math.round((item.pass / item.total) * 10000) / 100 : 0,
|
|
1702
|
+
avgDurationMs: avg,
|
|
1703
|
+
p95DurationMs: p95,
|
|
1704
|
+
gatePass: item.gatePass,
|
|
1705
|
+
gateFail: item.gateFail,
|
|
1706
|
+
lastAt: item.lastAt,
|
|
1707
|
+
lastOk: item.lastOk,
|
|
1708
|
+
lastError: item.lastError,
|
|
1709
|
+
};
|
|
1710
|
+
})
|
|
1711
|
+
.sort((a, b) => b.total - a.total);
|
|
1712
|
+
const reasons = Array.from(failureReasons.entries())
|
|
1713
|
+
.map(([reason, count]) => ({ reason, count }))
|
|
1714
|
+
.sort((a, b) => b.count - a.count)
|
|
1715
|
+
.slice(0, 20);
|
|
1716
|
+
return {
|
|
1717
|
+
ok: true,
|
|
1718
|
+
generatedAt: nowIso(),
|
|
1719
|
+
window: {
|
|
1720
|
+
requested: limit,
|
|
1721
|
+
actual: runs.length,
|
|
1722
|
+
},
|
|
1723
|
+
totals: {
|
|
1724
|
+
passCount,
|
|
1725
|
+
failCount,
|
|
1726
|
+
passRate: runs.length > 0 ? Math.round((passCount / runs.length) * 10000) / 100 : 0,
|
|
1727
|
+
avgDurationMs,
|
|
1728
|
+
p95DurationMs,
|
|
1729
|
+
},
|
|
1730
|
+
missions,
|
|
1731
|
+
failureReasons: reasons,
|
|
1732
|
+
recentRuns: runs.slice(-30).reverse(),
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function runDueOpsMissionSchedules(maxItemsRaw) {
|
|
1736
|
+
const maxItems = clampInt(maxItemsRaw, 5, 1, 200);
|
|
1737
|
+
const statusBefore = buildOpsMissionScheduleStatus();
|
|
1738
|
+
if (statusBefore.suspended === true) {
|
|
1739
|
+
return {
|
|
1740
|
+
ok: false,
|
|
1741
|
+
skipped: true,
|
|
1742
|
+
reason: 'policy-suspended',
|
|
1743
|
+
statusBefore,
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
const dueSchedules = Array.isArray(statusBefore.schedules)
|
|
1747
|
+
? statusBefore.schedules
|
|
1748
|
+
.filter(item => item.due === true)
|
|
1749
|
+
.sort((a, b) => Number(b.overdueMs || 0) - Number(a.overdueMs || 0))
|
|
1750
|
+
: [];
|
|
1751
|
+
const selected = dueSchedules.slice(0, maxItems);
|
|
1752
|
+
const results = [];
|
|
1753
|
+
for (const item of selected) {
|
|
1754
|
+
const id = Number(item.id);
|
|
1755
|
+
if (!Number.isFinite(id) || id <= 0) {
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
results.push(runOpsMissionScheduleTask(Math.trunc(id)));
|
|
1759
|
+
}
|
|
1760
|
+
return {
|
|
1761
|
+
ok: results.every(item => item.ok !== false),
|
|
1762
|
+
requested: maxItems,
|
|
1763
|
+
dueBefore: dueSchedules.length,
|
|
1764
|
+
executed: results.length,
|
|
1765
|
+
results,
|
|
1766
|
+
statusAfter: buildOpsMissionScheduleStatus(),
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1506
1769
|
function loadSchedules() {
|
|
1507
1770
|
try {
|
|
1508
1771
|
ensureAppStateDir();
|
|
@@ -3807,6 +4070,7 @@ function deleteOpsMission(nameRaw) {
|
|
|
3807
4070
|
return true;
|
|
3808
4071
|
}
|
|
3809
4072
|
function listOpsMissionRuns(limitRaw) {
|
|
4073
|
+
ensureOpsMissionRunsLoaded();
|
|
3810
4074
|
const limit = clampInt(limitRaw, 120, 1, 1000);
|
|
3811
4075
|
const runs = opsMissionRuns.slice(-limit).reverse();
|
|
3812
4076
|
return {
|
|
@@ -3816,6 +4080,7 @@ function listOpsMissionRuns(limitRaw) {
|
|
|
3816
4080
|
};
|
|
3817
4081
|
}
|
|
3818
4082
|
function clearOpsMissionRuns(body) {
|
|
4083
|
+
ensureOpsMissionRunsLoaded();
|
|
3819
4084
|
const keepLatest = clampInt(body.keepLatest, 0, 0, 1000);
|
|
3820
4085
|
const before = opsMissionRuns.length;
|
|
3821
4086
|
if (keepLatest === 0) {
|
|
@@ -3825,6 +4090,7 @@ function clearOpsMissionRuns(body) {
|
|
|
3825
4090
|
opsMissionRuns.splice(0, before - keepLatest);
|
|
3826
4091
|
}
|
|
3827
4092
|
const removed = before - opsMissionRuns.length;
|
|
4093
|
+
saveOpsMissionRunsToDisk();
|
|
3828
4094
|
pushEvent('ops-mission-runs-cleared', 'Ops mission run history pruned', { removed, keepLatest });
|
|
3829
4095
|
return {
|
|
3830
4096
|
ok: true,
|
|
@@ -4173,6 +4439,7 @@ function planOpsMission(nameRaw, body, host, port) {
|
|
|
4173
4439
|
};
|
|
4174
4440
|
}
|
|
4175
4441
|
function runOpsMission(nameRaw, body, host, port) {
|
|
4442
|
+
ensureOpsMissionRunsLoaded();
|
|
4176
4443
|
const mission = resolveOpsMission(nameRaw);
|
|
4177
4444
|
const startedAt = nowIso();
|
|
4178
4445
|
const startedMs = Date.now();
|
|
@@ -4282,6 +4549,7 @@ function runOpsMission(nameRaw, body, host, port) {
|
|
|
4282
4549
|
if (opsMissionRuns.length > MAX_JOBS) {
|
|
4283
4550
|
opsMissionRuns.splice(0, opsMissionRuns.length - MAX_JOBS);
|
|
4284
4551
|
}
|
|
4552
|
+
saveOpsMissionRunsToDisk();
|
|
4285
4553
|
pushEvent('ops-mission-run', 'Ops mission executed', {
|
|
4286
4554
|
name: mission.name,
|
|
4287
4555
|
ok,
|
|
@@ -4318,6 +4586,7 @@ function runOpsMission(nameRaw, body, host, port) {
|
|
|
4318
4586
|
if (opsMissionRuns.length > MAX_JOBS) {
|
|
4319
4587
|
opsMissionRuns.splice(0, opsMissionRuns.length - MAX_JOBS);
|
|
4320
4588
|
}
|
|
4589
|
+
saveOpsMissionRunsToDisk();
|
|
4321
4590
|
pushEvent('ops-mission-failed', 'Ops mission failed', {
|
|
4322
4591
|
name: mission.name,
|
|
4323
4592
|
error: message,
|
|
@@ -4336,6 +4605,8 @@ function runOpsMission(nameRaw, body, host, port) {
|
|
|
4336
4605
|
}
|
|
4337
4606
|
}
|
|
4338
4607
|
function buildAuditExport(host, port) {
|
|
4608
|
+
const missionAnalytics = buildOpsMissionAnalytics(1000);
|
|
4609
|
+
const missionScheduleStatus = buildOpsMissionScheduleStatus();
|
|
4339
4610
|
return {
|
|
4340
4611
|
ok: true,
|
|
4341
4612
|
exportedAt: nowIso(),
|
|
@@ -4354,6 +4625,8 @@ function buildAuditExport(host, port) {
|
|
|
4354
4625
|
opsMissionRuns: opsMissionRuns.slice(-200),
|
|
4355
4626
|
opsMissionSchedules: listOpsMissionSchedules(),
|
|
4356
4627
|
opsMissionPolicy: currentOpsMissionPolicyState(),
|
|
4628
|
+
opsMissionAnalytics: missionAnalytics,
|
|
4629
|
+
opsMissionScheduleStatus: missionScheduleStatus,
|
|
4357
4630
|
controlRoomHistory: listControlRoomHistory(200),
|
|
4358
4631
|
schedules: schedulesList(),
|
|
4359
4632
|
timeline: dashboardTimeline.slice(-300),
|
|
@@ -5303,6 +5576,7 @@ function setupSse(request, response) {
|
|
|
5303
5576
|
});
|
|
5304
5577
|
}
|
|
5305
5578
|
function buildStatePayload(host, port) {
|
|
5579
|
+
const missionScheduleStatus = buildOpsMissionScheduleStatus();
|
|
5306
5580
|
const devices = (0, adb_js_1.getConnectedDevices)();
|
|
5307
5581
|
const totals = laneTotals();
|
|
5308
5582
|
return {
|
|
@@ -5328,6 +5602,7 @@ function buildStatePayload(host, port) {
|
|
|
5328
5602
|
opsMissionCount: Object.keys(opsMissions).length,
|
|
5329
5603
|
opsMissionRunCount: opsMissionRuns.length,
|
|
5330
5604
|
opsMissionScheduleCount: Object.keys(opsMissionSchedules).length,
|
|
5605
|
+
opsMissionDueCount: typeof missionScheduleStatus.dueCount === 'number' ? missionScheduleStatus.dueCount : 0,
|
|
5331
5606
|
opsMissionPolicySuspended: currentOpsMissionPolicyState().suspended,
|
|
5332
5607
|
opsMissionPolicyResumeAt: currentOpsMissionPolicyState().resumeAt,
|
|
5333
5608
|
controlRoomHistoryCount: controlRoomHistory.length,
|
|
@@ -5366,6 +5641,8 @@ function buildDashboardPayload(host, port) {
|
|
|
5366
5641
|
};
|
|
5367
5642
|
}
|
|
5368
5643
|
function buildOpsBoard(host, port) {
|
|
5644
|
+
const missionAnalytics = buildOpsMissionAnalytics(300);
|
|
5645
|
+
const missionScheduleStatus = buildOpsMissionScheduleStatus();
|
|
5369
5646
|
const recentFailedJobs = jobs
|
|
5370
5647
|
.filter(job => job.status === 'failed')
|
|
5371
5648
|
.slice(0, 20)
|
|
@@ -5412,6 +5689,8 @@ function buildOpsBoard(host, port) {
|
|
|
5412
5689
|
opsMissionRuns: opsMissionRuns.slice(-60),
|
|
5413
5690
|
opsMissionSchedules: listOpsMissionSchedules(),
|
|
5414
5691
|
opsMissionPolicy: currentOpsMissionPolicyState(),
|
|
5692
|
+
opsMissionAnalytics: missionAnalytics,
|
|
5693
|
+
opsMissionScheduleStatus: missionScheduleStatus,
|
|
5415
5694
|
controlRoomHistory: listControlRoomHistory(80),
|
|
5416
5695
|
updateHint: UPDATE_HINT,
|
|
5417
5696
|
};
|
|
@@ -5433,6 +5712,8 @@ function buildMetricsPayload() {
|
|
|
5433
5712
|
};
|
|
5434
5713
|
}
|
|
5435
5714
|
function buildSessionExport() {
|
|
5715
|
+
const missionAnalytics = buildOpsMissionAnalytics(500);
|
|
5716
|
+
const missionScheduleStatus = buildOpsMissionScheduleStatus();
|
|
5436
5717
|
return {
|
|
5437
5718
|
exportedAt: nowIso(),
|
|
5438
5719
|
state: {
|
|
@@ -5465,6 +5746,8 @@ function buildSessionExport() {
|
|
|
5465
5746
|
opsMissionRuns,
|
|
5466
5747
|
opsMissionSchedules,
|
|
5467
5748
|
opsMissionPolicy: currentOpsMissionPolicyState(),
|
|
5749
|
+
opsMissionAnalytics: missionAnalytics,
|
|
5750
|
+
opsMissionScheduleStatus: missionScheduleStatus,
|
|
5468
5751
|
controlRoomHistory,
|
|
5469
5752
|
lanes: lanesSummary(),
|
|
5470
5753
|
jobs,
|
|
@@ -6008,6 +6291,19 @@ async function handleApi(request, response, context) {
|
|
|
6008
6291
|
});
|
|
6009
6292
|
return;
|
|
6010
6293
|
}
|
|
6294
|
+
if (method === 'GET' && pathname === '/api/ops/missions/analytics') {
|
|
6295
|
+
await withMetric('ops-missions-analytics', () => {
|
|
6296
|
+
const limitRaw = Number(url.searchParams.get('limit') ?? '300');
|
|
6297
|
+
sendJson(response, 200, buildOpsMissionAnalytics(limitRaw));
|
|
6298
|
+
});
|
|
6299
|
+
return;
|
|
6300
|
+
}
|
|
6301
|
+
if (method === 'GET' && pathname === '/api/ops/missions/schedules/status') {
|
|
6302
|
+
await withMetric('ops-missions-schedules-status', () => {
|
|
6303
|
+
sendJson(response, 200, buildOpsMissionScheduleStatus());
|
|
6304
|
+
});
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6011
6307
|
if (method === 'POST' && pathname === '/api/ops/missions/schedules/pause-all') {
|
|
6012
6308
|
await withMetric('ops-missions-schedules-pause-all', () => {
|
|
6013
6309
|
sendJson(response, 200, setAllOpsMissionSchedulesActive(false));
|
|
@@ -6027,6 +6323,13 @@ async function handleApi(request, response, context) {
|
|
|
6027
6323
|
});
|
|
6028
6324
|
return;
|
|
6029
6325
|
}
|
|
6326
|
+
if (method === 'POST' && pathname === '/api/ops/missions/schedules/run-due') {
|
|
6327
|
+
await withMetric('ops-missions-schedules-run-due', async () => {
|
|
6328
|
+
const body = await readJsonBody(request);
|
|
6329
|
+
sendJson(response, 200, runDueOpsMissionSchedules(body.maxItems));
|
|
6330
|
+
});
|
|
6331
|
+
return;
|
|
6332
|
+
}
|
|
6030
6333
|
if (method === 'POST' && pathname === '/api/ops/missions/schedules') {
|
|
6031
6334
|
await withMetric('ops-missions-schedules-save', async () => {
|
|
6032
6335
|
const body = await readJsonBody(request);
|
|
@@ -7912,6 +8215,16 @@ https://developer.android.com</textarea>
|
|
|
7912
8215
|
</div>
|
|
7913
8216
|
<div id="ops-mission-schedules-panel" class="metrics"></div>
|
|
7914
8217
|
<div id="ops-mission-forecast-panel" class="metrics"></div>
|
|
8218
|
+
<div class="split">
|
|
8219
|
+
<input id="ops-mission-analytics-limit" type="number" min="1" value="300" />
|
|
8220
|
+
<button class="s" id="ops-mission-analytics-btn">Load mission analytics</button>
|
|
8221
|
+
</div>
|
|
8222
|
+
<div class="split">
|
|
8223
|
+
<button class="s" id="ops-mission-schedules-status-btn">Load schedule status</button>
|
|
8224
|
+
<button class="p" id="ops-mission-run-due-btn">Run due schedules now</button>
|
|
8225
|
+
</div>
|
|
8226
|
+
<div id="ops-mission-analytics-panel" class="metrics"></div>
|
|
8227
|
+
<div id="ops-mission-schedule-status-panel" class="metrics"></div>
|
|
7915
8228
|
<hr style="border:0;border-top:1px solid #2f4a61;" />
|
|
7916
8229
|
<div class="split">
|
|
7917
8230
|
<select id="ops-mission-policy-block">
|
|
@@ -8068,6 +8381,9 @@ https://developer.android.com</textarea>
|
|
|
8068
8381
|
const $opsMissionSchedulesFactor = document.getElementById('ops-mission-schedules-factor');
|
|
8069
8382
|
const $opsMissionForecastHorizonMs = document.getElementById('ops-mission-forecast-horizon-ms');
|
|
8070
8383
|
const $opsMissionForecastPanel = document.getElementById('ops-mission-forecast-panel');
|
|
8384
|
+
const $opsMissionAnalyticsLimit = document.getElementById('ops-mission-analytics-limit');
|
|
8385
|
+
const $opsMissionAnalyticsPanel = document.getElementById('ops-mission-analytics-panel');
|
|
8386
|
+
const $opsMissionScheduleStatusPanel = document.getElementById('ops-mission-schedule-status-panel');
|
|
8071
8387
|
const $opsMissionPolicyBlock = document.getElementById('ops-mission-policy-block');
|
|
8072
8388
|
const $opsMissionPolicyMaxFailures = document.getElementById('ops-mission-policy-max-failures');
|
|
8073
8389
|
const $opsMissionPolicyCooldownMs = document.getElementById('ops-mission-policy-cooldown-ms');
|
|
@@ -8521,6 +8837,63 @@ https://developer.android.com</textarea>
|
|
|
8521
8837
|
$opsMissionPolicyPanel.appendChild(row);
|
|
8522
8838
|
}
|
|
8523
8839
|
|
|
8840
|
+
function renderOpsMissionAnalytics(payload) {
|
|
8841
|
+
if (!$opsMissionAnalyticsPanel) {
|
|
8842
|
+
return;
|
|
8843
|
+
}
|
|
8844
|
+
const totals = payload && payload.totals ? payload.totals : {};
|
|
8845
|
+
const missions = Array.isArray(payload && payload.missions) ? payload.missions : [];
|
|
8846
|
+
const reasons = Array.isArray(payload && payload.failureReasons) ? payload.failureReasons : [];
|
|
8847
|
+
$opsMissionAnalyticsPanel.innerHTML = '';
|
|
8848
|
+
const head = document.createElement('div');
|
|
8849
|
+
head.className = 'item';
|
|
8850
|
+
head.innerHTML =
|
|
8851
|
+
'<div class="meta">mission analytics</div>' +
|
|
8852
|
+
'<div>pass=' + (totals.passCount || 0) + ' fail=' + (totals.failCount || 0) + ' passRate=' + (totals.passRate || 0) + '%</div>' +
|
|
8853
|
+
'<div>avg=' + (totals.avgDurationMs || 0) + 'ms p95=' + (totals.p95DurationMs || 0) + 'ms</div>';
|
|
8854
|
+
$opsMissionAnalyticsPanel.appendChild(head);
|
|
8855
|
+
|
|
8856
|
+
for (const item of missions.slice(0, 12)) {
|
|
8857
|
+
const row = document.createElement('div');
|
|
8858
|
+
row.className = 'item';
|
|
8859
|
+
row.innerHTML =
|
|
8860
|
+
'<div class="meta">' + item.mission + '</div>' +
|
|
8861
|
+
'<div>total=' + item.total + ' pass=' + item.pass + ' fail=' + item.fail + ' passRate=' + item.passRate + '%</div>' +
|
|
8862
|
+
'<div>avg=' + item.avgDurationMs + 'ms p95=' + item.p95DurationMs + 'ms gatePass=' + (item.gatePass || 0) + ' gateFail=' + (item.gateFail || 0) + '</div>';
|
|
8863
|
+
$opsMissionAnalyticsPanel.appendChild(row);
|
|
8864
|
+
}
|
|
8865
|
+
for (const reason of reasons.slice(0, 6)) {
|
|
8866
|
+
const row = document.createElement('div');
|
|
8867
|
+
row.className = 'item';
|
|
8868
|
+
row.innerHTML = '<div>fail reason: ' + reason.reason + ' (' + reason.count + ')</div>';
|
|
8869
|
+
$opsMissionAnalyticsPanel.appendChild(row);
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
8872
|
+
|
|
8873
|
+
function renderOpsMissionScheduleStatus(payload) {
|
|
8874
|
+
if (!$opsMissionScheduleStatusPanel) {
|
|
8875
|
+
return;
|
|
8876
|
+
}
|
|
8877
|
+
const schedules = Array.isArray(payload && payload.schedules) ? payload.schedules : [];
|
|
8878
|
+
$opsMissionScheduleStatusPanel.innerHTML = '';
|
|
8879
|
+
const head = document.createElement('div');
|
|
8880
|
+
head.className = 'item';
|
|
8881
|
+
head.innerHTML =
|
|
8882
|
+
'<div class="meta">schedule status</div>' +
|
|
8883
|
+
'<div>total=' + (payload.total || 0) + ' active=' + (payload.activeCount || 0) + ' due=' + (payload.dueCount || 0) + '</div>' +
|
|
8884
|
+
'<div>suspended=' + String(payload.suspended === true) + ' reason=' + (payload.suspendedReason || '-') + ' resumeAt=' + (payload.resumeAt || '-') + '</div>';
|
|
8885
|
+
$opsMissionScheduleStatusPanel.appendChild(head);
|
|
8886
|
+
for (const item of schedules.slice(0, 24)) {
|
|
8887
|
+
const row = document.createElement('div');
|
|
8888
|
+
row.className = 'item';
|
|
8889
|
+
row.innerHTML =
|
|
8890
|
+
'<div class="meta">#' + item.id + ' ' + item.name + ' [' + (item.active ? 'active' : 'idle') + (item.running ? ', running' : '') + ']</div>' +
|
|
8891
|
+
'<div>mission=' + item.mission + ' due=' + String(item.due === true) + ' overdueMs=' + (item.overdueMs || 0) + '</div>' +
|
|
8892
|
+
'<div>nextRunAt=' + (item.nextRunAt || '-') + ' runs=' + item.runs + ' failures=' + item.failures + '</div>';
|
|
8893
|
+
$opsMissionScheduleStatusPanel.appendChild(row);
|
|
8894
|
+
}
|
|
8895
|
+
}
|
|
8896
|
+
|
|
8524
8897
|
function renderSchedules(payload) {
|
|
8525
8898
|
const entries = Array.isArray(payload.schedules) ? payload.schedules : [];
|
|
8526
8899
|
$schedules.innerHTML = '';
|
|
@@ -10300,6 +10673,36 @@ https://developer.android.com</textarea>
|
|
|
10300
10673
|
setMessage('Mission schedule forecast ready', false);
|
|
10301
10674
|
}
|
|
10302
10675
|
|
|
10676
|
+
async function loadOpsMissionAnalyticsUi() {
|
|
10677
|
+
const limit = Number($opsMissionAnalyticsLimit.value || '300');
|
|
10678
|
+
const result = await api('/api/ops/missions/analytics?limit=' + encodeURIComponent(String(limit)));
|
|
10679
|
+
renderOpsMissionAnalytics(result);
|
|
10680
|
+
renderOutput(result);
|
|
10681
|
+
setMessage('Mission analytics loaded', false);
|
|
10682
|
+
}
|
|
10683
|
+
|
|
10684
|
+
async function loadOpsMissionScheduleStatusUi() {
|
|
10685
|
+
const result = await api('/api/ops/missions/schedules/status');
|
|
10686
|
+
renderOpsMissionScheduleStatus(result);
|
|
10687
|
+
renderOutput(result);
|
|
10688
|
+
setMessage('Mission schedule status loaded', false);
|
|
10689
|
+
}
|
|
10690
|
+
|
|
10691
|
+
async function runDueOpsMissionSchedulesUi() {
|
|
10692
|
+
const result = await api('/api/ops/missions/schedules/run-due', 'POST', {
|
|
10693
|
+
maxItems: 10,
|
|
10694
|
+
});
|
|
10695
|
+
renderOutput(result);
|
|
10696
|
+
if (result.statusAfter) {
|
|
10697
|
+
renderOpsMissionScheduleStatus(result.statusAfter);
|
|
10698
|
+
} else {
|
|
10699
|
+
await loadOpsMissionScheduleStatusUi();
|
|
10700
|
+
}
|
|
10701
|
+
setMessage(result.ok ? 'Due schedules executed' : 'Due schedule run completed with issues', !result.ok);
|
|
10702
|
+
await listOpsMissionRunsUi();
|
|
10703
|
+
await listOpsMissionSchedulesUi();
|
|
10704
|
+
}
|
|
10705
|
+
|
|
10303
10706
|
async function runOpsStabilizeUi() {
|
|
10304
10707
|
const result = await api('/api/ops/stabilize', 'POST', {
|
|
10305
10708
|
laneId: selectedLaneId(),
|
|
@@ -10687,6 +11090,15 @@ https://developer.android.com</textarea>
|
|
|
10687
11090
|
document.getElementById('ops-mission-forecast-btn').addEventListener('click', async function () {
|
|
10688
11091
|
try { await forecastOpsMissionSchedulesUi(); } catch (error) { setMessage(String(error), true); }
|
|
10689
11092
|
});
|
|
11093
|
+
document.getElementById('ops-mission-analytics-btn').addEventListener('click', async function () {
|
|
11094
|
+
try { await loadOpsMissionAnalyticsUi(); } catch (error) { setMessage(String(error), true); }
|
|
11095
|
+
});
|
|
11096
|
+
document.getElementById('ops-mission-schedules-status-btn').addEventListener('click', async function () {
|
|
11097
|
+
try { await loadOpsMissionScheduleStatusUi(); } catch (error) { setMessage(String(error), true); }
|
|
11098
|
+
});
|
|
11099
|
+
document.getElementById('ops-mission-run-due-btn').addEventListener('click', async function () {
|
|
11100
|
+
try { await runDueOpsMissionSchedulesUi(); } catch (error) { setMessage(String(error), true); }
|
|
11101
|
+
});
|
|
10690
11102
|
document.getElementById('ops-mission-policy-load-btn').addEventListener('click', async function () {
|
|
10691
11103
|
try { await loadOpsMissionPolicyUi(); } catch (error) { setMessage(String(error), true); }
|
|
10692
11104
|
});
|
|
@@ -10777,6 +11189,8 @@ https://developer.android.com</textarea>
|
|
|
10777
11189
|
await listOpsMissionSchedulesUi();
|
|
10778
11190
|
await loadOpsMissionPolicyUi();
|
|
10779
11191
|
await forecastOpsMissionSchedulesUi();
|
|
11192
|
+
await loadOpsMissionAnalyticsUi();
|
|
11193
|
+
await loadOpsMissionScheduleStatusUi();
|
|
10780
11194
|
renderOutput({ ok: true, message: 'UI ready', updateHint: '${UPDATE_HINT}' });
|
|
10781
11195
|
setMessage('Ready.', false);
|
|
10782
11196
|
|
|
@@ -10803,6 +11217,10 @@ https://developer.android.com</textarea>
|
|
|
10803
11217
|
renderOpsMissionSchedules(missionSchedulesPayload);
|
|
10804
11218
|
const missionPolicyPayload = await api('/api/ops/missions/policy');
|
|
10805
11219
|
renderOpsMissionPolicy(missionPolicyPayload);
|
|
11220
|
+
const missionStatusPayload = await api('/api/ops/missions/schedules/status');
|
|
11221
|
+
renderOpsMissionScheduleStatus(missionStatusPayload);
|
|
11222
|
+
const missionAnalyticsPayload = await api('/api/ops/missions/analytics?limit=200');
|
|
11223
|
+
renderOpsMissionAnalytics(missionAnalyticsPayload);
|
|
10806
11224
|
const schedulesPayload = await api('/api/schedules');
|
|
10807
11225
|
renderSchedules(schedulesPayload);
|
|
10808
11226
|
const queueBoardPayload = await api('/api/board/queue');
|
|
@@ -10835,6 +11253,7 @@ https://developer.android.com</textarea>
|
|
|
10835
11253
|
function startWebUiServer(options = {}) {
|
|
10836
11254
|
const host = options.host ?? exports.DEFAULT_WEB_UI_HOST;
|
|
10837
11255
|
const port = options.port ?? exports.DEFAULT_WEB_UI_PORT;
|
|
11256
|
+
ensureOpsMissionRunsLoaded();
|
|
10838
11257
|
ensureSchedulesInitialized();
|
|
10839
11258
|
ensureOpsMissionSchedulesInitialized();
|
|
10840
11259
|
const server = http_1.default.createServer(async (request, response) => {
|