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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ const weather: CustomToolRecord = {
|
||||
status: "ok",
|
||||
last_error: "",
|
||||
last_checked_at: null,
|
||||
updated_at: null,
|
||||
kind: "http",
|
||||
config: {
|
||||
method: "GET",
|
||||
|
||||
@@ -12,6 +12,7 @@ function skill(overrides: Partial<SkillRecord> & { slug: string }): SkillRecord
|
||||
body: `${overrides.slug} body`,
|
||||
all_bots: false,
|
||||
enabled: true,
|
||||
updated_at: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import checkedAgo from "./checked-ago";
|
||||
|
||||
type Params = {
|
||||
at: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* When an extension's settings last changed, for a row's subtitle:
|
||||
* "Updated 5m ago". Nothing when it's never been stamped.
|
||||
*/
|
||||
export default function updatedAgo({ at }: Params) {
|
||||
return at ? `Updated ${checkedAgo({ at })}` : undefined;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import SettingsListRow from "../../(partials)/settings-list-row";
|
||||
import SettingsRowAction from "../../(partials)/settings-row-action";
|
||||
import checkedAgo from "../../(utils)/checked-ago";
|
||||
import extensionAccessLabel from "../../(utils)/extension-access-label";
|
||||
import updatedAgo from "../../(utils)/updated-ago";
|
||||
import McpServerModal from "./mcp-server-modal";
|
||||
|
||||
type Props = {
|
||||
@@ -63,6 +64,7 @@ export default function McpServerRow({ server, targets, onChanged, onError }: Pr
|
||||
subtitle={[
|
||||
McpTransports.find((t) => t.value == server.transport)?.title,
|
||||
extensionAccessLabel({ access: server.access }),
|
||||
updatedAgo({ at: server.updated_at }),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
|
||||
@@ -8,6 +8,7 @@ import ExtensionAccessModal from "../../(partials)/extension-access-modal";
|
||||
import SettingsListRow from "../../(partials)/settings-list-row";
|
||||
import SettingsRowAction from "../../(partials)/settings-row-action";
|
||||
import extensionAccessLabel from "../../(utils)/extension-access-label";
|
||||
import updatedAgo from "../../(utils)/updated-ago";
|
||||
import SkillModal from "./skill-modal";
|
||||
|
||||
type Props = {
|
||||
@@ -51,7 +52,12 @@ export default function SkillRow({ skill, targets, limits, onChanged, onError }:
|
||||
avatar={<IconTile Icon={BookOpen} />}
|
||||
title={skill.name}
|
||||
badge={skill.enabled ? undefined : "Off"}
|
||||
subtitle={[skill.slug, extensionAccessLabel({ access: skill.access }), skill.description]
|
||||
subtitle={[
|
||||
skill.slug,
|
||||
extensionAccessLabel({ access: skill.access }),
|
||||
updatedAgo({ at: skill.updated_at }),
|
||||
skill.description,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
>
|
||||
|
||||
@@ -11,6 +11,7 @@ import SettingsListRow from "../../(partials)/settings-list-row";
|
||||
import SettingsRowAction from "../../(partials)/settings-row-action";
|
||||
import checkedAgo from "../../(utils)/checked-ago";
|
||||
import extensionAccessLabel from "../../(utils)/extension-access-label";
|
||||
import updatedAgo from "../../(utils)/updated-ago";
|
||||
import CustomToolModal from "./custom-tool-modal";
|
||||
import CustomToolTryModal from "./custom-tool-try-modal";
|
||||
|
||||
@@ -72,6 +73,7 @@ export default function CustomToolRow({ tool, targets, onChanged, onRefreshed, o
|
||||
subtitle={[
|
||||
CustomToolKinds.find((k) => k.value == tool.kind)?.title,
|
||||
extensionAccessLabel({ access: tool.access }),
|
||||
updatedAgo({ at: tool.updated_at }),
|
||||
tool.description,
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -752,6 +752,10 @@ export type McpServerRecord = {
|
||||
status: McpServerStatus;
|
||||
last_error: string;
|
||||
last_synced_at: number | null;
|
||||
/**
|
||||
* When its settings last changed (an edit or who gets it), not a run or check.
|
||||
*/
|
||||
updated_at: number | null;
|
||||
};
|
||||
|
||||
export type CustomToolHttpConfig = {
|
||||
@@ -785,6 +789,10 @@ export type CustomToolRecord = {
|
||||
status: CustomToolStatus;
|
||||
last_error: string;
|
||||
last_checked_at: number | null;
|
||||
/**
|
||||
* When its settings last changed (an edit or who gets it), not a run or check.
|
||||
*/
|
||||
updated_at: number | null;
|
||||
} & (
|
||||
| { kind: "http"; config: CustomToolHttpConfig }
|
||||
| { kind: "script"; config: CustomToolScriptConfig }
|
||||
@@ -802,6 +810,10 @@ export type SkillRecord = {
|
||||
body: string;
|
||||
all_bots: boolean;
|
||||
enabled: boolean;
|
||||
/**
|
||||
* When its settings last changed (an edit or who gets it), not a run or check.
|
||||
*/
|
||||
updated_at: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -943,6 +955,7 @@ export type PublicCustomTool = {
|
||||
status: CustomToolStatus;
|
||||
last_error: string;
|
||||
last_checked_at: number | null;
|
||||
updated_at: number | null;
|
||||
access: ExtensionAccess;
|
||||
parameters?: Record<string, unknown>;
|
||||
config?: CustomToolHttpConfig | CustomToolScriptConfig | CustomToolPromptConfig;
|
||||
|
||||
@@ -10,6 +10,7 @@ const current: SkillRecord = {
|
||||
body: "1. Search the client's rankings.\n2. Write the report.\n3. Attach it.",
|
||||
all_bots: false,
|
||||
enabled: true,
|
||||
updated_at: null,
|
||||
};
|
||||
|
||||
describe("describeSkillProposal", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ const current: SkillRecord = {
|
||||
body: "1. Install Baileys.\n2. Pair.",
|
||||
all_bots: false,
|
||||
enabled: true,
|
||||
updated_at: null,
|
||||
};
|
||||
|
||||
describe("skillProposalChanges", () => {
|
||||
|
||||
Reference in New Issue
Block a user