the-android-mcp 3.13.0 → 3.14.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/web-ui.d.ts.map +1 -1
- package/dist/web-ui.js +405 -2
- 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;AAuhQzC,wBAAgB,gBAAgB,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CAAC,MAAM,CA4B5F;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
|
@@ -2355,6 +2355,41 @@ function runRunbookCampaign(body) {
|
|
|
2355
2355
|
updateHint: UPDATE_HINT,
|
|
2356
2356
|
};
|
|
2357
2357
|
}
|
|
2358
|
+
function planRunbookCampaign(body) {
|
|
2359
|
+
const runbookName = typeof body.name === 'string' ? body.name.trim() : '';
|
|
2360
|
+
if (!runbookName) {
|
|
2361
|
+
throw new Error('runbook name is required');
|
|
2362
|
+
}
|
|
2363
|
+
const requestedDeviceIds = Array.isArray(body.deviceIds)
|
|
2364
|
+
? body.deviceIds.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
2365
|
+
: [];
|
|
2366
|
+
const connectedDeviceIds = (0, adb_js_1.getConnectedDevices)().map(device => device.id);
|
|
2367
|
+
const targetDeviceIds = requestedDeviceIds.length > 0 ? requestedDeviceIds : connectedDeviceIds;
|
|
2368
|
+
if (targetDeviceIds.length === 0) {
|
|
2369
|
+
throw new Error('no connected devices for campaign');
|
|
2370
|
+
}
|
|
2371
|
+
const laneHints = targetDeviceIds.map(deviceId => ({
|
|
2372
|
+
deviceId,
|
|
2373
|
+
laneId: `device:${deviceId}`,
|
|
2374
|
+
queueDepth: lanes[`device:${deviceId}`]?.queue.length ?? 0,
|
|
2375
|
+
}));
|
|
2376
|
+
return {
|
|
2377
|
+
ok: true,
|
|
2378
|
+
name: runbookName,
|
|
2379
|
+
targetCount: targetDeviceIds.length,
|
|
2380
|
+
targets: laneHints,
|
|
2381
|
+
payloadDefaults: {
|
|
2382
|
+
packageName: typeof body.packageName === 'string' ? body.packageName : 'com.android.chrome',
|
|
2383
|
+
waitForReadyMs: clampInt(body.waitForReadyMs, 900, 200, 10000),
|
|
2384
|
+
},
|
|
2385
|
+
recommendations: [
|
|
2386
|
+
'Start with continueOnError=true for multi-device execution.',
|
|
2387
|
+
'Use queue snapshots before campaign for fast rollback.',
|
|
2388
|
+
'Run control-room after campaign to verify stability score.',
|
|
2389
|
+
],
|
|
2390
|
+
updateHint: UPDATE_HINT,
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2358
2393
|
function seedRecommendedAlertRules(body) {
|
|
2359
2394
|
const totals = laneTotals();
|
|
2360
2395
|
const avgQueuePerLane = totals.laneCount > 0 ? Math.ceil(totals.queueDepth / totals.laneCount) : totals.queueDepth;
|
|
@@ -2471,6 +2506,36 @@ function buildControlRoomPayload(host, port) {
|
|
|
2471
2506
|
updateHint: UPDATE_HINT,
|
|
2472
2507
|
};
|
|
2473
2508
|
}
|
|
2509
|
+
function previewAutoHeal(body) {
|
|
2510
|
+
const laneId = typeof body.laneId === 'string' && body.laneId.trim().length > 0 ? body.laneId.trim() : undefined;
|
|
2511
|
+
const laneTargets = laneId ? [laneId] : Object.keys(lanes);
|
|
2512
|
+
const resumableLanes = laneTargets.filter(id => Boolean(lanes[id]?.paused));
|
|
2513
|
+
const retryFailedLimit = clampInt(body.retryFailedLimit, opsPolicy.autoRetryFailedLimit, 0, 500);
|
|
2514
|
+
const retryCandidates = jobs
|
|
2515
|
+
.filter(job => job.status === 'failed')
|
|
2516
|
+
.filter(job => !laneId || job.laneId === laneId)
|
|
2517
|
+
.slice(0, retryFailedLimit);
|
|
2518
|
+
const openIncidentCount = alertIncidents.filter(item => !item.acknowledgedAt).length;
|
|
2519
|
+
const runRecoverRunbook = body.runRecoverRunbook === true;
|
|
2520
|
+
return {
|
|
2521
|
+
ok: true,
|
|
2522
|
+
laneId,
|
|
2523
|
+
laneTargetCount: laneTargets.length,
|
|
2524
|
+
openIncidentCount,
|
|
2525
|
+
resumableLaneCount: resumableLanes.length,
|
|
2526
|
+
resumableLanes,
|
|
2527
|
+
retryFailedLimit,
|
|
2528
|
+
retryCandidateCount: retryCandidates.length,
|
|
2529
|
+
retryCandidates: retryCandidates.map(job => ({
|
|
2530
|
+
id: job.id,
|
|
2531
|
+
type: job.type,
|
|
2532
|
+
laneId: job.laneId,
|
|
2533
|
+
error: job.error,
|
|
2534
|
+
})),
|
|
2535
|
+
wouldRunRecoverRunbook: runRecoverRunbook,
|
|
2536
|
+
updateHint: UPDATE_HINT,
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2474
2539
|
function runAutoHeal(body) {
|
|
2475
2540
|
const laneId = typeof body.laneId === 'string' && body.laneId.trim().length > 0 ? body.laneId.trim() : undefined;
|
|
2476
2541
|
const ackOpenIncidents = body.ackOpenIncidents !== false;
|
|
@@ -2548,6 +2613,64 @@ function runAutoHeal(body) {
|
|
|
2548
2613
|
updateHint: UPDATE_HINT,
|
|
2549
2614
|
};
|
|
2550
2615
|
}
|
|
2616
|
+
function runOpsStabilize(body, host, port) {
|
|
2617
|
+
const seeded = seedRecommendedAlertRules(body);
|
|
2618
|
+
const before = buildControlRoomPayload(host, port);
|
|
2619
|
+
const alertsCheck = evaluateAlertRulesNow();
|
|
2620
|
+
const healed = runAutoHeal({
|
|
2621
|
+
...body,
|
|
2622
|
+
ackOpenIncidents: body.ackOpenIncidents !== false,
|
|
2623
|
+
resumePausedLanes: body.resumePausedLanes !== false,
|
|
2624
|
+
runRecoverRunbook: body.runRecoverRunbook !== false,
|
|
2625
|
+
});
|
|
2626
|
+
const after = buildControlRoomPayload(host, port);
|
|
2627
|
+
pushEvent('ops-stabilize', 'One-click stabilize workflow executed', {
|
|
2628
|
+
beforeSeverity: before.severity,
|
|
2629
|
+
afterSeverity: after.severity,
|
|
2630
|
+
});
|
|
2631
|
+
return {
|
|
2632
|
+
ok: true,
|
|
2633
|
+
seeded,
|
|
2634
|
+
alertsCheck,
|
|
2635
|
+
healed,
|
|
2636
|
+
before,
|
|
2637
|
+
after,
|
|
2638
|
+
updateHint: UPDATE_HINT,
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
function runWatchdog(body, host, port) {
|
|
2642
|
+
const controlBefore = buildControlRoomPayload(host, port);
|
|
2643
|
+
const diagnostics = buildDiagnosticsReport(host, port);
|
|
2644
|
+
const alertsCheck = evaluateAlertRulesNow();
|
|
2645
|
+
const autoHealOnAlert = body.autoHealOnAlert === true;
|
|
2646
|
+
let autoHealResult;
|
|
2647
|
+
if (autoHealOnAlert && (controlBefore.severity === 'red' || controlBefore.severity === 'amber')) {
|
|
2648
|
+
autoHealResult = runAutoHeal({
|
|
2649
|
+
laneId: typeof body.laneId === 'string' ? body.laneId : undefined,
|
|
2650
|
+
retryFailedLimit: clampInt(body.retryFailedLimit, 12, 0, 500),
|
|
2651
|
+
runRecoverRunbook: true,
|
|
2652
|
+
maxRunbooks: clampInt(body.maxRunbooks, 2, 1, 50),
|
|
2653
|
+
ackOpenIncidents: true,
|
|
2654
|
+
resumePausedLanes: true,
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
const controlAfter = buildControlRoomPayload(host, port);
|
|
2658
|
+
pushEvent('ops-watchdog', 'Watchdog check executed', {
|
|
2659
|
+
beforeSeverity: controlBefore.severity,
|
|
2660
|
+
afterSeverity: controlAfter.severity,
|
|
2661
|
+
autoHealTriggered: Boolean(autoHealResult),
|
|
2662
|
+
});
|
|
2663
|
+
return {
|
|
2664
|
+
ok: true,
|
|
2665
|
+
controlBefore,
|
|
2666
|
+
alertsCheck,
|
|
2667
|
+
diagnostics,
|
|
2668
|
+
autoHealTriggered: Boolean(autoHealResult),
|
|
2669
|
+
autoHealResult,
|
|
2670
|
+
controlAfter,
|
|
2671
|
+
updateHint: UPDATE_HINT,
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2551
2674
|
function buildAuditExport(host, port) {
|
|
2552
2675
|
return {
|
|
2553
2676
|
ok: true,
|
|
@@ -2908,6 +3031,63 @@ function applyQueueSnapshot(nameRaw, body) {
|
|
|
2908
3031
|
updateHint: UPDATE_HINT,
|
|
2909
3032
|
};
|
|
2910
3033
|
}
|
|
3034
|
+
function diffQueueSnapshot(nameRaw) {
|
|
3035
|
+
const name = nameRaw.trim();
|
|
3036
|
+
if (!name) {
|
|
3037
|
+
throw new Error('snapshot name is required');
|
|
3038
|
+
}
|
|
3039
|
+
const snapshot = queueSnapshots[name];
|
|
3040
|
+
if (!snapshot) {
|
|
3041
|
+
throw new Error(`snapshot '${name}' not found`);
|
|
3042
|
+
}
|
|
3043
|
+
const currentExport = exportQueueState();
|
|
3044
|
+
const currentStats = queueSnapshotStats(currentExport);
|
|
3045
|
+
const storedStats = queueSnapshotStats(snapshot.payload);
|
|
3046
|
+
const storedMap = new Map();
|
|
3047
|
+
for (const lane of storedStats.lanes) {
|
|
3048
|
+
const id = typeof lane.laneId === 'string' ? lane.laneId : '';
|
|
3049
|
+
if (!id) {
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
const queued = Array.isArray(lane.queued) ? lane.queued.length : 0;
|
|
3053
|
+
storedMap.set(id, queued);
|
|
3054
|
+
}
|
|
3055
|
+
const currentMap = new Map();
|
|
3056
|
+
for (const lane of currentStats.lanes) {
|
|
3057
|
+
const id = typeof lane.laneId === 'string' ? lane.laneId : '';
|
|
3058
|
+
if (!id) {
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
const queued = Array.isArray(lane.queued) ? lane.queued.length : 0;
|
|
3062
|
+
currentMap.set(id, queued);
|
|
3063
|
+
}
|
|
3064
|
+
const laneDiffs = [];
|
|
3065
|
+
const laneIds = new Set([...storedMap.keys(), ...currentMap.keys()]);
|
|
3066
|
+
for (const laneId of laneIds) {
|
|
3067
|
+
const snapshotQueued = storedMap.get(laneId) ?? 0;
|
|
3068
|
+
const currentQueued = currentMap.get(laneId) ?? 0;
|
|
3069
|
+
laneDiffs.push({
|
|
3070
|
+
laneId,
|
|
3071
|
+
snapshotQueued,
|
|
3072
|
+
currentQueued,
|
|
3073
|
+
delta: currentQueued - snapshotQueued,
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
return {
|
|
3077
|
+
ok: true,
|
|
3078
|
+
name,
|
|
3079
|
+
snapshotCapturedAt: snapshot.capturedAt,
|
|
3080
|
+
summary: {
|
|
3081
|
+
snapshotLaneCount: storedStats.laneCount,
|
|
3082
|
+
currentLaneCount: currentStats.laneCount,
|
|
3083
|
+
snapshotQueuedCount: storedStats.queuedCount,
|
|
3084
|
+
currentQueuedCount: currentStats.queuedCount,
|
|
3085
|
+
queuedDelta: currentStats.queuedCount - storedStats.queuedCount,
|
|
3086
|
+
},
|
|
3087
|
+
laneDiffs: laneDiffs.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta)).slice(0, 40),
|
|
3088
|
+
updateHint: UPDATE_HINT,
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
2911
3091
|
function schedulesList() {
|
|
2912
3092
|
return Object.values(schedules)
|
|
2913
3093
|
.sort((a, b) => a.id - b.id)
|
|
@@ -3776,6 +3956,27 @@ async function handleApi(request, response, context) {
|
|
|
3776
3956
|
});
|
|
3777
3957
|
return;
|
|
3778
3958
|
}
|
|
3959
|
+
if (method === 'POST' && pathname === '/api/ops/auto-heal/preview') {
|
|
3960
|
+
await withMetric('ops-auto-heal-preview', async () => {
|
|
3961
|
+
const body = await readJsonBody(request);
|
|
3962
|
+
sendJson(response, 200, previewAutoHeal(body));
|
|
3963
|
+
});
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
if (method === 'POST' && pathname === '/api/ops/stabilize') {
|
|
3967
|
+
await withMetric('ops-stabilize', async () => {
|
|
3968
|
+
const body = await readJsonBody(request);
|
|
3969
|
+
sendJson(response, 200, runOpsStabilize(body, context.host, context.port));
|
|
3970
|
+
});
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
if (method === 'POST' && pathname === '/api/ops/watchdog/run') {
|
|
3974
|
+
await withMetric('ops-watchdog', async () => {
|
|
3975
|
+
const body = await readJsonBody(request);
|
|
3976
|
+
sendJson(response, 200, runWatchdog(body, context.host, context.port));
|
|
3977
|
+
});
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3779
3980
|
if (method === 'GET' && pathname === '/api/diagnostics/report') {
|
|
3780
3981
|
await withMetric('diagnostics-report', () => {
|
|
3781
3982
|
sendJson(response, 200, buildDiagnosticsReport(context.host, context.port));
|
|
@@ -4060,6 +4261,20 @@ async function handleApi(request, response, context) {
|
|
|
4060
4261
|
});
|
|
4061
4262
|
return;
|
|
4062
4263
|
}
|
|
4264
|
+
if (method === 'POST' && pathname === '/api/queue/snapshots/diff') {
|
|
4265
|
+
await withMetric('queue-snapshots-diff', async () => {
|
|
4266
|
+
const body = await readJsonBody(request);
|
|
4267
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
4268
|
+
try {
|
|
4269
|
+
sendJson(response, 200, diffQueueSnapshot(name));
|
|
4270
|
+
}
|
|
4271
|
+
catch (error) {
|
|
4272
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4273
|
+
sendJson(response, 400, { error: message });
|
|
4274
|
+
}
|
|
4275
|
+
});
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4063
4278
|
if (method === 'POST' && pathname === '/api/lanes/pause-all') {
|
|
4064
4279
|
await withMetric('lanes-pause-all', () => {
|
|
4065
4280
|
const changed = setAllLanesPaused(true);
|
|
@@ -4748,6 +4963,20 @@ async function handleApi(request, response, context) {
|
|
|
4748
4963
|
});
|
|
4749
4964
|
return;
|
|
4750
4965
|
}
|
|
4966
|
+
if (method === 'POST' && pathname === '/api/runbook/campaign/plan') {
|
|
4967
|
+
await withMetric('runbook-campaign-plan', async () => {
|
|
4968
|
+
const body = await readJsonBody(request);
|
|
4969
|
+
try {
|
|
4970
|
+
const result = planRunbookCampaign(body);
|
|
4971
|
+
sendJson(response, 200, result);
|
|
4972
|
+
}
|
|
4973
|
+
catch (error) {
|
|
4974
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4975
|
+
sendJson(response, 400, { error: message });
|
|
4976
|
+
}
|
|
4977
|
+
});
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4751
4980
|
if (method === 'GET' && pathname === '/api/runbook/catalog') {
|
|
4752
4981
|
await withMetric('runbook-catalog', () => {
|
|
4753
4982
|
sendJson(response, 200, {
|
|
@@ -5329,20 +5558,36 @@ https://developer.android.com</textarea>
|
|
|
5329
5558
|
<button class="p" id="control-room-btn">Load control room</button>
|
|
5330
5559
|
<button class="w" id="auto-heal-btn">Run auto-heal</button>
|
|
5331
5560
|
</div>
|
|
5561
|
+
<div class="split">
|
|
5562
|
+
<input id="ops-lane-id" placeholder="lane id (optional)" />
|
|
5563
|
+
<input id="watchdog-auto-heal" placeholder="watchdog auto-heal: true/false" value="true" />
|
|
5564
|
+
</div>
|
|
5332
5565
|
<div class="split">
|
|
5333
5566
|
<input id="auto-heal-retry-limit" type="number" min="0" value="15" />
|
|
5334
5567
|
<input id="auto-heal-runbooks" type="number" min="1" value="2" />
|
|
5335
5568
|
</div>
|
|
5569
|
+
<div class="split">
|
|
5570
|
+
<button class="s" id="auto-heal-preview-btn">Preview auto-heal</button>
|
|
5571
|
+
<button class="p" id="ops-stabilize-btn">Run stabilize</button>
|
|
5572
|
+
</div>
|
|
5573
|
+
<button class="s" id="watchdog-run-btn">Run watchdog cycle</button>
|
|
5574
|
+
<div id="control-room-panel" class="metrics"></div>
|
|
5336
5575
|
<hr style="border:0;border-top:1px solid #2f4a61;" />
|
|
5337
5576
|
<input id="queue-snapshot-name" placeholder="queue snapshot name" value="pre-change" />
|
|
5338
5577
|
<div class="split">
|
|
5339
5578
|
<button class="s" id="queue-snapshot-save-btn">Save queue snapshot</button>
|
|
5340
5579
|
<button class="s" id="queue-snapshot-list-btn">List queue snapshots</button>
|
|
5341
5580
|
</div>
|
|
5342
|
-
<
|
|
5581
|
+
<div class="split">
|
|
5582
|
+
<button class="p" id="queue-snapshot-apply-btn">Apply queue snapshot</button>
|
|
5583
|
+
<button class="s" id="queue-snapshot-diff-btn">Diff queue snapshot</button>
|
|
5584
|
+
</div>
|
|
5343
5585
|
<textarea id="campaign-device-ids">[]</textarea>
|
|
5344
5586
|
<div class="split">
|
|
5345
5587
|
<button class="p" id="runbook-campaign-btn">Run runbook campaign</button>
|
|
5588
|
+
<button class="s" id="runbook-campaign-plan-btn">Plan campaign</button>
|
|
5589
|
+
</div>
|
|
5590
|
+
<div class="split">
|
|
5346
5591
|
<button class="s" id="alert-seed-btn">Seed alert pack</button>
|
|
5347
5592
|
</div>
|
|
5348
5593
|
</article>
|
|
@@ -5440,8 +5685,11 @@ https://developer.android.com</textarea>
|
|
|
5440
5685
|
const $cloneTargetDevice = document.getElementById('clone-target-device');
|
|
5441
5686
|
const $incidentsKeepLatest = document.getElementById('incidents-keep-latest');
|
|
5442
5687
|
const $incidentsOlderThan = document.getElementById('incidents-older-than');
|
|
5688
|
+
const $opsLaneId = document.getElementById('ops-lane-id');
|
|
5689
|
+
const $watchdogAutoHeal = document.getElementById('watchdog-auto-heal');
|
|
5443
5690
|
const $autoHealRetryLimit = document.getElementById('auto-heal-retry-limit');
|
|
5444
5691
|
const $autoHealRunbooks = document.getElementById('auto-heal-runbooks');
|
|
5692
|
+
const $controlRoomPanel = document.getElementById('control-room-panel');
|
|
5445
5693
|
const $queueSnapshotName = document.getElementById('queue-snapshot-name');
|
|
5446
5694
|
const $campaignDeviceIds = document.getElementById('campaign-device-ids');
|
|
5447
5695
|
|
|
@@ -5638,6 +5886,33 @@ https://developer.android.com</textarea>
|
|
|
5638
5886
|
}
|
|
5639
5887
|
}
|
|
5640
5888
|
|
|
5889
|
+
function renderControlRoom(payload) {
|
|
5890
|
+
if (!$controlRoomPanel) {
|
|
5891
|
+
return;
|
|
5892
|
+
}
|
|
5893
|
+
$controlRoomPanel.innerHTML = '';
|
|
5894
|
+
const severity = payload && payload.severity ? String(payload.severity) : 'unknown';
|
|
5895
|
+
const score = payload && typeof payload.score === 'number' ? payload.score : 0;
|
|
5896
|
+
const queuePressure = payload && typeof payload.queuePressure === 'number' ? payload.queuePressure : 0;
|
|
5897
|
+
const openIncidentCount = payload && typeof payload.openIncidentCount === 'number' ? payload.openIncidentCount : 0;
|
|
5898
|
+
const failedJobCount = payload && typeof payload.failedJobCount === 'number' ? payload.failedJobCount : 0;
|
|
5899
|
+
const top = document.createElement('div');
|
|
5900
|
+
top.className = 'item';
|
|
5901
|
+
top.innerHTML =
|
|
5902
|
+
'<div class="meta">control room</div>' +
|
|
5903
|
+
'<div>severity=' + severity + ' score=' + score + '</div>' +
|
|
5904
|
+
'<div>queuePressure=' + queuePressure + ' openIncidents=' + openIncidentCount + ' failedJobs=' + failedJobCount + '</div>';
|
|
5905
|
+
$controlRoomPanel.appendChild(top);
|
|
5906
|
+
|
|
5907
|
+
const recs = Array.isArray(payload && payload.recommendations) ? payload.recommendations : [];
|
|
5908
|
+
for (const rec of recs.slice(0, 6)) {
|
|
5909
|
+
const row = document.createElement('div');
|
|
5910
|
+
row.className = 'item';
|
|
5911
|
+
row.innerHTML = '<div>' + String(rec) + '</div>';
|
|
5912
|
+
$controlRoomPanel.appendChild(row);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
|
|
5641
5916
|
function renderSchedules(payload) {
|
|
5642
5917
|
const entries = Array.isArray(payload.schedules) ? payload.schedules : [];
|
|
5643
5918
|
$schedules.innerHTML = '';
|
|
@@ -5823,6 +6098,25 @@ https://developer.android.com</textarea>
|
|
|
5823
6098
|
};
|
|
5824
6099
|
}
|
|
5825
6100
|
|
|
6101
|
+
function selectedLaneId() {
|
|
6102
|
+
const explicit = ($opsLaneId.value || '').trim();
|
|
6103
|
+
if (explicit) {
|
|
6104
|
+
return explicit;
|
|
6105
|
+
}
|
|
6106
|
+
return selectedDeviceId() ? 'device:' + selectedDeviceId() : undefined;
|
|
6107
|
+
}
|
|
6108
|
+
|
|
6109
|
+
function asBoolean(value, fallback) {
|
|
6110
|
+
const text = String(value || '').trim().toLowerCase();
|
|
6111
|
+
if (text === 'true' || text === '1' || text === 'yes' || text === 'y') {
|
|
6112
|
+
return true;
|
|
6113
|
+
}
|
|
6114
|
+
if (text === 'false' || text === '0' || text === 'no' || text === 'n') {
|
|
6115
|
+
return false;
|
|
6116
|
+
}
|
|
6117
|
+
return fallback;
|
|
6118
|
+
}
|
|
6119
|
+
|
|
5826
6120
|
async function refreshJobsAndLanes() {
|
|
5827
6121
|
const jobsPayload = await api('/api/jobs');
|
|
5828
6122
|
renderJobs(jobsPayload);
|
|
@@ -5863,6 +6157,9 @@ https://developer.android.com</textarea>
|
|
|
5863
6157
|
if (!$cloneSourceLane.value || !$cloneSourceLane.value.trim()) {
|
|
5864
6158
|
$cloneSourceLane.value = 'device:' + state.deviceId;
|
|
5865
6159
|
}
|
|
6160
|
+
if (!$opsLaneId.value || !$opsLaneId.value.trim()) {
|
|
6161
|
+
$opsLaneId.value = 'device:' + state.deviceId;
|
|
6162
|
+
}
|
|
5866
6163
|
if (!$campaignDeviceIds.value || !$campaignDeviceIds.value.trim()) {
|
|
5867
6164
|
$campaignDeviceIds.value = JSON.stringify([state.deviceId], null, 2);
|
|
5868
6165
|
}
|
|
@@ -6711,13 +7008,14 @@ https://developer.android.com</textarea>
|
|
|
6711
7008
|
|
|
6712
7009
|
async function loadControlRoomUi() {
|
|
6713
7010
|
const result = await api('/api/ops/control-room');
|
|
7011
|
+
renderControlRoom(result);
|
|
6714
7012
|
renderOutput(result);
|
|
6715
7013
|
setMessage('Control room loaded', false);
|
|
6716
7014
|
}
|
|
6717
7015
|
|
|
6718
7016
|
async function runAutoHealUi() {
|
|
6719
7017
|
const result = await api('/api/ops/auto-heal', 'POST', {
|
|
6720
|
-
laneId:
|
|
7018
|
+
laneId: selectedLaneId(),
|
|
6721
7019
|
ackOpenIncidents: true,
|
|
6722
7020
|
resumePausedLanes: true,
|
|
6723
7021
|
retryFailedLimit: Number($autoHealRetryLimit.value || '15'),
|
|
@@ -6728,6 +7026,17 @@ https://developer.android.com</textarea>
|
|
|
6728
7026
|
setMessage('Auto-heal executed', false);
|
|
6729
7027
|
await refreshJobsAndLanes();
|
|
6730
7028
|
await loadAlertsUi();
|
|
7029
|
+
await loadControlRoomUi();
|
|
7030
|
+
}
|
|
7031
|
+
|
|
7032
|
+
async function previewAutoHealUi() {
|
|
7033
|
+
const result = await api('/api/ops/auto-heal/preview', 'POST', {
|
|
7034
|
+
laneId: selectedLaneId(),
|
|
7035
|
+
retryFailedLimit: Number($autoHealRetryLimit.value || '15'),
|
|
7036
|
+
runRecoverRunbook: true,
|
|
7037
|
+
});
|
|
7038
|
+
renderOutput(result);
|
|
7039
|
+
setMessage('Auto-heal preview ready', false);
|
|
6731
7040
|
}
|
|
6732
7041
|
|
|
6733
7042
|
async function saveQueueSnapshotUi() {
|
|
@@ -6764,6 +7073,16 @@ https://developer.android.com</textarea>
|
|
|
6764
7073
|
await refreshJobsAndLanes();
|
|
6765
7074
|
}
|
|
6766
7075
|
|
|
7076
|
+
async function diffQueueSnapshotUi() {
|
|
7077
|
+
const name = ($queueSnapshotName.value || '').trim();
|
|
7078
|
+
if (!name) {
|
|
7079
|
+
throw new Error('queue snapshot name required');
|
|
7080
|
+
}
|
|
7081
|
+
const result = await api('/api/queue/snapshots/diff', 'POST', { name });
|
|
7082
|
+
renderOutput(result);
|
|
7083
|
+
setMessage('Queue snapshot diff loaded', false);
|
|
7084
|
+
}
|
|
7085
|
+
|
|
6767
7086
|
async function runRunbookCampaignUi() {
|
|
6768
7087
|
let deviceIds = [];
|
|
6769
7088
|
const raw = ($campaignDeviceIds.value || '').trim();
|
|
@@ -6798,6 +7117,36 @@ https://developer.android.com</textarea>
|
|
|
6798
7117
|
await refreshJobsAndLanes();
|
|
6799
7118
|
}
|
|
6800
7119
|
|
|
7120
|
+
async function planRunbookCampaignUi() {
|
|
7121
|
+
let deviceIds = [];
|
|
7122
|
+
const raw = ($campaignDeviceIds.value || '').trim();
|
|
7123
|
+
if (raw) {
|
|
7124
|
+
if (raw.startsWith('[')) {
|
|
7125
|
+
try {
|
|
7126
|
+
const parsed = JSON.parse(raw);
|
|
7127
|
+
if (Array.isArray(parsed)) {
|
|
7128
|
+
deviceIds = parsed.filter(function (value) { return typeof value === 'string' && value.trim().length > 0; });
|
|
7129
|
+
}
|
|
7130
|
+
} catch (error) {
|
|
7131
|
+
throw new Error('campaign devices must be JSON array');
|
|
7132
|
+
}
|
|
7133
|
+
} else {
|
|
7134
|
+
deviceIds = raw
|
|
7135
|
+
.split('\n')
|
|
7136
|
+
.map(function (line) { return line.trim(); })
|
|
7137
|
+
.filter(function (line) { return line.length > 0; });
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
const result = await api('/api/runbook/campaign/plan', 'POST', {
|
|
7141
|
+
name: $runbookName.value || 'recover-lane',
|
|
7142
|
+
deviceIds,
|
|
7143
|
+
packageName: 'com.android.chrome',
|
|
7144
|
+
waitForReadyMs: 900,
|
|
7145
|
+
});
|
|
7146
|
+
renderOutput(result);
|
|
7147
|
+
setMessage('Runbook campaign plan ready', false);
|
|
7148
|
+
}
|
|
7149
|
+
|
|
6801
7150
|
async function seedAlertRulesUi() {
|
|
6802
7151
|
const result = await api('/api/alerts/rules/seed', 'POST', {
|
|
6803
7152
|
cooldownMs: Number($alertCooldown.value || '120000'),
|
|
@@ -6808,6 +7157,42 @@ https://developer.android.com</textarea>
|
|
|
6808
7157
|
await loadAlertsUi();
|
|
6809
7158
|
}
|
|
6810
7159
|
|
|
7160
|
+
async function runOpsStabilizeUi() {
|
|
7161
|
+
const result = await api('/api/ops/stabilize', 'POST', {
|
|
7162
|
+
laneId: selectedLaneId(),
|
|
7163
|
+
retryFailedLimit: Number($autoHealRetryLimit.value || '15'),
|
|
7164
|
+
maxRunbooks: Number($autoHealRunbooks.value || '2'),
|
|
7165
|
+
runRecoverRunbook: true,
|
|
7166
|
+
ackOpenIncidents: true,
|
|
7167
|
+
resumePausedLanes: true,
|
|
7168
|
+
cooldownMs: Number($alertCooldown.value || '120000'),
|
|
7169
|
+
queueDepthThreshold: Number($alertThreshold.value || '10'),
|
|
7170
|
+
});
|
|
7171
|
+
renderOutput(result);
|
|
7172
|
+
setMessage('Stabilize workflow completed', false);
|
|
7173
|
+
await refreshJobsAndLanes();
|
|
7174
|
+
await loadAlertsUi();
|
|
7175
|
+
if (result.after) {
|
|
7176
|
+
renderControlRoom(result.after);
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
7179
|
+
|
|
7180
|
+
async function runWatchdogUi() {
|
|
7181
|
+
const result = await api('/api/ops/watchdog/run', 'POST', {
|
|
7182
|
+
laneId: selectedLaneId(),
|
|
7183
|
+
autoHealOnAlert: asBoolean($watchdogAutoHeal.value, true),
|
|
7184
|
+
retryFailedLimit: Number($autoHealRetryLimit.value || '15'),
|
|
7185
|
+
maxRunbooks: Number($autoHealRunbooks.value || '2'),
|
|
7186
|
+
});
|
|
7187
|
+
renderOutput(result);
|
|
7188
|
+
setMessage('Watchdog cycle completed', false);
|
|
7189
|
+
if (result.controlAfter) {
|
|
7190
|
+
renderControlRoom(result.controlAfter);
|
|
7191
|
+
}
|
|
7192
|
+
await refreshJobsAndLanes();
|
|
7193
|
+
await loadAlertsUi();
|
|
7194
|
+
}
|
|
7195
|
+
|
|
6811
7196
|
document.getElementById('open-url-btn').addEventListener('click', async function () {
|
|
6812
7197
|
try { await openUrl($urlInput.value); } catch (error) { setMessage(String(error), true); }
|
|
6813
7198
|
});
|
|
@@ -7018,6 +7403,15 @@ https://developer.android.com</textarea>
|
|
|
7018
7403
|
document.getElementById('auto-heal-btn').addEventListener('click', async function () {
|
|
7019
7404
|
try { await runAutoHealUi(); } catch (error) { setMessage(String(error), true); }
|
|
7020
7405
|
});
|
|
7406
|
+
document.getElementById('auto-heal-preview-btn').addEventListener('click', async function () {
|
|
7407
|
+
try { await previewAutoHealUi(); } catch (error) { setMessage(String(error), true); }
|
|
7408
|
+
});
|
|
7409
|
+
document.getElementById('ops-stabilize-btn').addEventListener('click', async function () {
|
|
7410
|
+
try { await runOpsStabilizeUi(); } catch (error) { setMessage(String(error), true); }
|
|
7411
|
+
});
|
|
7412
|
+
document.getElementById('watchdog-run-btn').addEventListener('click', async function () {
|
|
7413
|
+
try { await runWatchdogUi(); } catch (error) { setMessage(String(error), true); }
|
|
7414
|
+
});
|
|
7021
7415
|
document.getElementById('queue-snapshot-save-btn').addEventListener('click', async function () {
|
|
7022
7416
|
try { await saveQueueSnapshotUi(); } catch (error) { setMessage(String(error), true); }
|
|
7023
7417
|
});
|
|
@@ -7027,9 +7421,15 @@ https://developer.android.com</textarea>
|
|
|
7027
7421
|
document.getElementById('queue-snapshot-apply-btn').addEventListener('click', async function () {
|
|
7028
7422
|
try { await applyQueueSnapshotUi(); } catch (error) { setMessage(String(error), true); }
|
|
7029
7423
|
});
|
|
7424
|
+
document.getElementById('queue-snapshot-diff-btn').addEventListener('click', async function () {
|
|
7425
|
+
try { await diffQueueSnapshotUi(); } catch (error) { setMessage(String(error), true); }
|
|
7426
|
+
});
|
|
7030
7427
|
document.getElementById('runbook-campaign-btn').addEventListener('click', async function () {
|
|
7031
7428
|
try { await runRunbookCampaignUi(); } catch (error) { setMessage(String(error), true); }
|
|
7032
7429
|
});
|
|
7430
|
+
document.getElementById('runbook-campaign-plan-btn').addEventListener('click', async function () {
|
|
7431
|
+
try { await planRunbookCampaignUi(); } catch (error) { setMessage(String(error), true); }
|
|
7432
|
+
});
|
|
7033
7433
|
document.getElementById('alert-seed-btn').addEventListener('click', async function () {
|
|
7034
7434
|
try { await seedAlertRulesUi(); } catch (error) { setMessage(String(error), true); }
|
|
7035
7435
|
});
|
|
@@ -7077,6 +7477,7 @@ https://developer.android.com</textarea>
|
|
|
7077
7477
|
await loadHeatmapUi();
|
|
7078
7478
|
await loadPolicyUi();
|
|
7079
7479
|
await loadOpsBoardUi();
|
|
7480
|
+
await loadControlRoomUi();
|
|
7080
7481
|
await loadQueueBoardUi();
|
|
7081
7482
|
await listSchedulesUi();
|
|
7082
7483
|
await loadAlertsUi();
|
|
@@ -7096,6 +7497,8 @@ https://developer.android.com</textarea>
|
|
|
7096
7497
|
renderRecorderSessions(recorderPayload);
|
|
7097
7498
|
const opsBoardPayload = await api('/api/ops/board');
|
|
7098
7499
|
renderOpsBoard(opsBoardPayload);
|
|
7500
|
+
const controlRoomPayload = await api('/api/ops/control-room');
|
|
7501
|
+
renderControlRoom(controlRoomPayload);
|
|
7099
7502
|
const schedulesPayload = await api('/api/schedules');
|
|
7100
7503
|
renderSchedules(schedulesPayload);
|
|
7101
7504
|
const queueBoardPayload = await api('/api/board/queue');
|