diff --git a/apps/dashboard/src/app/(dashboard)/agents/mcp/page.tsx b/apps/dashboard/src/app/(dashboard)/agents/mcp/page.tsx index 6cdbc1be4..940152ec9 100644 --- a/apps/dashboard/src/app/(dashboard)/agents/mcp/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/agents/mcp/page.tsx @@ -220,6 +220,7 @@ const tools = [ { group: "Workspace", items: [ + "get_workspace", "list_notifications", "list_private_locations", "list_audit_logs", diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/nav-actions.tsx b/apps/dashboard/src/app/(dashboard)/monitors/[id]/nav-actions.tsx index cfcdca253..e6a2b7767 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/nav-actions.tsx +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/nav-actions.tsx @@ -68,6 +68,14 @@ export function NavActions() { }), ); + const updateMonitorsMutation = useMutation( + trpc.monitor.updateMonitors.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: trpc.monitor.pathKey() }); + }, + }), + ); + const testHttpMutation = useMutation(trpc.checker.testHttp.mutationOptions()); const testTcpMutation = useMutation(trpc.checker.testTcp.mutationOptions()); const testDnsMutation = useMutation(trpc.checker.testDns.mutationOptions()); @@ -78,8 +86,28 @@ export function NavActions() { const curlCommand = monitor?.jobType === "http" ? buildCurlCommand(monitor) : null; + function setActive(active: boolean) { + const promise = updateMonitorsMutation.mutateAsync({ + ids: [Number.parseInt(id)], + active, + }); + toast.promise(promise, { + loading: active ? "Resuming monitor..." : "Pausing monitor...", + success: active ? "Monitor resumed" : "Monitor paused", + error: (error) => { + if (isTRPCClientError(error)) { + return error.message; + } + return active ? "Failed to resume monitor" : "Failed to pause monitor"; + }, + }); + } + + const hiddenAction = monitor?.active ? "resume" : "pause"; const actions = getActions({ edit: () => router.push(`/monitors/${id}/edit`), + pause: () => setActive(false), + resume: () => setActive(true), "copy-id": async () => { await navigator.clipboard.writeText(id); toast.success("Monitor ID copied to clipboard"); @@ -105,7 +133,11 @@ export function NavActions() { }, }); }, - }).filter((action) => action.id !== "copy-curl" || Boolean(curlCommand)); + }).filter( + (action) => + action.id !== hiddenAction && + (action.id !== "copy-curl" || Boolean(curlCommand)), + ); async function testAction() { if (monitor?.jobType === "http") { diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx b/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx index e058cf554..5eb9b9c5b 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx @@ -1,10 +1,9 @@ "use client"; -import { Pause, Play, Trend } from "@openstatus/icons"; +import { Pause, Trend } from "@openstatus/icons"; import { Button } from "@openstatus/ui/components/ui/button"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { endOfDay } from "date-fns"; -import NextLink from "next/link"; import { useParams } from "next/navigation"; import { useQueryStates } from "nuqs"; import React, { useEffect } from "react"; @@ -65,15 +64,6 @@ export function Client() { trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }), ); const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); - const queryClient = useQueryClient(); - const resumeMonitor = useMutation( - trpc.monitor.updateMonitors.mutationOptions({ - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: trpc.monitor.pathKey() }); - }, - onError: () => toast.error("Failed to resume the monitor"), - }), - ); const isBlockedPeriod = workspace?.plan === "free" && isPaidPeriod(period); // snap free workspaces back synchronously so charts never fire (and the server @@ -83,8 +73,8 @@ export function Client() { useEffect(() => { if (!isBlockedPeriod) return; const timeout = setTimeout(() => { - toast.error("30 and 90 day windows require a paid plan", { - description: "Showing the last 14 days instead.", + toast.error("Windows beyond 7 days require a paid plan", { + description: "Showing the last 7 days instead.", }); setSearchParams({ period: FREE_MAX_PERIOD }); }, 0); @@ -92,7 +82,7 @@ export function Client() { }, [isBlockedPeriod, setSearchParams]); const selectedRegions = regions ?? undefined; - const fromDate = periodToFromDate[effectivePeriod]; + const fromDate = periodToFromDate(effectivePeriod); const toDate = endOfDay(new Date()); const regionTimelineQuery = { @@ -138,7 +128,6 @@ export function Client() { const filtersRegions = workspace?.plan !== "free" && !!selectedRegions?.length; const filteredOut = noData && filtersRegions; - const maxPeriod = workspace?.plan === "free" ? FREE_MAX_PERIOD : "90d"; const configuredRegions = React.useMemo( () => [ @@ -214,21 +203,6 @@ export function Client() { This monitor is paused - - - - ) : noData ? ( @@ -309,6 +283,7 @@ export function Client() { degradedAfter={monitor.degradedAfter} type={monitor.jobType as "http" | "tcp" | "dns" | "icmp" | "grpc"} period={effectivePeriod} + interval={interval} regions={selectedRegions} /> )} diff --git a/apps/dashboard/src/components/chart/chart-area-latency.tsx b/apps/dashboard/src/components/chart/chart-area-latency.tsx index c76fe07a0..944391b6a 100644 --- a/apps/dashboard/src/components/chart/chart-area-latency.tsx +++ b/apps/dashboard/src/components/chart/chart-area-latency.tsx @@ -21,11 +21,13 @@ import { } from "recharts"; import { + type INTERVALS, type PERCENTILES, type PERIODS, mapLatency, } from "@/data/metrics.client"; import { periodToFromDate } from "@/data/metrics.client"; +import { formatAxisTick, formatDateTime } from "@/lib/formatter"; import { useTRPC } from "@/lib/trpc/client"; import { ChartTooltipNumber } from "./chart-tooltip-number"; @@ -45,17 +47,19 @@ export function ChartAreaLatency({ period, type, percentile, + interval, regions, }: { monitorId: string; degradedAfter: number | null; percentile: (typeof PERCENTILES)[number]; period: (typeof PERIODS)[number]; + interval: (typeof INTERVALS)[number]; type: "http" | "tcp" | "dns" | "icmp" | "grpc"; regions: string[] | undefined; }) { const trpc = useTRPC(); - const fromDate = periodToFromDate[period]; + const fromDate = periodToFromDate(period); const toDate = endOfDay(new Date()); const { data: latency } = useQuery( @@ -63,6 +67,7 @@ export function ChartAreaLatency({ monitorId, period, type, + interval, regions, fromDate: fromDate.toISOString(), toDate: toDate.toISOString(), @@ -80,13 +85,18 @@ export function ChartAreaLatency({ tickLine={false} axisLine={false} tickMargin={8} - // tickFormatter={(value) => value.slice(0, 3)} + tickFormatter={(value) => + formatAxisTick(new Date(value), period, interval) + } /> + formatDateTime(new Date(payload[0]?.payload.timestamp)) + } formatter={(value, name) => ( ) : null} + degradedAfter && degradedAfter <= dataMax * 2 + ? degradedAfter + : dataMax, + ]} tickLine={false} axisLine={false} tickMargin={8} diff --git a/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx b/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx index 3902e2682..fdc42beee 100644 --- a/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx +++ b/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx @@ -9,6 +9,7 @@ import { ChartTooltipContent, } from "@openstatus/ui/components/ui/chart"; import { useQuery } from "@tanstack/react-query"; +import { endOfDay } from "date-fns"; import { Area, AreaChart, @@ -24,7 +25,9 @@ import { type PERCENTILES, type PERIODS, mapTimingPhases, + periodToFromDate, } from "@/data/metrics.client"; +import { formatAxisTick, formatDateTime } from "@/lib/formatter"; import { useTRPC } from "@/lib/trpc/client"; import { @@ -75,6 +78,8 @@ export function ChartAreaTimingPhases({ type: "http"; }) { const trpc = useTRPC(); + const fromDate = periodToFromDate(period); + const toDate = endOfDay(new Date()); const { data: timingPhases } = useQuery( trpc.tinybird.metricsTimingPhases.queryOptions({ @@ -83,6 +88,8 @@ export function ChartAreaTimingPhases({ type, interval, regions, + fromDate: fromDate.toISOString(), + toDate: toDate.toISOString(), }), ); @@ -99,13 +106,18 @@ export function ChartAreaTimingPhases({ tickLine={false} axisLine={false} tickMargin={8} - // tickFormatter={(value) => value.slice(0, 3)} + tickFormatter={(value) => + formatAxisTick(new Date(value), period, interval) + } /> + formatDateTime(new Date(payload[0]?.payload.timestamp)) + } formatter={(value, name, item, index) => { if (index !== 4) { return ( @@ -196,7 +208,14 @@ export function ChartAreaTimingPhases({ ) : null} + degradedAfter && degradedAfter <= dataMax * 2 + ? degradedAfter + : dataMax, + ]} tickLine={false} axisLine={false} tickMargin={8} diff --git a/apps/dashboard/src/components/chart/chart-bar-uptime-light.tsx b/apps/dashboard/src/components/chart/chart-bar-uptime-light.tsx index 86a96c9e9..51fa98764 100644 --- a/apps/dashboard/src/components/chart/chart-bar-uptime-light.tsx +++ b/apps/dashboard/src/components/chart/chart-bar-uptime-light.tsx @@ -13,6 +13,7 @@ import { Bar, BarChart, XAxis } from "recharts"; import { TableCellEmpty } from "@/components/data-table/table-cell-empty"; import { mapUptime } from "@/data/metrics.client"; +import { formatDateTime } from "@/lib/formatter"; import { useTRPC } from "@/lib/trpc/client"; // import { startOfDay, subDays } from "date-fns"; @@ -70,7 +71,14 @@ export function ChartBarUptimeLight({ cursor={false} allowEscapeViewBox={{ x: false, y: true }} wrapperStyle={{ zIndex: 1 }} - content={} + content={ + + formatDateTime(new Date(payload[0]?.payload.interval)) + } + /> + } /> diff --git a/apps/dashboard/src/components/chart/chart-bar-uptime.tsx b/apps/dashboard/src/components/chart/chart-bar-uptime.tsx index a0d726a80..47a05542f 100644 --- a/apps/dashboard/src/components/chart/chart-bar-uptime.tsx +++ b/apps/dashboard/src/components/chart/chart-bar-uptime.tsx @@ -19,6 +19,7 @@ import { periodToFromDate, periodToInterval, } from "@/data/metrics.client"; +import { formatAxisTick, formatDateTime } from "@/lib/formatter"; import { useTRPC } from "@/lib/trpc/client"; const chartConfig = { @@ -49,7 +50,7 @@ export function ChartBarUptime({ }) { const isMobile = useIsMobile(); const trpc = useTRPC(); - const fromDate = periodToFromDate[period]; + const fromDate = periodToFromDate(period); const toDate = endOfDay(new Date()); const interval = periodToInterval[period]; @@ -78,7 +79,14 @@ export function ChartBarUptime({ } + content={ + + formatDateTime(new Date(payload[0]?.payload.interval)) + } + /> + } /> @@ -96,6 +104,9 @@ export function ChartBarUptime({ tickMargin={8} minTickGap={10} axisLine={false} + tickFormatter={(value) => + formatAxisTick(new Date(value), period, interval) + } /> } /> diff --git a/apps/dashboard/src/components/chat/tool-renderers/get-workspace.tsx b/apps/dashboard/src/components/chat/tool-renderers/get-workspace.tsx new file mode 100644 index 000000000..69eca0087 --- /dev/null +++ b/apps/dashboard/src/components/chat/tool-renderers/get-workspace.tsx @@ -0,0 +1,58 @@ +import type { AgentToolOutput } from "@openstatus/services/agent-tools"; + +import { TableCellDate } from "@/components/data-table/table-cell-date"; +import { TableCellNumber } from "@/components/data-table/table-cell-number"; +import { TableCellText } from "@/components/data-table/table-cell-text"; + +import type { DetailsTableData } from "./details-table"; + +type Output = AgentToolOutput<"get_workspace">; + +export function getWorkspaceDetails(output: Output): DetailsTableData { + const { usage } = output; + return { + sections: [ + { + title: "Workspace", + rows: [ + { label: "Name", value: }, + { label: "Slug", value: }, + { label: "Plan", value: }, + { + label: "Trial ends", + value: ( + + ), + }, + ], + }, + { + title: "Usage", + rows: [ + { + label: "Monitors", + value: , + }, + { + label: "Status pages", + value: , + }, + { + label: "Page components", + value: , + }, + { + label: "Notification channels", + value: , + }, + { + label: "Status reports", + value: , + }, + ], + }, + ], + }; +} diff --git a/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx b/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx index c4eeb8073..cfa8a3ba1 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx @@ -100,7 +100,7 @@ describe("listIncidentsTable", () => { expect(table.empty).toBe("No incidents."); }); - it("marks closed incidents and falls back for a missing commander", () => { + it("marks closed incidents", () => { const table = listIncidentsTable( asFixture({ items: [ @@ -127,14 +127,15 @@ describe("listIncidentsTable", () => { "title", "severity", "status", - "commander", "id", ]); const [closed, open] = table.rows; - expect(text(closed.cells.status)).toBe("resolved · closed"); - expect(text(closed.cells.commander)).toBe("Unassigned"); - expect(text(open.cells.status)).toBe("open"); - expect(text(open.cells.commander)).toBe("Ada"); + expect(renderToStaticMarkup(<>{closed.cells.title})).toContain( + 'href="/incidents/1"', + ); + expect(text(closed.cells.severity)).toBe("Major"); + expect(text(closed.cells.status)).toBe("Resolved · closed"); + expect(text(open.cells.status)).toBe("Open"); expect(text(open.cells.id)).toBe("2"); }); }); @@ -146,7 +147,8 @@ describe("getIncidentDetails", () => { id: 5, title: "Checkout down", severity: "critical", - status: "open", + status: "resolved", + closed: true, commander: null, summary: null, startedAt: "2024-01-01T00:00:00Z", @@ -172,6 +174,7 @@ describe("getIncidentDetails", () => { ]); const value = (label: string) => text(overview.rows.find((r) => r.label === label)?.value); + expect(value("Status")).toBe("Resolved · closed"); expect(value("Commander")).toBe("Unassigned"); expect(value("Status report")).toBe("Payments degraded"); }); diff --git a/apps/dashboard/src/components/chat/tool-renderers/incidents.tsx b/apps/dashboard/src/components/chat/tool-renderers/incidents.tsx index ab72a9660..c9fdecd99 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/incidents.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/incidents.tsx @@ -6,15 +6,21 @@ import type { import type { ChangeRow } from "@/components/common/changes-table"; import { EntityRef } from "@/components/common/entity-ref/entity-ref"; import { TableCellDate } from "@/components/data-table/table-cell-date"; +import { TableCellLink } from "@/components/data-table/table-cell-link"; import { TableCellNumber } from "@/components/data-table/table-cell-number"; import { TableCellText } from "@/components/data-table/table-cell-text"; +import { + IncidentSeverityBadge, + IncidentStatusBadge, +} from "@/components/incidents/incident-badge"; +import { entityKinds } from "@/data/entity-refs.client"; import type { DetailsTableData } from "./details-table"; import type { ResultTableData } from "./result-table"; export function listIncidentsTable( output: AgentToolOutput<"list_incidents">, -): ResultTableData<"title" | "severity" | "status" | "commander" | "id"> { +): ResultTableData<"title" | "severity" | "status" | "id"> { const items = output?.items ?? []; return { empty: "No incidents.", @@ -22,20 +28,19 @@ export function listIncidentsTable( { key: "title", header: "Title" }, { key: "severity", header: "Severity" }, { key: "status", header: "Status" }, - { key: "commander", header: "Commander" }, { key: "id", header: "ID" }, ], rows: items.map((i) => ({ id: i.id, cells: { - title: , - severity: , - status: ( - - ), - commander: ( - + title: ( + ), + severity: , + status: , id: , }, })), @@ -50,12 +55,28 @@ export function getIncidentDetails( { rows: [ { label: "ID", value: }, - { label: "Title", value: }, + { + label: "Title", + value: ( + + ), + }, { label: "Severity", - value: , + value: , + }, + { + label: "Status", + value: ( + + ), }, - { label: "Status", value: }, { label: "Commander", value: ( diff --git a/apps/dashboard/src/components/chat/tool-renderers/index.tsx b/apps/dashboard/src/components/chat/tool-renderers/index.tsx index 6458574fa..33af64495 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/index.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/index.tsx @@ -20,6 +20,7 @@ import { getMonitorStatusTable } from "./get-monitor-status"; import { getMonitorSummaryDetails } from "./get-monitor-summary"; import { getResponseLogDetails } from "./get-response-log"; import { getStatusReportDetails } from "./get-status-report"; +import { getWorkspaceDetails } from "./get-workspace"; import { declareIncidentChanges, getIncidentDetails, @@ -318,6 +319,12 @@ export const toolRenderers: ToolRendererRegistry = { ), summary: (o) => (o.id ? `log ${o.id}` : undefined), }, + get_workspace: { + renderResult: ({ output }) => ( + + ), + summary: (o) => `${o.slug} · ${o.plan}`, + }, list_audit_logs: { renderResult: ({ output }) => ( diff --git a/apps/dashboard/src/components/controls-search/dropdown-interval.tsx b/apps/dashboard/src/components/controls-search/dropdown-interval.tsx index 48f938109..4d391e066 100644 --- a/apps/dashboard/src/components/controls-search/dropdown-interval.tsx +++ b/apps/dashboard/src/components/controls-search/dropdown-interval.tsx @@ -17,7 +17,12 @@ import { } from "nuqs"; import { useEffect } from "react"; -import { INTERVALS, PERIODS, periodToMinInterval } from "@/data/metrics.client"; +import { + INTERVALS, + PERIODS, + periodToMaxInterval, + periodToMinInterval, +} from "@/data/metrics.client"; const MAPPING = { 5: "5 minutes", @@ -38,15 +43,18 @@ export function DropdownInterval() { const [period] = useQueryState("period", parsePeriod); const min = periodToMinInterval[period]; - const options = INTERVALS.filter((item) => item >= min); + const max = periodToMaxInterval[period]; + const options = INTERVALS.filter((item) => item >= min && item <= max); + const selected = Math.min( + Math.max(interval, min), + max, + ) as (typeof INTERVALS)[number]; - // snap a now-too-fine selection up to the period's floor so the dropdown, - // URL, and charts agree (e.g. 5min carried over into the 90d view) + // snap an out-of-range selection into the period's bounds so the dropdown, + // URL, and charts agree (e.g. 5min carried into 90d, or 1d carried into 1d) useEffect(() => { - if (interval < min) setInterval(min); - }, [interval, min, setInterval]); - - const selected = Math.max(interval, min) as (typeof INTERVALS)[number]; + if (interval !== selected) setInterval(selected); + }, [interval, selected, setInterval]); return ( { @@ -249,39 +259,46 @@ export function FloatingButton({ } }} > - + {BAR_TYPE.map((v) => ( - - {v} + + {LABELS.type[v]} ))}
- +
diff --git a/apps/web/src/content/pages/docs/reference/mcp-server.mdx b/apps/web/src/content/pages/docs/reference/mcp-server.mdx index c0e3600a1..e1d240f0f 100644 --- a/apps/web/src/content/pages/docs/reference/mcp-server.mdx +++ b/apps/web/src/content/pages/docs/reference/mcp-server.mdx @@ -62,7 +62,7 @@ For AI agents that should only observe state — health summaries, paging on-cal ## Tools -The server exposes 39 tools (37 on plans without the `audit-log` feature), grouped by resource: 35 scoped to the workspace tied to your credential, plus 4 public content tools that read openstatus.dev pages on any credential and any scope (see [Content](#content)). Mutations write to the audit log with `actor_type = "mcp"` (see [Audit log](#audit-log)). +The server exposes 40 tools (38 on plans without the `audit-log` feature), grouped by resource: 36 scoped to the workspace tied to your credential, plus 4 public content tools that read openstatus.dev pages on any credential and any scope (see [Content](#content)). Mutations write to the audit log with `actor_type = "mcp"` (see [Audit log](#audit-log)). ### Pages @@ -134,6 +134,12 @@ These tools act on declared incidents, not on monitor downtime, and are internal |--------------------------|------|---------| | `list_private_locations` | read | List the workspace's private locations with their name, status (`active` / `error`), metadata, and `lastSeenAt`. Agent tokens are never exposed. | +### Workspace + +| Tool | Type | Purpose | +|-----------------|------|---------| +| `get_workspace` | read | The workspace's name, slug, plan, trial end, and usage counts (monitors, status pages, components, notification channels, status reports). Plan limits and billing identifiers are never exposed. | + ### Audit Registered only when the workspace plan includes the `audit-log` feature — otherwise these tools are absent from `tools/list`. diff --git a/packages/api/src/router/tinybird/index.ts b/packages/api/src/router/tinybird/index.ts index e9928a4f2..7d5ccc1fb 100644 --- a/packages/api/src/router/tinybird/index.ts +++ b/packages/api/src/router/tinybird/index.ts @@ -18,12 +18,12 @@ const types = ["http", "tcp", "dns", "icmp", "grpc"] as const; type Period = (typeof periods)[number]; type Type = (typeof types)[number]; -// 30d/90d windows are paid-only; free workspaces are blocked server-side. +// Free workspaces only get up to 7d; longer windows are blocked server-side. function assertPaidPeriod(plan: string, period: Period) { - if (plan === "free" && (period === "30d" || period === "90d")) { + if (plan === "free" && period !== "1d" && period !== "7d") { throw new TRPCError({ code: "FORBIDDEN", - message: "Upgrade to a paid plan to access 30d and 90d data", + message: "Upgrade to a paid plan to access more than 7 days of data", }); } } @@ -40,9 +40,37 @@ const periodToMinInterval: Record = { "90d": 1440, }; +// Coarsest useful resolution per period — mirrors the dashboard's periodToMaxInterval. +const periodToMaxInterval: Record = { + "1d": 240, + "7d": 1440, + "14d": 1440, + "30d": 1440, + "90d": 1440, +}; + function clampInterval(period: Period, interval?: number) { if (interval == null) return interval; - return Math.max(interval, periodToMinInterval[period]); + return Math.min( + Math.max(interval, periodToMinInterval[period]), + periodToMaxInterval[period], + ); +} + +// latency pipes read fixed-window MVs (14d → 30d, 1d/7d → 14d, dns 1d → 7d) +// and ignore the dates, so trim to the requested window here +function trimToWindow( + rows: T[], + fromDate?: string, + toDate?: string, +) { + const from = fromDate ? new Date(fromDate).getTime() : undefined; + const to = toDate ? new Date(toDate).getTime() : undefined; + return rows.filter( + (row) => + (from === undefined || row.timestamp >= from) && + (to === undefined || row.timestamp <= to), + ); } // NEW: workspace-level counters helper @@ -563,8 +591,9 @@ export const tinybirdRouter = createTRPCRouter({ period: z.enum(periods), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), type: z.enum(types).prefault("http"), - fromDate: z.string().optional(), - toDate: z.string().optional(), + interval: z.int().optional(), + fromDate: z.iso.datetime().optional(), + toDate: z.iso.datetime().optional(), }), ) .query(async (opts) => { @@ -590,11 +619,20 @@ export const tinybirdRouter = createTRPCRouter({ assertPaidPeriod(opts.ctx.workspace.plan, opts.input.period); + opts.input.interval = clampInterval( + opts.input.period, + opts.input.interval, + ); + const procedure = getMetricsLatencyProcedure( opts.input.period, opts.input.type, ); - return await procedure(opts.input); + const result = await procedure(opts.input); + return { + ...result, + data: trimToWindow(result.data, opts.input.fromDate, opts.input.toDate), + }; }), metricsTimingPhases: protectedProcedure @@ -605,6 +643,8 @@ export const tinybirdRouter = createTRPCRouter({ interval: z.int().optional(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), type: z.literal("http"), + fromDate: z.iso.datetime().optional(), + toDate: z.iso.datetime().optional(), }), ) .query(async (opts) => { @@ -647,7 +687,11 @@ export const tinybirdRouter = createTRPCRouter({ }); } - return await procedure(opts.input); + const result = await procedure(opts.input); + return { + ...result, + data: trimToWindow(result.data, opts.input.fromDate, opts.input.toDate), + }; }), workspace30d: protectedProcedure diff --git a/packages/services/src/agent-tools/index.ts b/packages/services/src/agent-tools/index.ts index 90516666c..bd354a46a 100644 --- a/packages/services/src/agent-tools/index.ts +++ b/packages/services/src/agent-tools/index.ts @@ -47,6 +47,7 @@ import { updateStatusReportUpdateTool, } from "./status-report"; import type { AnyAgentTool } from "./types"; +import { getWorkspaceTool } from "./workspace"; export { getAuditLogTool, listAuditLogsTool } from "./audit"; export { getContentPageTool, searchContentTool } from "./content"; @@ -82,6 +83,7 @@ export { listNotificationsTool } from "./notification"; export { listStatusPagesTool } from "./page"; export { listPageComponentsTool } from "./page-component"; export { listPrivateLocationsTool } from "./private-location"; +export { getWorkspaceTool } from "./workspace"; export { addStatusReportUpdateTool, createStatusReportTool, @@ -152,6 +154,7 @@ export const agentTools = { get_response_log: getResponseLogTool, list_notifications: listNotificationsTool, list_private_locations: listPrivateLocationsTool, + get_workspace: getWorkspaceTool, list_audit_logs: listAuditLogsTool, get_audit_log: getAuditLogTool, search_docs: searchDocsTool, diff --git a/packages/services/src/agent-tools/workspace.ts b/packages/services/src/agent-tools/workspace.ts new file mode 100644 index 000000000..2ab130f07 --- /dev/null +++ b/packages/services/src/agent-tools/workspace.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +import { getWorkspaceUsage } from "../workspace"; +import type { AgentTool } from "./types"; + +const GetWorkspaceInput = z.object({}).strict(); +const GetWorkspaceOutput = z.object({ + id: z.number().int(), + slug: z.string(), + name: z.string().nullable(), + plan: z.string(), + trialEndsAt: z.string().nullable(), + usage: z.object({ + monitors: z.number().int(), + statusPages: z.number().int(), + pageComponents: z.number().int(), + notificationChannels: z.number().int(), + statusReports: z.number().int(), + }), +}); + +export const getWorkspaceTool: AgentTool< + z.infer, + z.infer +> = { + name: "get_workspace", + description: + "The current workspace: name, slug, plan, trial end and usage counts for monitors, status pages, components, notification channels and status reports. Use to answer which-plan or how-many questions. Plan limits and billing identifiers are NOT exposed.", + scope: "read", + destructive: false, + inputSchema: GetWorkspaceInput, + outputSchema: GetWorkspaceOutput, + async run({ ctx }) { + const { workspace } = ctx; + const usage = await getWorkspaceUsage({ ctx }); + return { + id: workspace.id, + slug: workspace.slug, + name: workspace.name ?? null, + plan: workspace.plan ?? "free", + trialEndsAt: workspace.trialEndsAt?.toISOString() ?? null, + usage: { + monitors: usage.monitors, + statusPages: usage.pages, + pageComponents: usage.pageComponents, + notificationChannels: usage.notifications, + statusReports: usage.statusReports, + }, + }; + }, +}; diff --git a/packages/tinybird/src/client.ts b/packages/tinybird/src/client.ts index fde1f7af5..8f4634b01 100644 --- a/packages/tinybird/src/client.ts +++ b/packages/tinybird/src/client.ts @@ -1781,6 +1781,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -1794,6 +1795,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -1807,6 +1809,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -1820,6 +1823,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2265,6 +2269,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2278,6 +2283,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2291,6 +2297,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2304,6 +2311,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2781,6 +2789,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2801,6 +2810,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2842,6 +2852,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -2862,6 +2873,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -3069,6 +3081,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -3444,6 +3457,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -3464,6 +3478,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -3484,6 +3499,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -3504,6 +3520,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -3524,6 +3541,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }), @@ -3544,6 +3562,7 @@ export class OSTinybird { parameters: z.object({ monitorId: z.string(), regions: z.array(z.enum(monitorRegions).or(z.string())).optional(), + interval: z.int().optional(), fromDate: z.string().optional(), toDate: z.string().optional(), }),