Add instance heartbeat settings sidebar
This commit is contained in:
@@ -8,9 +8,11 @@ import {
|
||||
createAgentKeySchema,
|
||||
createAgentHireSchema,
|
||||
createAgentSchema,
|
||||
deriveAgentUrlKey,
|
||||
isUuidLike,
|
||||
resetAgentSessionSchema,
|
||||
testAdapterEnvironmentSchema,
|
||||
type InstanceSchedulerHeartbeatAgent,
|
||||
updateAgentPermissionsSchema,
|
||||
updateAgentInstructionsPathSchema,
|
||||
wakeAgentSchema,
|
||||
@@ -202,6 +204,21 @@ export function agentRoutes(db: Db) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseNumberLike(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value !== "string") return null;
|
||||
const parsed = Number(value.trim());
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseSchedulerHeartbeatPolicy(runtimeConfig: unknown) {
|
||||
const heartbeat = asRecord(asRecord(runtimeConfig)?.heartbeat) ?? {};
|
||||
return {
|
||||
enabled: parseBooleanLike(heartbeat.enabled) ?? true,
|
||||
intervalSec: Math.max(0, parseNumberLike(heartbeat.intervalSec) ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function generateEd25519PrivateKeyPem(): string {
|
||||
const { privateKey } = generateKeyPairSync("ed25519");
|
||||
return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
||||
@@ -454,6 +471,81 @@ export function agentRoutes(db: Db) {
|
||||
res.json(result.map((agent) => redactForRestrictedAgentView(agent)));
|
||||
});
|
||||
|
||||
router.get("/instance/scheduler-heartbeats", async (req, res) => {
|
||||
assertBoard(req);
|
||||
|
||||
const accessConditions = [];
|
||||
if (req.actor.source !== "local_implicit" && !req.actor.isInstanceAdmin) {
|
||||
const allowedCompanyIds = req.actor.companyIds ?? [];
|
||||
if (allowedCompanyIds.length === 0) {
|
||||
res.json([]);
|
||||
return;
|
||||
}
|
||||
accessConditions.push(inArray(agentsTable.companyId, allowedCompanyIds));
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: agentsTable.id,
|
||||
companyId: agentsTable.companyId,
|
||||
agentName: agentsTable.name,
|
||||
role: agentsTable.role,
|
||||
title: agentsTable.title,
|
||||
status: agentsTable.status,
|
||||
adapterType: agentsTable.adapterType,
|
||||
runtimeConfig: agentsTable.runtimeConfig,
|
||||
lastHeartbeatAt: agentsTable.lastHeartbeatAt,
|
||||
companyName: companies.name,
|
||||
companyIssuePrefix: companies.issuePrefix,
|
||||
})
|
||||
.from(agentsTable)
|
||||
.innerJoin(companies, eq(agentsTable.companyId, companies.id))
|
||||
.where(accessConditions.length > 0 ? and(...accessConditions) : undefined)
|
||||
.orderBy(companies.name, agentsTable.name);
|
||||
|
||||
const items: InstanceSchedulerHeartbeatAgent[] = rows
|
||||
.map((row) => {
|
||||
const policy = parseSchedulerHeartbeatPolicy(row.runtimeConfig);
|
||||
const statusEligible =
|
||||
row.status !== "paused" &&
|
||||
row.status !== "terminated" &&
|
||||
row.status !== "pending_approval";
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
companyId: row.companyId,
|
||||
companyName: row.companyName,
|
||||
companyIssuePrefix: row.companyIssuePrefix,
|
||||
agentName: row.agentName,
|
||||
agentUrlKey: deriveAgentUrlKey(row.agentName, row.id),
|
||||
role: row.role as InstanceSchedulerHeartbeatAgent["role"],
|
||||
title: row.title,
|
||||
status: row.status as InstanceSchedulerHeartbeatAgent["status"],
|
||||
adapterType: row.adapterType,
|
||||
intervalSec: policy.intervalSec,
|
||||
heartbeatEnabled: policy.enabled,
|
||||
schedulerActive: statusEligible && policy.enabled && policy.intervalSec > 0,
|
||||
lastHeartbeatAt: row.lastHeartbeatAt,
|
||||
};
|
||||
})
|
||||
.filter((item) =>
|
||||
item.intervalSec > 0 &&
|
||||
item.status !== "paused" &&
|
||||
item.status !== "terminated" &&
|
||||
item.status !== "pending_approval",
|
||||
)
|
||||
.sort((left, right) => {
|
||||
if (left.schedulerActive !== right.schedulerActive) {
|
||||
return left.schedulerActive ? -1 : 1;
|
||||
}
|
||||
const companyOrder = left.companyName.localeCompare(right.companyName);
|
||||
if (companyOrder !== 0) return companyOrder;
|
||||
return left.agentName.localeCompare(right.agentName);
|
||||
});
|
||||
|
||||
res.json(items);
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/org", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
|
||||
Reference in New Issue
Block a user