Show when each extension was last updated

MCP, skill and tool rows in Settings say "Updated 5m ago" beside how
they're reached and who gets them. updated_at now means a change to an
extension's settings (an edit, or any Access save): runs, connections and
MCP syncs keep their own last_checked_at / last_synced_at and no longer
touch it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-26 08:07:44 +01:00
co-authored by Claude Opus 5.5
parent b9008b22cb
commit 2df1bf83f4
18 changed files with 62 additions and 16 deletions
@@ -37,6 +37,7 @@ export default async function grabToolsOverview({ user_id }: Params): Promise<Cu
status: tool.status,
last_error: tool.last_error,
last_checked_at: tool.last_checked_at,
updated_at: tool.updated_at,
access: { all_bots: tool.all_bots, ...access(tool.id) },
...(targets.can_edit ? { parameters: tool.parameters, config: tool.config } : {}),
};
@@ -24,6 +24,7 @@ export default function parseCustomToolRow({ row }: Params): CustomToolRecord {
status: (row.status || "untested") as CustomToolStatus,
last_error: row.last_error || "",
last_checked_at: typeof row.last_checked_at == "number" ? row.last_checked_at : null,
updated_at: typeof row.updated_at == "number" ? row.updated_at : null,
kind: row.kind || "prompt",
config: parseJsonColumn<Record<string, unknown>>({ text: row.config, fallback: {} }),
} as CustomToolRecord;
@@ -29,5 +29,6 @@ export default function parseMcpServerRow({ row }: Params): McpServerRecord {
status: (row.status || "untested") as McpServerStatus,
last_error: row.last_error || "",
last_synced_at: typeof row.last_synced_at == "number" ? row.last_synced_at : null,
updated_at: typeof row.updated_at == "number" ? row.updated_at : null,
};
}
@@ -17,5 +17,6 @@ export default function parseSkillRow({ row }: Params): SkillRecord {
body: row.body || "",
all_bots: row.all_bots == 1,
enabled: row.enabled != 0,
updated_at: typeof row.updated_at == "number" ? row.updated_at : null,
};
}
@@ -11,13 +11,12 @@ type Params = {
/**
* Saves how a custom tool's run went (`status`, `last_error`,
* `last_checked_at`), for its row in Settings, under Tools. Never throws:
* a run doesn't fail over its bookkeeping.
* a run doesn't fail over its bookkeeping. `updated_at` is left alone: it
* says when the tool's settings last changed.
*/
export default async function recordCustomToolRun({ custom_tool_id, error }: Params) {
const now = Date.now();
await BunSQLite.sql({
sql: `UPDATE custom_tools SET status = ?, last_error = ?, last_checked_at = ?, updated_at = ? WHERE id = ?`,
values: [error ? "error" : "ok", (error || "").slice(0, 4000), now, now, custom_tool_id],
sql: `UPDATE custom_tools SET status = ?, last_error = ?, last_checked_at = ? WHERE id = ?`,
values: [error ? "error" : "ok", (error || "").slice(0, 4000), Date.now(), custom_tool_id],
}).catch(() => {});
}
@@ -11,13 +11,12 @@ type Params = {
/**
* Saves whether a bot's run could connect to an MCP server, so its row in
* Settings, under MCPs, shows a server that stopped answering (or came
* back). Its tools listing is left as it was. Never throws.
* back). Its tools listing and `updated_at` (when its settings last
* changed) are left as they were. Never throws.
*/
export default async function recordMcpServerConnection({ mcp_server_id, error }: Params) {
const now = Date.now();
await BunSQLite.sql({
sql: `UPDATE mcp_servers SET status = ?, last_error = ?, last_synced_at = ?, updated_at = ? WHERE id = ?`,
values: [error ? "error" : "ok", (error || "").slice(0, 4000), now, now, mcp_server_id],
sql: `UPDATE mcp_servers SET status = ?, last_error = ?, last_synced_at = ? WHERE id = ?`,
values: [error ? "error" : "ok", (error || "").slice(0, 4000), Date.now(), mcp_server_id],
}).catch(() => {});
}
@@ -21,7 +21,8 @@ type Params = {
* super admins and admins. An admin never sees the coordinator, so its
* attachment is kept as it is; only a super admin sets "every bot", which
* when turned on gives it to every bot (`attachToAllBots`) and every new
* one. Turning it off takes it from no one.
* one. Turning it off takes it from no one. Either way it counts as a
* change to the extension (`updated_at`).
*/
export default async function setExtensionAccess({ user_id, kind, id, bot_ids, workspace_ids, all_bots }: Params) {
const role = await assertCrewManager({ user_id });
@@ -63,5 +64,7 @@ export default async function setExtensionAccess({ user_id, kind, id, bot_ids, w
});
if (all_bots && found.payload?.[0]?.all_bots != 1) await attachToAllBots({ kind, id });
} else {
await BunSQLite.sql({ sql: `UPDATE ${table} SET updated_at = ? WHERE id = ?`, values: [now, id] });
}
}
@@ -37,8 +37,8 @@ export default async function syncMcpServer({ mcp_server_id, reinstall }: Params
const tools = await listMcpTools({ client: await open.client() });
await BunSQLite.sql({
sql: `UPDATE mcp_servers SET tools_cache = ?, status = 'ok', last_error = '', last_synced_at = ?, updated_at = ? WHERE id = ?`,
values: [JSON.stringify(tools), now, now, server.id],
sql: `UPDATE mcp_servers SET tools_cache = ?, status = 'ok', last_error = '', last_synced_at = ? WHERE id = ?`,
values: [JSON.stringify(tools), now, server.id],
});
return { ok: true as const, tools };
@@ -49,8 +49,8 @@ export default async function syncMcpServer({ mcp_server_id, reinstall }: Params
}).slice(0, 4000);
await BunSQLite.sql({
sql: `UPDATE mcp_servers SET status = 'error', last_error = ?, last_synced_at = ?, updated_at = ? WHERE id = ?`,
values: [message, now, now, server.id],
sql: `UPDATE mcp_servers SET status = 'error', last_error = ?, last_synced_at = ? WHERE id = ?`,
values: [message, now, server.id],
});
return { ok: false as const, error: message };
@@ -39,6 +39,7 @@ export default async function testMcpDraft({
status: "untested",
last_error: "",
last_synced_at: null,
updated_at: null,
},
envs,
});