From 1161229947c7dffadb660bba3317993bf101bbb2 Mon Sep 17 00:00:00 2001 From: finxol Date: Wed, 24 Jun 2026 11:00:25 +0200 Subject: [PATCH] feat(status-page): add GetStatusPageOverview RPC --- .../status-page/__tests__/status-page.test.ts | 240 +++++++++ .../rpc/handlers/status-page/converters.ts | 46 ++ .../routes/rpc/handlers/status-page/index.ts | 455 ++++++++---------- apps/server/static/openapi.yaml | 157 ++++++ .../pages/docs/sdk/nodejs/reference.mdx | 18 +- .../docs/sdk/nodejs/status-page-service.mdx | 22 +- .../openstatus/status_page/v1/service.proto | 43 ++ .../status_page/v1/status_page.proto | 34 ++ packages/proto/gen/openapi.yaml | 157 ++++++ .../openstatus/status_page/v1/service_pb.ts | 117 ++++- .../status_page/v1/status_page_pb.ts | 117 ++++- packages/proto/plan/api.md | 4 +- packages/services/src/page/get-content.ts | 177 +++++++ packages/services/src/page/index.ts | 6 + 14 files changed, 1315 insertions(+), 278 deletions(-) create mode 100644 packages/services/src/page/get-content.ts diff --git a/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts b/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts index 801187e31..f24c800a9 100644 --- a/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts +++ b/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts @@ -2848,6 +2848,246 @@ describe("StatusPageService.GetOverallStatus", () => { }); }); +describe("StatusPageService.GetStatusPageOverview", () => { + test("returns full overview by id", async () => { + const res = await connectRequest( + "GetStatusPageOverview", + { id: String(testPageId) }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.statusPage.id).toBe(String(testPageId)); + expect(data).toHaveProperty("configuration"); + expect(data).toHaveProperty("components"); + expect(data).toHaveProperty("overallStatus"); + expect(data).toHaveProperty("componentStatuses"); + }); + + test("returns configuration defaults when unset", async () => { + const createdPage = await db + .insert(page) + .values({ + workspaceId: 1, + title: `${TEST_PREFIX}-overview-default-config`, + slug: `${TEST_PREFIX}-overview-default-config-slug`, + description: "", + customDomain: "", + published: true, + accessType: "public", + }) + .returning() + .get(); + + try { + const res = await connectRequest( + "GetStatusPageOverview", + { id: String(createdPage.id) }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.configuration.metricType).toBe("PAGE_METRIC_TYPE_REQUESTS"); + expect(data.configuration.barType).toBe("PAGE_BAR_TYPE_ABSOLUTE"); + expect(data.configuration.showUptime).toBe(true); + expect(data.configuration.themeKey).toBe("default"); + } finally { + await db.delete(page).where(eq(page.id, createdPage.id)); + } + }); + + test("maps stored configuration values", async () => { + const createdPage = await db + .insert(page) + .values({ + workspaceId: 1, + title: `${TEST_PREFIX}-overview-custom-config`, + slug: `${TEST_PREFIX}-overview-custom-config-slug`, + description: "", + customDomain: "", + published: true, + accessType: "public", + configuration: { + value: "duration", + type: "manual", + uptime: false, + theme: "dracula", + }, + }) + .returning() + .get(); + + try { + const res = await connectRequest( + "GetStatusPageOverview", + { id: String(createdPage.id) }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.configuration.metricType).toBe("PAGE_METRIC_TYPE_DURATION"); + expect(data.configuration.barType).toBe("PAGE_BAR_TYPE_MANUAL"); + // proto3 JSON omits a false bool (the default), so it reads back as undefined + expect(data.configuration.showUptime ?? false).toBe(false); + expect(data.configuration.themeKey).toBe("dracula"); + } finally { + await db.delete(page).where(eq(page.id, createdPage.id)); + } + }); + + test("falls back to default configuration for invalid stored values", async () => { + const createdPage = await db + .insert(page) + .values({ + workspaceId: 1, + title: `${TEST_PREFIX}-overview-bad-config`, + slug: `${TEST_PREFIX}-overview-bad-config-slug`, + description: "", + customDomain: "", + published: true, + accessType: "public", + configuration: { value: "latency", theme: "removed-theme-xyz" }, + }) + .returning() + .get(); + + try { + const res = await connectRequest( + "GetStatusPageOverview", + { id: String(createdPage.id) }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.configuration.metricType).toBe("PAGE_METRIC_TYPE_REQUESTS"); + expect(data.configuration.themeKey).toBe("default"); + } finally { + await db.delete(page).where(eq(page.id, createdPage.id)); + } + }); + + test("returns 401 when no auth key provided", async () => { + const res = await connectRequest("GetStatusPageOverview", { + id: String(testPageId), + }); + + expect(res.status).toBe(401); + }); + + test("returns 404 for non-existent page", async () => { + const res = await connectRequest( + "GetStatusPageOverview", + { id: "99999" }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(404); + }); + + test("returns 404 for a page in another workspace", async () => { + const otherPage = await db + .insert(page) + .values({ + workspaceId: 2, + title: `${TEST_PREFIX}-overview-other-ws`, + slug: `${TEST_PREFIX}-overview-other-ws-slug`, + description: "", + customDomain: "", + published: true, + accessType: "public", + }) + .returning() + .get(); + + try { + const res = await connectRequest( + "GetStatusPageOverview", + { id: String(otherPage.id) }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(404); + } finally { + await db.delete(page).where(eq(page.id, otherPage.id)); + } + }); + + test("rejects slug identifier (id required)", async () => { + const res = await connectRequest( + "GetStatusPageOverview", + { slug: testPageSlug }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(400); + }); + + test("reflects degraded status from an active report", async () => { + const report = await db + .insert(statusReport) + .values({ + workspaceId: 1, + pageId: testPageId, + title: `${TEST_PREFIX}-overview-report`, + status: "investigating", + }) + .returning() + .get(); + + await db.insert(statusReportsToPageComponents).values({ + statusReportId: report.id, + pageComponentId: testComponentId, + }); + + try { + const res = await connectRequest( + "GetStatusPageOverview", + { id: String(testPageId) }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.overallStatus).toBe("OVERALL_STATUS_DEGRADED"); + const reported = data.statusReports.find( + (r: { title: string }) => r.title === `${TEST_PREFIX}-overview-report`, + ); + expect(reported).toBeDefined(); + } finally { + await db + .delete(statusReportsToPageComponents) + .where(eq(statusReportsToPageComponents.statusReportId, report.id)); + await db.delete(statusReport).where(eq(statusReport.id, report.id)); + } + }); + + test("excludes uptime time-series buckets", async () => { + const res = await connectRequest( + "GetStatusPageOverview", + { id: String(testPageId) }, + { "x-openstatus-key": "1" }, + ); + + expect(res.status).toBe(200); + + const data = await res.json(); + expect(Array.isArray(data.components)).toBe(true); + expect(data.components.length).toBeGreaterThan(0); + for (const component of data.components) { + expect(component).not.toHaveProperty("buckets"); + } + }); +}); + // ========================================================================== // New Fields: icon, custom_domain, theme, access_type, password, auth_email_domains // ========================================================================== diff --git a/apps/server/src/routes/rpc/handlers/status-page/converters.ts b/apps/server/src/routes/rpc/handlers/status-page/converters.ts index 7c29b7324..7d6314866 100644 --- a/apps/server/src/routes/rpc/handlers/status-page/converters.ts +++ b/apps/server/src/routes/rpc/handlers/status-page/converters.ts @@ -1,8 +1,10 @@ +import type { PageConfiguration as DBPageConfiguration } from "@openstatus/db/src/schema"; import type { Locale } from "@openstatus/locales"; import type { CustomTheme, PageComponent, PageComponentGroup, + PageConfiguration, PageSubscriber, StatusPage, StatusPageSummary, @@ -14,7 +16,9 @@ import { ComponentEventType, OverallStatus, PageAccessType, + PageBarType, PageComponentType, + PageMetricType, PageTheme, Locale as ProtoLocale, SubscriberSource, @@ -161,6 +165,48 @@ export function protoThemeToDb(theme: PageTheme): "system" | "light" | "dark" { } } +function metricTypeToProto( + value: DBPageConfiguration["value"], +): PageMetricType { + switch (value) { + case "duration": + return PageMetricType.DURATION; + case "requests": + return PageMetricType.REQUESTS; + case "manual": + return PageMetricType.MANUAL; + default: + return PageMetricType.UNSPECIFIED; + } +} + +function barTypeToProto(type: DBPageConfiguration["type"]): PageBarType { + switch (type) { + case "absolute": + return PageBarType.ABSOLUTE; + case "manual": + return PageBarType.MANUAL; + default: + return PageBarType.UNSPECIFIED; + } +} + +/** + * Convert the DB page configuration (parsed via pageConfigurationSchema) to proto. + */ +export function dbConfigurationToProto( + configuration: DBPageConfiguration, +): PageConfiguration { + return { + $typeName: "openstatus.status_page.v1.PageConfiguration" as const, + metricType: metricTypeToProto(configuration.value), + barType: barTypeToProto(configuration.type), + showUptime: configuration.uptime, + themeKey: configuration.theme, + days: configuration.days, + }; +} + /** * Convert DB component type string to proto enum. */ diff --git a/apps/server/src/routes/rpc/handlers/status-page/index.ts b/apps/server/src/routes/rpc/handlers/status-page/index.ts index 82f9a3a41..3122f0e8a 100644 --- a/apps/server/src/routes/rpc/handlers/status-page/index.ts +++ b/apps/server/src/routes/rpc/handlers/status-page/index.ts @@ -18,10 +18,9 @@ import { page, pageComponent, pageComponentGroup, + pageConfigurationSchema, pageSubscriber, statusReport, - statusReportUpdate, - statusReportUpdateToPageComponents, statusReportsToPageComponents, } from "@openstatus/db/src/schema"; import type { @@ -48,6 +47,8 @@ import { deletePage, getPage, getPageBySlug, + getStatusPageContent, + type StatusPageContent, listPages, updatePageAppearance, updatePageCustomDomain, @@ -79,6 +80,7 @@ import { dayStatusToProto, dbComponentToProto, dbComponentTypeToProto, + dbConfigurationToProto, dbGroupToProto, dbPageToProto, dbPageToProtoSummary, @@ -309,6 +311,161 @@ async function getMonitorById(id: number, workspaceId: number) { .get(); } +type ResolvedPage = NonNullable>>; + +async function fetchStatusPageContent( + pageData: ResolvedPage, + prefetched?: StatusPageContent, +) { + const content = + prefetched ?? (await getStatusPageContent({ pageId: pageData.id })); + + const { dbStatusToProto, dbUpdateToProto } = + await import("../status-report/converters"); + + const statusReports = content.statusReports.map((report) => ({ + $typeName: "openstatus.status_report.v1.StatusReport" as const, + id: String(report.id), + status: dbStatusToProto(report.status), + title: report.title, + pageComponentIds: report.pageComponentIds.map(String), + updates: report.updates.map((u) => + dbUpdateToProto({ ...u, componentImpacts: u.componentImpacts }), + ), + createdAt: report.createdAt?.toISOString() ?? "", + updatedAt: report.updatedAt?.toISOString() ?? "", + })); + + const maintenancesProto = content.maintenances.map((m) => ({ + $typeName: "openstatus.maintenance.v1.MaintenanceSummary" as const, + id: String(m.id), + title: m.title, + message: m.message, + from: m.from.toISOString(), + to: m.to.toISOString(), + pageId: String(pageData.id), + pageComponentIds: m.pageComponentIds.map(String), + createdAt: m.createdAt?.toISOString() ?? "", + updatedAt: m.updatedAt?.toISOString() ?? "", + })); + + return { + components: content.components.map(dbComponentToProto), + groups: content.groups.map(dbGroupToProto), + statusReports, + maintenances: maintenancesProto, + }; +} + +async function computeOverallStatus( + pageData: ResolvedPage, + prefetched?: { id: number }[], + database: typeof db = db, +) { + const components = + prefetched ?? + (await database + .select({ id: pageComponent.id }) + .from(pageComponent) + .where(eq(pageComponent.pageId, pageData.id)) + .all()); + + const componentIds = components.map((c) => c.id); + const now = new Date(); + + let hasActiveStatusReport = false; + const componentReportStatus = new Map(); + + if (componentIds.length > 0) { + const activeReports = await database + .select({ + componentId: statusReportsToPageComponents.pageComponentId, + }) + .from(statusReportsToPageComponents) + .innerJoin( + statusReport, + eq(statusReportsToPageComponents.statusReportId, statusReport.id), + ) + .where( + and( + inArray(statusReportsToPageComponents.pageComponentId, componentIds), + inArray(statusReport.status, [ + "investigating", + "identified", + "monitoring", + ]), + ), + ) + .all(); + + hasActiveStatusReport = activeReports.length > 0; + + for (const report of activeReports) { + componentReportStatus.set(report.componentId, true); + } + } + + let hasActiveMaintenance = false; + const componentMaintenanceStatus = new Map(); + + const activeMaintenances = await database + .select() + .from(maintenance) + .where( + and( + eq(maintenance.pageId, pageData.id), + lte(maintenance.from, now), + gte(maintenance.to, now), + ), + ) + .all(); + + hasActiveMaintenance = activeMaintenances.length > 0; + + if (activeMaintenances.length > 0) { + const maintenanceIds = activeMaintenances.map((m) => m.id); + const maintenanceComponentAssocs = await database + .select() + .from(maintenancesToPageComponents) + .where( + inArray(maintenancesToPageComponents.maintenanceId, maintenanceIds), + ) + .all(); + + for (const assoc of maintenanceComponentAssocs) { + componentMaintenanceStatus.set(assoc.pageComponentId, true); + } + } + + const overallStatus = hasActiveStatusReport + ? OverallStatus.DEGRADED + : hasActiveMaintenance + ? OverallStatus.MAINTENANCE + : OverallStatus.OPERATIONAL; + + const componentStatuses = components.map((c) => { + const hasReport = componentReportStatus.get(c.id) ?? false; + const hasMaintenance = componentMaintenanceStatus.get(c.id) ?? false; + + const status = hasReport + ? OverallStatus.DEGRADED + : hasMaintenance + ? OverallStatus.MAINTENANCE + : OverallStatus.OPERATIONAL; + + return { + $typeName: "openstatus.status_page.v1.ComponentStatus" as const, + componentId: String(c.id), + status, + }; + }); + + return { + overallStatus, + componentStatuses, + }; +} + /** * Status page service implementation for ConnectRPC. */ @@ -1401,160 +1558,7 @@ export const statusPageServiceImpl: ServiceImpl = { validatePublicAccess(pageData, identifierValue); } - // Get components - const components = await db - .select() - .from(pageComponent) - .where(eq(pageComponent.pageId, pageData.id)) - .orderBy(pageComponent.order) - .all(); - - // Get groups - const groups = await db - .select() - .from(pageComponentGroup) - .where(eq(pageComponentGroup.pageId, pageData.id)) - .all(); - - // Get active status reports (not resolved) - const activeReports = await db - .select() - .from(statusReport) - .where( - and( - eq(statusReport.pageId, pageData.id), - inArray(statusReport.status, [ - "investigating", - "identified", - "monitoring", - ]), - ), - ) - .orderBy(desc(statusReport.createdAt)) - .all(); - - // Get status report updates for active reports - const reportIds = activeReports.map((r) => r.id); - const reportUpdates = - reportIds.length > 0 - ? await db - .select() - .from(statusReportUpdate) - .where(inArray(statusReportUpdate.statusReportId, reportIds)) - .orderBy(desc(statusReportUpdate.date)) - .all() - : []; - - // Get page component IDs for each report - const reportComponents = - reportIds.length > 0 - ? await db - .select() - .from(statusReportsToPageComponents) - .where( - inArray(statusReportsToPageComponents.statusReportId, reportIds), - ) - .all() - : []; - - // Get impact rows for the reports' updates, grouped per update id - const updateIds = reportUpdates.map((u) => u.id); - const updateImpacts = - updateIds.length > 0 - ? await db - .select() - .from(statusReportUpdateToPageComponents) - .where( - inArray( - statusReportUpdateToPageComponents.statusReportUpdateId, - updateIds, - ), - ) - .all() - : []; - const impactsByUpdate = new Map(); - for (const row of updateImpacts) { - const arr = impactsByUpdate.get(row.statusReportUpdateId); - if (arr) arr.push(row); - else impactsByUpdate.set(row.statusReportUpdateId, [row]); - } - - // Import the converters from status-report service - const { dbStatusToProto, dbUpdateToProto } = - await import("../status-report/converters"); - - // Convert reports to proto format - const statusReports = activeReports.map((report) => { - const updates = reportUpdates.filter( - (u) => u.statusReportId === report.id, - ); - const componentIds = reportComponents - .filter((rc) => rc.statusReportId === report.id) - .map((rc) => String(rc.pageComponentId)); - - return { - $typeName: "openstatus.status_report.v1.StatusReport" as const, - id: String(report.id), - status: dbStatusToProto(report.status), - title: report.title, - pageComponentIds: componentIds, - updates: updates.map((u) => - dbUpdateToProto({ - ...u, - componentImpacts: (impactsByUpdate.get(u.id) ?? []).map((row) => ({ - pageComponentId: row.pageComponentId, - impact: row.impact, - })), - }), - ), - createdAt: report.createdAt?.toISOString() ?? "", - updatedAt: report.updatedAt?.toISOString() ?? "", - }; - }); - - // Get maintenances for the page (upcoming and recent) - const pageMaintenances = await db - .select() - .from(maintenance) - .where(eq(maintenance.pageId, pageData.id)) - .orderBy(desc(maintenance.from)) - .all(); - - // Get component associations for maintenances - const maintenanceIds = pageMaintenances.map((m) => m.id); - const maintenanceComponents = - maintenanceIds.length > 0 - ? await db - .select() - .from(maintenancesToPageComponents) - .where( - inArray( - maintenancesToPageComponents.maintenanceId, - maintenanceIds, - ), - ) - .all() - : []; - - // Convert maintenances to proto format - const maintenancesProto = pageMaintenances.map((m) => { - const componentIds = maintenanceComponents - .filter((mc) => mc.maintenanceId === m.id) - .map((mc) => String(mc.pageComponentId)); - - return { - $typeName: "openstatus.maintenance.v1.MaintenanceSummary" as const, - id: String(m.id), - title: m.title, - message: m.message, - from: m.from.toISOString(), - to: m.to.toISOString(), - pageId: String(pageData.id), - pageComponentIds: componentIds, - createdAt: m.createdAt?.toISOString() ?? "", - updatedAt: m.updatedAt?.toISOString() ?? "", - }; - }); + const content = await fetchStatusPageContent(pageData); const statusPage = dbPageToProto(pageData); if (isPublicAccess) { @@ -1565,10 +1569,7 @@ export const statusPageServiceImpl: ServiceImpl = { return { statusPage, - components: components.map(dbComponentToProto), - groups: groups.map(dbGroupToProto), - statusReports, - maintenances: maintenancesProto, + ...content, }; }, @@ -1602,119 +1603,43 @@ export const statusPageServiceImpl: ServiceImpl = { validatePublicAccess(pageData, identifierValue); } - // Get components - const components = await db - .select() - .from(pageComponent) - .where(eq(pageComponent.pageId, pageData.id)) - .all(); - - const componentIds = components.map((c) => c.id); - const now = new Date(); - - // Check for active status reports (degraded state) - let hasActiveStatusReport = false; - const componentReportStatus = new Map(); - - if (componentIds.length > 0) { - const activeReports = await db - .select({ - componentId: statusReportsToPageComponents.pageComponentId, - }) - .from(statusReportsToPageComponents) - .innerJoin( - statusReport, - eq(statusReportsToPageComponents.statusReportId, statusReport.id), - ) - .where( - and( - inArray( - statusReportsToPageComponents.pageComponentId, - componentIds, - ), - inArray(statusReport.status, [ - "investigating", - "identified", - "monitoring", - ]), - ), - ) - .all(); - - hasActiveStatusReport = activeReports.length > 0; - - // Track which components have active reports - for (const report of activeReports) { - componentReportStatus.set(report.componentId, true); - } - } - - // Check for active maintenances (info state - current time between from and to) - let hasActiveMaintenance = false; - const componentMaintenanceStatus = new Map(); - - const activeMaintenances = await db - .select() - .from(maintenance) - .where( - and( - eq(maintenance.pageId, pageData.id), - lte(maintenance.from, now), - gte(maintenance.to, now), - ), - ) - .all(); - - hasActiveMaintenance = activeMaintenances.length > 0; + return computeOverallStatus(pageData); + }, - // Get component associations for active maintenances - if (activeMaintenances.length > 0) { - const maintenanceIds = activeMaintenances.map((m) => m.id); - const maintenanceComponentAssocs = await db - .select() - .from(maintenancesToPageComponents) - .where( - inArray(maintenancesToPageComponents.maintenanceId, maintenanceIds), - ) - .all(); + async getStatusPageOverview(req, ctx) { + try { + const rpcCtx = getRpcContext(ctx); + const pageData = await getPageById(Number(req.id), rpcCtx.workspace.id); - // Track which components are under maintenance - for (const assoc of maintenanceComponentAssocs) { - componentMaintenanceStatus.set(assoc.pageComponentId, true); + if (!pageData) { + throw statusPageNotFoundError(req.id); } - } - // Determine overall status based on priority: degraded > maintenance > operational - // Note: In the existing codebase, status reports indicate "degraded" state - // and maintenances indicate "info/maintenance" state - const overallStatus = hasActiveStatusReport - ? OverallStatus.DEGRADED - : hasActiveMaintenance - ? OverallStatus.MAINTENANCE - : OverallStatus.OPERATIONAL; + const pageContent = await getStatusPageContent({ pageId: pageData.id }); + const [content, status] = await Promise.all([ + fetchStatusPageContent(pageData, pageContent), + computeOverallStatus(pageData, pageContent.components), + ]); - // Build component statuses based on their individual state - const componentStatuses = components.map((c) => { - const hasReport = componentReportStatus.get(c.id) ?? false; - const hasMaintenance = componentMaintenanceStatus.get(c.id) ?? false; - - const status = hasReport - ? OverallStatus.DEGRADED - : hasMaintenance - ? OverallStatus.MAINTENANCE - : OverallStatus.OPERATIONAL; + // legacy/pre-enforcement rows may hold an invalid configuration; fall back to defaults + const parsedConfig = pageConfigurationSchema.safeParse( + pageData.configuration ?? {}, + ); return { - $typeName: "openstatus.status_page.v1.ComponentStatus" as const, - componentId: String(c.id), - status, + statusPage: dbPageToProto(pageData), + configuration: dbConfigurationToProto( + parsedConfig.success + ? parsedConfig.data + : pageConfigurationSchema.parse({}), + ), + ...content, + overallStatus: status.overallStatus, + componentStatuses: status.componentStatuses, }; - }); - - return { - overallStatus, - componentStatuses, - }; + } catch (err) { + toConnectError(err); + } }, async getPageComponentDailySummary(req, ctx) { diff --git a/apps/server/static/openapi.yaml b/apps/server/static/openapi.yaml index 375b28727..c5788f8df 100644 --- a/apps/server/static/openapi.yaml +++ b/apps/server/static/openapi.yaml @@ -3013,6 +3013,65 @@ components: title: GetStatusPageContentResponse additionalProperties: false description: GetStatusPageContentResponse is the response containing the full status page content. + openstatus.status_page.v1.GetStatusPageOverviewRequest: + type: object + properties: + id: + type: string + title: id + minLength: 1 + description: ID of the status page (required, workspace-scoped). + title: GetStatusPageOverviewRequest + additionalProperties: false + description: GetStatusPageOverviewRequest requests the full overview of a status page by id. + openstatus.status_page.v1.GetStatusPageOverviewResponse: + type: object + properties: + statusPage: + title: status_page + description: The status page details. + $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage' + configuration: + title: configuration + description: Rich rendering configuration of the page. + $ref: '#/components/schemas/openstatus.status_page.v1.PageConfiguration' + components: + type: array + items: + $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent' + title: components + description: Components on the status page. + groups: + type: array + items: + $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup' + title: groups + description: Component groups on the status page. + statusReports: + type: array + items: + $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport' + title: status_reports + description: Active and recent status reports. + maintenances: + type: array + items: + $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary' + title: maintenances + description: Scheduled maintenances. + overallStatus: + title: overall_status + description: Aggregated status across all components. + $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus' + componentStatuses: + type: array + items: + $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus' + title: component_statuses + description: Status of individual components. + title: GetStatusPageOverviewResponse + additionalProperties: false + description: GetStatusPageOverviewResponse bundles all data for a single status page. openstatus.status_page.v1.GetStatusPageRequest: type: object properties: @@ -3156,6 +3215,14 @@ components: - PAGE_ACCESS_TYPE_AUTHENTICATED - PAGE_ACCESS_TYPE_IP_RESTRICTED description: PageAccessType defines who can access the status page. + openstatus.status_page.v1.PageBarType: + type: string + title: PageBarType + enum: + - PAGE_BAR_TYPE_UNSPECIFIED + - PAGE_BAR_TYPE_ABSOLUTE + - PAGE_BAR_TYPE_MANUAL + description: PageBarType mirrors page.configuration.type (how the status bar is computed). openstatus.status_page.v1.PageComponent: type: object properties: @@ -3282,6 +3349,44 @@ components: - PAGE_COMPONENT_TYPE_MONITOR - PAGE_COMPONENT_TYPE_STATIC description: PageComponentType defines the type of a component on a status page. + openstatus.status_page.v1.PageConfiguration: + type: object + properties: + metricType: + title: metric_type + description: Which metric the status bars represent (configuration.value). + $ref: '#/components/schemas/openstatus.status_page.v1.PageMetricType' + barType: + title: bar_type + description: How the status bar is computed (configuration.type). + $ref: '#/components/schemas/openstatus.status_page.v1.PageBarType' + showUptime: + type: boolean + title: show_uptime + description: Whether to show the uptime percentage (configuration.uptime). + themeKey: + type: string + title: theme_key + description: |- + Theme key from the theme store (configuration.theme), e.g. "default". + Free-form string rather than an enum because the theme catalog is dynamic. + days: + type: integer + title: days + format: int32 + description: 'Number of uptime-bar days rendered on the page (configuration.days): 30 or 45.' + title: PageConfiguration + additionalProperties: false + description: PageConfiguration is the rich rendering config stored in page.configuration. + openstatus.status_page.v1.PageMetricType: + type: string + title: PageMetricType + enum: + - PAGE_METRIC_TYPE_UNSPECIFIED + - PAGE_METRIC_TYPE_DURATION + - PAGE_METRIC_TYPE_REQUESTS + - PAGE_METRIC_TYPE_MANUAL + description: PageMetricType mirrors page.configuration.value (which metric the status bars represent). openstatus.status_page.v1.PageSubscriber: type: object properties: @@ -5664,6 +5769,58 @@ paths: application/json: schema: $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse' + /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageOverview: + get: + tags: + - StatusPageService + summary: GetStatusPageOverview + description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.' + operationId: StatusPageService_GetStatusPageOverview.get + parameters: + - name: message + in: query + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest' + responses: + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/connect.error' + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse' + post: + tags: + - StatusPageService + summary: GetStatusPageOverview + description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.' + operationId: StatusPageService_GetStatusPageOverview + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest' + required: true + responses: + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/connect.error' + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse' /rpc/openstatus.status_page.v1.StatusPageService/ListStatusPages: get: tags: diff --git a/apps/web/src/content/pages/docs/sdk/nodejs/reference.mdx b/apps/web/src/content/pages/docs/sdk/nodejs/reference.mdx index 6059737a7..baf09d3f5 100644 --- a/apps/web/src/content/pages/docs/sdk/nodejs/reference.mdx +++ b/apps/web/src/content/pages/docs/sdk/nodejs/reference.mdx @@ -114,6 +114,21 @@ description: "Complete reference for enums, regions, assertions, and TypeScript | `MONITOR` | Linked to a monitor | | `STATIC` | Static component (manual) | +### PageMetricType + +| Value | Description | +|-------|-------------| +| `DURATION` | Status bars represent response duration | +| `REQUESTS` | Status bars represent request counts | +| `MANUAL` | Status bars set manually | + +### PageBarType + +| Value | Description | +|-------|-------------| +| `ABSOLUTE` | Status bar computed from absolute values | +| `MANUAL` | Status bar set manually | + ### NumberComparator | Value | Description | @@ -343,7 +358,8 @@ All types and enums exported from `@openstatus/sdk-node`: - `ListSubscribersRequest`, `ListSubscribersResponse` - `GetStatusPageContentRequest`, `GetStatusPageContentResponse` - `GetOverallStatusRequest`, `GetOverallStatusResponse`, `ComponentStatus` -- `OverallStatus`, `PageAccessType`, `PageTheme`, `PageComponentType` +- `GetStatusPageOverviewRequest`, `GetStatusPageOverviewResponse`, `PageConfiguration` +- `OverallStatus`, `PageAccessType`, `PageTheme`, `PageComponentType`, `PageMetricType`, `PageBarType` ### Maintenance Types diff --git a/apps/web/src/content/pages/docs/sdk/nodejs/status-page-service.mdx b/apps/web/src/content/pages/docs/sdk/nodejs/status-page-service.mdx index 4cd93d3d8..fa912a234 100644 --- a/apps/web/src/content/pages/docs/sdk/nodejs/status-page-service.mdx +++ b/apps/web/src/content/pages/docs/sdk/nodejs/status-page-service.mdx @@ -4,7 +4,7 @@ title: Status Page Service description: "Manage status pages, components, groups, and subscribers with the openstatus Node.js SDK" --- -Manage status pages, components, component groups, and subscribers. The Status Page Service provides 18 RPC methods. +Manage status pages, components, component groups, and subscribers. The Status Page Service provides 20 RPC methods. ## Status Page CRUD @@ -224,6 +224,26 @@ for (const { componentId, status } of componentStatuses) { } ``` +## Get Status Page Overview + +Get everything about a single status page in one authenticated call: the page, its rendering configuration, components, groups, active status reports, maintenances, and the computed overall and per-component statuses. Workspace-scoped by `id` only (no public slug access). Uptime time-series is not included — use Get Page Component Daily Summary for that. + +```typescript +import { createOpenStatusClient, OverallStatus } from "@openstatus/sdk-node"; + +const client = createOpenStatusClient({ + apiKey: process.env.OPENSTATUS_API_KEY, +}); + +const overview = await client.statusPage.v1.StatusPageService + .getStatusPageOverview({ id: "page_123" }); + +console.log(`Page: ${overview.statusPage?.title}`); +console.log(`Theme: ${overview.configuration?.themeKey}`); +console.log(`Components: ${overview.components.length}`); +console.log(`Overall: ${OverallStatus[overview.overallStatus]}`); +``` + ## Get Page Component Daily Summary Get per-component daily uptime buckets merged with the incident, maintenance, and status-report timeline over the last N days (max 45). This is the single source of truth for rendering per-component status bars and uptime calendars: each component carries gap-filled daily `buckets` (with a resolved `status`) plus the `events` affecting it. Identify the page by ID or slug; optionally restrict to specific `componentIds`. diff --git a/packages/proto/api/openstatus/status_page/v1/service.proto b/packages/proto/api/openstatus/status_page/v1/service.proto index 1a03a3c13..985521e04 100644 --- a/packages/proto/api/openstatus/status_page/v1/service.proto +++ b/packages/proto/api/openstatus/status_page/v1/service.proto @@ -111,6 +111,16 @@ service StatusPageService { }; } + // GetStatusPageOverview returns everything about a single status page in one + // authenticated call: page, rich configuration, components, groups, active + // reports, maintenances, and computed statuses. + rpc GetStatusPageOverview(GetStatusPageOverviewRequest) returns (GetStatusPageOverviewResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + option (gnostic.openapi.v3.operation) = { + description: "Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that." + }; + } + // GetPageComponentDailySummary returns per-component daily uptime buckets merged // with the incident / maintenance / report timeline — the single source of truth // for rendering per-component status bars and uptime calendars. @@ -651,6 +661,39 @@ message GetOverallStatusResponse { repeated ComponentStatus component_statuses = 2; } +// GetStatusPageOverviewRequest requests the full overview of a status page by id. +message GetStatusPageOverviewRequest { + // ID of the status page (required, workspace-scoped). + string id = 1 [(buf.validate.field).string.min_len = 1]; +} + +// GetStatusPageOverviewResponse bundles all data for a single status page. +message GetStatusPageOverviewResponse { + // The status page details. + StatusPage status_page = 1; + + // Rich rendering configuration of the page. + PageConfiguration configuration = 2; + + // Components on the status page. + repeated PageComponent components = 3; + + // Component groups on the status page. + repeated PageComponentGroup groups = 4; + + // Active and recent status reports. + repeated openstatus.status_report.v1.StatusReport status_reports = 5; + + // Scheduled maintenances. + repeated openstatus.maintenance.v1.MaintenanceSummary maintenances = 6; + + // Aggregated status across all components. + OverallStatus overall_status = 7; + + // Status of individual components. + repeated ComponentStatus component_statuses = 8; +} + // ============================================================================= // Per-Component Daily Summary Messages // ============================================================================= diff --git a/packages/proto/api/openstatus/status_page/v1/status_page.proto b/packages/proto/api/openstatus/status_page/v1/status_page.proto index 977260091..bc0343404 100644 --- a/packages/proto/api/openstatus/status_page/v1/status_page.proto +++ b/packages/proto/api/openstatus/status_page/v1/status_page.proto @@ -52,6 +52,40 @@ message CustomTheme { map dark = 2; } +// PageMetricType mirrors page.configuration.value (which metric the status bars represent). +enum PageMetricType { + PAGE_METRIC_TYPE_UNSPECIFIED = 0; + PAGE_METRIC_TYPE_DURATION = 1; + PAGE_METRIC_TYPE_REQUESTS = 2; + PAGE_METRIC_TYPE_MANUAL = 3; +} + +// PageBarType mirrors page.configuration.type (how the status bar is computed). +enum PageBarType { + PAGE_BAR_TYPE_UNSPECIFIED = 0; + PAGE_BAR_TYPE_ABSOLUTE = 1; + PAGE_BAR_TYPE_MANUAL = 2; +} + +// PageConfiguration is the rich rendering config stored in page.configuration. +message PageConfiguration { + // Which metric the status bars represent (configuration.value). + PageMetricType metric_type = 1; + + // How the status bar is computed (configuration.type). + PageBarType bar_type = 2; + + // Whether to show the uptime percentage (configuration.uptime). + bool show_uptime = 3; + + // Theme key from the theme store (configuration.theme), e.g. "default". + // Free-form string rather than an enum because the theme catalog is dynamic. + string theme_key = 4; + + // Number of uptime-bar days rendered on the page (configuration.days): 30 or 45. + int32 days = 5; +} + // StatusPage represents a full status page with all details. message StatusPage { // Unique identifier for the status page. diff --git a/packages/proto/gen/openapi.yaml b/packages/proto/gen/openapi.yaml index 375b28727..c5788f8df 100644 --- a/packages/proto/gen/openapi.yaml +++ b/packages/proto/gen/openapi.yaml @@ -3013,6 +3013,65 @@ components: title: GetStatusPageContentResponse additionalProperties: false description: GetStatusPageContentResponse is the response containing the full status page content. + openstatus.status_page.v1.GetStatusPageOverviewRequest: + type: object + properties: + id: + type: string + title: id + minLength: 1 + description: ID of the status page (required, workspace-scoped). + title: GetStatusPageOverviewRequest + additionalProperties: false + description: GetStatusPageOverviewRequest requests the full overview of a status page by id. + openstatus.status_page.v1.GetStatusPageOverviewResponse: + type: object + properties: + statusPage: + title: status_page + description: The status page details. + $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage' + configuration: + title: configuration + description: Rich rendering configuration of the page. + $ref: '#/components/schemas/openstatus.status_page.v1.PageConfiguration' + components: + type: array + items: + $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent' + title: components + description: Components on the status page. + groups: + type: array + items: + $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup' + title: groups + description: Component groups on the status page. + statusReports: + type: array + items: + $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport' + title: status_reports + description: Active and recent status reports. + maintenances: + type: array + items: + $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary' + title: maintenances + description: Scheduled maintenances. + overallStatus: + title: overall_status + description: Aggregated status across all components. + $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus' + componentStatuses: + type: array + items: + $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus' + title: component_statuses + description: Status of individual components. + title: GetStatusPageOverviewResponse + additionalProperties: false + description: GetStatusPageOverviewResponse bundles all data for a single status page. openstatus.status_page.v1.GetStatusPageRequest: type: object properties: @@ -3156,6 +3215,14 @@ components: - PAGE_ACCESS_TYPE_AUTHENTICATED - PAGE_ACCESS_TYPE_IP_RESTRICTED description: PageAccessType defines who can access the status page. + openstatus.status_page.v1.PageBarType: + type: string + title: PageBarType + enum: + - PAGE_BAR_TYPE_UNSPECIFIED + - PAGE_BAR_TYPE_ABSOLUTE + - PAGE_BAR_TYPE_MANUAL + description: PageBarType mirrors page.configuration.type (how the status bar is computed). openstatus.status_page.v1.PageComponent: type: object properties: @@ -3282,6 +3349,44 @@ components: - PAGE_COMPONENT_TYPE_MONITOR - PAGE_COMPONENT_TYPE_STATIC description: PageComponentType defines the type of a component on a status page. + openstatus.status_page.v1.PageConfiguration: + type: object + properties: + metricType: + title: metric_type + description: Which metric the status bars represent (configuration.value). + $ref: '#/components/schemas/openstatus.status_page.v1.PageMetricType' + barType: + title: bar_type + description: How the status bar is computed (configuration.type). + $ref: '#/components/schemas/openstatus.status_page.v1.PageBarType' + showUptime: + type: boolean + title: show_uptime + description: Whether to show the uptime percentage (configuration.uptime). + themeKey: + type: string + title: theme_key + description: |- + Theme key from the theme store (configuration.theme), e.g. "default". + Free-form string rather than an enum because the theme catalog is dynamic. + days: + type: integer + title: days + format: int32 + description: 'Number of uptime-bar days rendered on the page (configuration.days): 30 or 45.' + title: PageConfiguration + additionalProperties: false + description: PageConfiguration is the rich rendering config stored in page.configuration. + openstatus.status_page.v1.PageMetricType: + type: string + title: PageMetricType + enum: + - PAGE_METRIC_TYPE_UNSPECIFIED + - PAGE_METRIC_TYPE_DURATION + - PAGE_METRIC_TYPE_REQUESTS + - PAGE_METRIC_TYPE_MANUAL + description: PageMetricType mirrors page.configuration.value (which metric the status bars represent). openstatus.status_page.v1.PageSubscriber: type: object properties: @@ -5664,6 +5769,58 @@ paths: application/json: schema: $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse' + /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageOverview: + get: + tags: + - StatusPageService + summary: GetStatusPageOverview + description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.' + operationId: StatusPageService_GetStatusPageOverview.get + parameters: + - name: message + in: query + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest' + responses: + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/connect.error' + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse' + post: + tags: + - StatusPageService + summary: GetStatusPageOverview + description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.' + operationId: StatusPageService_GetStatusPageOverview + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest' + required: true + responses: + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/connect.error' + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse' /rpc/openstatus.status_page.v1.StatusPageService/ListStatusPages: get: tags: diff --git a/packages/proto/gen/ts/openstatus/status_page/v1/service_pb.ts b/packages/proto/gen/ts/openstatus/status_page/v1/service_pb.ts index 19ab4e6d6..16ca78464 100644 --- a/packages/proto/gen/ts/openstatus/status_page/v1/service_pb.ts +++ b/packages/proto/gen/ts/openstatus/status_page/v1/service_pb.ts @@ -12,7 +12,7 @@ import type { PageComponent, PageComponentGroup, PageComponentType } from "./pag import { file_openstatus_status_page_v1_page_component } from "./page_component_pb.ts"; import type { PageSubscriber } from "./page_subscriber_pb.ts"; import { file_openstatus_status_page_v1_page_subscriber } from "./page_subscriber_pb.ts"; -import type { CustomTheme, Locale, OverallStatus, PageAccessType, PageTheme, StatusPage, StatusPageSummary } from "./status_page_pb.ts"; +import type { CustomTheme, Locale, OverallStatus, PageAccessType, PageConfiguration, PageTheme, StatusPage, StatusPageSummary } from "./status_page_pb.ts"; import { file_openstatus_status_page_v1_status_page } from "./status_page_pb.ts"; import type { PageComponentImpact, StatusReport } from "../../status_report/v1/status_report_pb.ts"; import { file_openstatus_status_report_v1_status_report } from "../../status_report/v1/status_report_pb.ts"; @@ -22,7 +22,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/status_page/v1/service.proto. */ export const file_openstatus_status_page_v1_service: GenFile = /*@__PURE__*/ - fileDesc("CidvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3NlcnZpY2UucHJvdG8SGW9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEivgcKF0NyZWF0ZVN0YXR1c1BhZ2VSZXF1ZXN0EjAKBXRpdGxlGAEgASgJQiG6RxQ6EhIQQWNtZSBDb3JwIFN0YXR1c7pIB3IFEAEYgAISIgoLZGVzY3JpcHRpb24YAiABKAlCCLpIBXIDGIAISACIAQESSQoEc2x1ZxgDIAEoCUI7ukcSOhASDm15LXN0YXR1cy1wYWdlukgjciEQARiAAjIaXlthLXowLTldKyg/Oi1bYS16MC05XSspKiQSOQoMaG9tZXBhZ2VfdXJsGAQgASgJQh66Rxs6GRIXaHR0cHM6Ly93d3cuZXhhbXBsZS5jb21IAYgBARIYCgtjb250YWN0X3VybBgFIAEoCUgCiAEBEj4KDmRlZmF1bHRfbG9jYWxlGAYgASgOMiEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5Mb2NhbGVIA4gBARIyCgdsb2NhbGVzGAcgAygOMiEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5Mb2NhbGUSGwoEaWNvbhgIIAEoCUIIukgFcgMYgAhIBIgBARIkCg1jdXN0b21fZG9tYWluGAkgASgJQgi6SAVyAxiAAkgFiAEBEjgKBXRoZW1lGAogASgOMiQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlVGhlbWVIBogBARJDCgthY2Nlc3NfdHlwZRgLIAEoDjIpLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUFjY2Vzc1R5cGVIB4gBARIhCghwYXNzd29yZBgMIAEoCUIKukgHcgUQARiAAkgIiAEBEhoKEmF1dGhfZW1haWxfZG9tYWlucxgNIAMoCRIYCgthbGxvd19pbmRleBgOIAEoCEgJiAEBEh4KEWFsbG93ZWRfaXBfcmFuZ2VzGA8gASgJSAqIAQESQQoMY3VzdG9tX3RoZW1lGBAgASgLMiYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DdXN0b21UaGVtZUgLiAEBQg4KDF9kZXNjcmlwdGlvbkIPCg1faG9tZXBhZ2VfdXJsQg4KDF9jb250YWN0X3VybEIRCg9fZGVmYXVsdF9sb2NhbGVCBwoFX2ljb25CEAoOX2N1c3RvbV9kb21haW5CCAoGX3RoZW1lQg4KDF9hY2Nlc3NfdHlwZUILCglfcGFzc3dvcmRCDgoMX2FsbG93X2luZGV4QhQKEl9hbGxvd2VkX2lwX3Jhbmdlc0IPCg1fY3VzdG9tX3RoZW1lIlYKGENyZWF0ZVN0YXR1c1BhZ2VSZXNwb25zZRI6CgtzdGF0dXNfcGFnZRgBIAEoCzIlLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuU3RhdHVzUGFnZSIrChRHZXRTdGF0dXNQYWdlUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASJTChVHZXRTdGF0dXNQYWdlUmVzcG9uc2USOgoLc3RhdHVzX3BhZ2UYASABKAsyJS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN0YXR1c1BhZ2UiagoWTGlzdFN0YXR1c1BhZ2VzUmVxdWVzdBIdCgVsaW1pdBgBIAEoBUIJukgGGgQYZCgBSACIAQESHAoGb2Zmc2V0GAIgASgFQge6SAQaAigASAGIAQFCCAoGX2xpbWl0QgkKB19vZmZzZXQicQoXTGlzdFN0YXR1c1BhZ2VzUmVzcG9uc2USQgoMc3RhdHVzX3BhZ2VzGAEgAygLMiwub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5TdGF0dXNQYWdlU3VtbWFyeRISCgp0b3RhbF9zaXplGAIgASgFIqQHChdVcGRhdGVTdGF0dXNQYWdlUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQARIeCgV0aXRsZRgCIAEoCUIKukgHcgUQARiAAkgAiAEBEiIKC2Rlc2NyaXB0aW9uGAMgASgJQgi6SAVyAxiACEgBiAEBEjkKBHNsdWcYBCABKAlCJrpII3IhEAEYgAIyGl5bYS16MC05XSsoPzotW2EtejAtOV0rKSokSAKIAQESGQoMaG9tZXBhZ2VfdXJsGAUgASgJSAOIAQESGAoLY29udGFjdF91cmwYBiABKAlIBIgBARI+Cg5kZWZhdWx0X2xvY2FsZRgHIAEoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlSAWIAQESMgoHbG9jYWxlcxgIIAMoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlEhsKBGljb24YCSABKAlCCLpIBXIDGIAISAaIAQESJAoNY3VzdG9tX2RvbWFpbhgKIAEoCUIIukgFcgMYgAJIB4gBARI4CgV0aGVtZRgLIAEoDjIkLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZVRoZW1lSAiIAQESQwoLYWNjZXNzX3R5cGUYDCABKA4yKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VBY2Nlc3NUeXBlSAmIAQESIQoIcGFzc3dvcmQYDSABKAlCCrpIB3IFEAEYgAJICogBARIaChJhdXRoX2VtYWlsX2RvbWFpbnMYDiADKAkSGAoLYWxsb3dfaW5kZXgYDyABKAhIC4gBARIeChFhbGxvd2VkX2lwX3JhbmdlcxgQIAEoCUgMiAEBEkEKDGN1c3RvbV90aGVtZRgRIAEoCzImLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ3VzdG9tVGhlbWVIDYgBAUIICgZfdGl0bGVCDgoMX2Rlc2NyaXB0aW9uQgcKBV9zbHVnQg8KDV9ob21lcGFnZV91cmxCDgoMX2NvbnRhY3RfdXJsQhEKD19kZWZhdWx0X2xvY2FsZUIHCgVfaWNvbkIQCg5fY3VzdG9tX2RvbWFpbkIICgZfdGhlbWVCDgoMX2FjY2Vzc190eXBlQgsKCV9wYXNzd29yZEIOCgxfYWxsb3dfaW5kZXhCFAoSX2FsbG93ZWRfaXBfcmFuZ2VzQg8KDV9jdXN0b21fdGhlbWUiVgoYVXBkYXRlU3RhdHVzUGFnZVJlc3BvbnNlEjoKC3N0YXR1c19wYWdlGAEgASgLMiUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5TdGF0dXNQYWdlIi4KF0RlbGV0ZVN0YXR1c1BhZ2VSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIisKGERlbGV0ZVN0YXR1c1BhZ2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIu8BChpBZGRNb25pdG9yQ29tcG9uZW50UmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhsKCm1vbml0b3JfaWQYAiABKAlCB7pIBHICEAESGwoEbmFtZRgDIAEoCUIIukgFcgMYgAJIAIgBARIiCgtkZXNjcmlwdGlvbhgEIAEoCUIIukgFcgMYgAhIAYgBARISCgVvcmRlchgFIAEoBUgCiAEBEhUKCGdyb3VwX2lkGAYgASgJSAOIAQFCBwoFX25hbWVCDgoMX2Rlc2NyaXB0aW9uQggKBl9vcmRlckILCglfZ3JvdXBfaWQiWgobQWRkTW9uaXRvckNvbXBvbmVudFJlc3BvbnNlEjsKCWNvbXBvbmVudBgBIAEoCzIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUNvbXBvbmVudCLFAQoZQWRkU3RhdGljQ29tcG9uZW50UmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEYgAISIgoLZGVzY3JpcHRpb24YAyABKAlCCLpIBXIDGIAISACIAQESEgoFb3JkZXIYBCABKAVIAYgBARIVCghncm91cF9pZBgFIAEoCUgCiAEBQg4KDF9kZXNjcmlwdGlvbkIICgZfb3JkZXJCCwoJX2dyb3VwX2lkIlkKGkFkZFN0YXRpY0NvbXBvbmVudFJlc3BvbnNlEjsKCWNvbXBvbmVudBgBIAEoCzIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUNvbXBvbmVudCItChZSZW1vdmVDb21wb25lbnRSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIioKF1JlbW92ZUNvbXBvbmVudFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgi8wEKFlVwZGF0ZUNvbXBvbmVudFJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESGwoEbmFtZRgCIAEoCUIIukgFcgMYgAJIAIgBARIiCgtkZXNjcmlwdGlvbhgDIAEoCUIIukgFcgMYgAhIAYgBARISCgVvcmRlchgEIAEoBUgCiAEBEhUKCGdyb3VwX2lkGAUgASgJSAOIAQESGAoLZ3JvdXBfb3JkZXIYBiABKAVIBIgBAUIHCgVfbmFtZUIOCgxfZGVzY3JpcHRpb25CCAoGX29yZGVyQgsKCV9ncm91cF9pZEIOCgxfZ3JvdXBfb3JkZXIiVgoXVXBkYXRlQ29tcG9uZW50UmVzcG9uc2USOwoJY29tcG9uZW50GAEgASgLMigub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50In0KG0NyZWF0ZUNvbXBvbmVudEdyb3VwUmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEYgAISGQoMZGVmYXVsdF9vcGVuGAMgASgISACIAQFCDwoNX2RlZmF1bHRfb3BlbiJcChxDcmVhdGVDb21wb25lbnRHcm91cFJlc3BvbnNlEjwKBWdyb3VwGAEgASgLMi0ub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50R3JvdXAiMgobRGVsZXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIi8KHERlbGV0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCKGAQobVXBkYXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABEh0KBG5hbWUYAiABKAlCCrpIB3IFEAEYgAJIAIgBARIZCgxkZWZhdWx0X29wZW4YAyABKAhIAYgBAUIHCgVfbmFtZUIPCg1fZGVmYXVsdF9vcGVuIlwKHFVwZGF0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2USPAoFZ3JvdXAYASABKAsyLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnRHcm91cCJhChZTdWJzY3JpYmVUb1BhZ2VSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESLQoFZW1haWwYAiABKAlCHrpHFDoSEhB1c2VyQGV4YW1wbGUuY29tukgEcgJgASJYChdTdWJzY3JpYmVUb1BhZ2VSZXNwb25zZRI9CgpzdWJzY3JpYmVyGAEgASgLMikub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlU3Vic2NyaWJlciImCgxFbWFpbENoYW5uZWwSFgoFZW1haWwYASABKAlCB7pIBHICYAEiOwoUV2ViaG9va0NoYW5uZWxIZWFkZXISFAoDa2V5GAEgASgJQge6SARyAhABEg0KBXZhbHVlGAIgASgJInYKDldlYmhvb2tDaGFubmVsEiIKC3dlYmhvb2tfdXJsGAEgASgJQg26SApyCBABGIAQiAEBEkAKB2hlYWRlcnMYAiADKAsyLy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLldlYmhvb2tDaGFubmVsSGVhZGVyIpACCh1DcmVhdGVQYWdlU3Vic2NyaXB0aW9uUmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhsKBG5hbWUYAiABKAlCCLpIBXIDGP8BSAGIAQESFQoNY29tcG9uZW50X2lkcxgDIAMoCRJACg1lbWFpbF9jaGFubmVsGAogASgLMicub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5FbWFpbENoYW5uZWxIABJECg93ZWJob29rX2NoYW5uZWwYCyABKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLldlYmhvb2tDaGFubmVsSABCEAoHY2hhbm5lbBIFukgCCAFCBwoFX25hbWUiXwoeQ3JlYXRlUGFnZVN1YnNjcmlwdGlvblJlc3BvbnNlEj0KCnN1YnNjcmliZXIYASABKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VTdWJzY3JpYmVyImMKGlVuc3Vic2NyaWJlRnJvbVBhZ2VSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESDwoFZW1haWwYAiABKAlIABIMCgJpZBgDIAEoCUgAQgwKCmlkZW50aWZpZXIiLgobVW5zdWJzY3JpYmVGcm9tUGFnZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiwAEKFkxpc3RTdWJzY3JpYmVyc1JlcXVlc3QSGAoHcGFnZV9pZBgBIAEoCUIHukgEcgIQARIdCgVsaW1pdBgCIAEoBUIJukgGGgQYZCgBSACIAQESHAoGb2Zmc2V0GAMgASgFQge6SAQaAigASAGIAQESIQoUaW5jbHVkZV91bnN1YnNjcmliZWQYBCABKAhIAogBAUIICgZfbGltaXRCCQoHX29mZnNldEIXChVfaW5jbHVkZV91bnN1YnNjcmliZWQibQoXTGlzdFN1YnNjcmliZXJzUmVzcG9uc2USPgoLc3Vic2NyaWJlcnMYASADKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VTdWJzY3JpYmVyEhIKCnRvdGFsX3NpemUYAiABKAUiSQobR2V0U3RhdHVzUGFnZUNvbnRlbnRSZXF1ZXN0EgwKAmlkGAEgASgJSAASDgoEc2x1ZxgCIAEoCUgAQgwKCmlkZW50aWZpZXIi3wIKHEdldFN0YXR1c1BhZ2VDb250ZW50UmVzcG9uc2USOgoLc3RhdHVzX3BhZ2UYASABKAsyJS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN0YXR1c1BhZ2USPAoKY29tcG9uZW50cxgCIAMoCzIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUNvbXBvbmVudBI9CgZncm91cHMYAyADKAsyLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnRHcm91cBJBCg5zdGF0dXNfcmVwb3J0cxgEIAMoCzIpLm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5TdGF0dXNSZXBvcnQSQwoMbWFpbnRlbmFuY2VzGAUgAygLMi0ub3BlbnN0YXR1cy5tYWludGVuYW5jZS52MS5NYWludGVuYW5jZVN1bW1hcnkiRQoXR2V0T3ZlcmFsbFN0YXR1c1JlcXVlc3QSDAoCaWQYASABKAlIABIOCgRzbHVnGAIgASgJSABCDAoKaWRlbnRpZmllciJhCg9Db21wb25lbnRTdGF0dXMSFAoMY29tcG9uZW50X2lkGAEgASgJEjgKBnN0YXR1cxgCIAEoDjIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuT3ZlcmFsbFN0YXR1cyKkAQoYR2V0T3ZlcmFsbFN0YXR1c1Jlc3BvbnNlEkAKDm92ZXJhbGxfc3RhdHVzGAEgASgOMigub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5PdmVyYWxsU3RhdHVzEkYKEmNvbXBvbmVudF9zdGF0dXNlcxgCIAMoCzIqLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ29tcG9uZW50U3RhdHVzIp0BCiNHZXRQYWdlQ29tcG9uZW50RGFpbHlTdW1tYXJ5UmVxdWVzdBIMCgJpZBgBIAEoCUgAEg4KBHNsdWcYAiABKAlIABIjCg1jb21wb25lbnRfaWRzGAMgAygJQgy6SAmSAQYiBHICEAESHAoEZGF5cxgEIAEoBUIJukgGGgQYLSgBSAGIAQFCDAoKaWRlbnRpZmllckIHCgVfZGF5cyLuAQoSQ29tcG9uZW50RGF5QnVja2V0EgsKA2RheRgBIAEoCRINCgVjb3VudBgCIAEoAxIKCgJvaxgDIAEoAxIQCghkZWdyYWRlZBgEIAEoAxINCgVlcnJvchgFIAEoAxI9CgZzdGF0dXMYBiABKA4yLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNvbXBvbmVudERheVN0YXR1cxJFCgZpbXBhY3QYByABKA4yMC5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuUGFnZUNvbXBvbmVudEltcGFjdEgAiAEBQgkKB19pbXBhY3QioAIKDkNvbXBvbmVudEV2ZW50EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSOwoEdHlwZRgDIAEoDjItLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ29tcG9uZW50RXZlbnRUeXBlEj8KBnN0YXR1cxgEIAEoDjIvLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ29tcG9uZW50RXZlbnRTdGF0dXMSDAoEZnJvbRgFIAEoCRIPCgJ0bxgGIAEoCUgAiAEBEkUKBmltcGFjdBgHIAEoDjIwLm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5QYWdlQ29tcG9uZW50SW1wYWN0SAGIAQFCBQoDX3RvQgkKB19pbXBhY3QingIKGVBhZ2VDb21wb25lbnREYWlseVN1bW1hcnkSFAoMY29tcG9uZW50X2lkGAEgASgJEjoKBHR5cGUYAiABKA4yLC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnRUeXBlEhcKCm1vbml0b3JfaWQYAyABKAlIAIgBARIMCgRuYW1lGAQgASgJEj4KB2J1Y2tldHMYBSADKAsyLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNvbXBvbmVudERheUJ1Y2tldBI5CgZldmVudHMYBiADKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNvbXBvbmVudEV2ZW50Qg0KC19tb25pdG9yX2lkInAKJEdldFBhZ2VDb21wb25lbnREYWlseVN1bW1hcnlSZXNwb25zZRJICgpjb21wb25lbnRzGAEgAygLMjQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50RGFpbHlTdW1tYXJ5KugBChJDb21wb25lbnREYXlTdGF0dXMSJAogQ09NUE9ORU5UX0RBWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIkCiBDT01QT05FTlRfREFZX1NUQVRVU19PUEVSQVRJT05BTBABEiEKHUNPTVBPTkVOVF9EQVlfU1RBVFVTX0RFR1JBREVEEAISHQoZQ09NUE9ORU5UX0RBWV9TVEFUVVNfRE9XThADEiQKIENPTVBPTkVOVF9EQVlfU1RBVFVTX01BSU5URU5BTkNFEAQSHgoaQ09NUE9ORU5UX0RBWV9TVEFUVVNfRU1QVFkQBSqkAQoSQ29tcG9uZW50RXZlbnRUeXBlEiQKIENPTVBPTkVOVF9FVkVOVF9UWVBFX1VOU1BFQ0lGSUVEEAASJAogQ09NUE9ORU5UX0VWRU5UX1RZUEVfTUFJTlRFTkFOQ0UQARIhCh1DT01QT05FTlRfRVZFTlRfVFlQRV9JTkNJREVOVBACEh8KG0NPTVBPTkVOVF9FVkVOVF9UWVBFX1JFUE9SVBADKtQBChRDb21wb25lbnRFdmVudFN0YXR1cxImCiJDT01QT05FTlRfRVZFTlRfU1RBVFVTX1VOU1BFQ0lGSUVEEAASJgoiQ09NUE9ORU5UX0VWRU5UX1NUQVRVU19PUEVSQVRJT05BTBABEiMKH0NPTVBPTkVOVF9FVkVOVF9TVEFUVVNfREVHUkFERUQQAhIfChtDT01QT05FTlRfRVZFTlRfU1RBVFVTX0RPV04QAxImCiJDT01QT05FTlRfRVZFTlRfU1RBVFVTX01BSU5URU5BTkNFEAQyqSMKEVN0YXR1c1BhZ2VTZXJ2aWNlEnsKEENyZWF0ZVN0YXR1c1BhZ2USMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNyZWF0ZVN0YXR1c1BhZ2VSZXF1ZXN0GjMub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DcmVhdGVTdGF0dXNQYWdlUmVzcG9uc2USdwoNR2V0U3RhdHVzUGFnZRIvLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZVJlcXVlc3QaMC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldFN0YXR1c1BhZ2VSZXNwb25zZSIDkAIBEn0KD0xpc3RTdGF0dXNQYWdlcxIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN0YXR1c1BhZ2VzUmVxdWVzdBoyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN0YXR1c1BhZ2VzUmVzcG9uc2UiA5ACARJ7ChBVcGRhdGVTdGF0dXNQYWdlEjIub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VcGRhdGVTdGF0dXNQYWdlUmVxdWVzdBozLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVXBkYXRlU3RhdHVzUGFnZVJlc3BvbnNlEnsKEERlbGV0ZVN0YXR1c1BhZ2USMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkRlbGV0ZVN0YXR1c1BhZ2VSZXF1ZXN0GjMub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5EZWxldGVTdGF0dXNQYWdlUmVzcG9uc2UShAEKE0FkZE1vbml0b3JDb21wb25lbnQSNS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkFkZE1vbml0b3JDb21wb25lbnRSZXF1ZXN0GjYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5BZGRNb25pdG9yQ29tcG9uZW50UmVzcG9uc2USgQEKEkFkZFN0YXRpY0NvbXBvbmVudBI0Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQWRkU3RhdGljQ29tcG9uZW50UmVxdWVzdBo1Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQWRkU3RhdGljQ29tcG9uZW50UmVzcG9uc2USeAoPUmVtb3ZlQ29tcG9uZW50EjEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5SZW1vdmVDb21wb25lbnRSZXF1ZXN0GjIub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5SZW1vdmVDb21wb25lbnRSZXNwb25zZRJ4Cg9VcGRhdGVDb21wb25lbnQSMS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlVwZGF0ZUNvbXBvbmVudFJlcXVlc3QaMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlVwZGF0ZUNvbXBvbmVudFJlc3BvbnNlEocBChRDcmVhdGVDb21wb25lbnRHcm91cBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ3JlYXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DcmVhdGVDb21wb25lbnRHcm91cFJlc3BvbnNlEocBChREZWxldGVDb21wb25lbnRHcm91cBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuRGVsZXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5EZWxldGVDb21wb25lbnRHcm91cFJlc3BvbnNlEocBChRVcGRhdGVDb21wb25lbnRHcm91cBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVXBkYXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VcGRhdGVDb21wb25lbnRHcm91cFJlc3BvbnNlErkECg9TdWJzY3JpYmVUb1BhZ2USMS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN1YnNjcmliZVRvUGFnZVJlcXVlc3QaMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN1YnNjcmliZVRvUGFnZVJlc3BvbnNlIr4Duke6Axq3A0VuZC11c2VyIGVtYWlsIHNlbGYtc2lnbnVwIHdpdGggZG91YmxlIG9wdC1pbiB2ZXJpZmljYXRpb24uIEEgdmVyaWZpY2F0aW9uIGVtYWlsIGlzIHNlbnQgYW5kIHRoZSBzdWJzY3JpcHRpb24gYWN0aXZhdGVzIG9ubHkgYWZ0ZXIgdGhlIHJlY2lwaWVudCBjb25maXJtcy4gSWYgdGhlIGVtYWlsIHdhcyBwcmV2aW91c2x5IHVuc3Vic2NyaWJlZCwgdGhlIGV4aXN0aW5nIHJvdyBpcyByZWFjdGl2YXRlZCBpbnN0ZWFkIG9mIGEgZHVwbGljYXRlIGJlaW5nIGNyZWF0ZWQuIFVzZSB0aGlzIGZvciBzdWJzY3JpYmVyLWluaXRpYXRlZCBzaWdudXBzIG9uIHRoZSBwdWJsaWMgc3RhdHVzIHBhZ2U7IHVzZSBDcmVhdGVQYWdlU3Vic2NyaXB0aW9uIGZvciBvcGVyYXRvci1pbml0aWF0ZWQgKHZlbmRvcikgc3Vic2NyaXB0aW9ucyB0aGF0IHNob3VsZCBza2lwIHZlcmlmaWNhdGlvbi4S/QQKFkNyZWF0ZVBhZ2VTdWJzY3JpcHRpb24SOC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNyZWF0ZVBhZ2VTdWJzY3JpcHRpb25SZXF1ZXN0Gjkub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DcmVhdGVQYWdlU3Vic2NyaXB0aW9uUmVzcG9uc2Ui7QO6R+kDGuYDT3BlcmF0b3ItYWRkZWQgc3Vic2NyaWJlciAoZW1haWwgb3Igd2ViaG9vaykgd2l0aCBubyB2ZXJpZmljYXRpb24gZmxvdyDigJQgdGhlIHBhcnRuZXIgc3RhcnRzIHJlY2VpdmluZyBub3RpZmljYXRpb25zIGltbWVkaWF0ZWx5LiBTdXBwb3J0cyBTbGFjayBhbmQgRGlzY29yZCB3ZWJob29rIFVSTHMgaW4gYWRkaXRpb24gdG8gZW1haWwuIEEgbWFuYWdlbWVudCB0b2tlbiBpcyBzdGlsbCBnZW5lcmF0ZWQgc28gdGhlIHN1YnNjcmliZXIgY2FuIHNlbGYtbWFuYWdlICh1cGRhdGUgc2NvcGUsIHVuc3Vic2NyaWJlKSB3aXRob3V0IG9wZXJhdG9yIGludm9sdmVtZW50LiBVc2UgdGhpcyBmb3IgdmVuZG9yL3BhcnRuZXIgaW50ZWdyYXRpb25zIHdoZXJlIGNvbnNlbnQgaXMgZXN0YWJsaXNoZWQgb3V0LW9mLWJhbmQ7IHVzZSBTdWJzY3JpYmVUb1BhZ2UgZm9yIHN1YnNjcmliZXItaW5pdGlhdGVkIHNpZ251cHMgdGhhdCByZXF1aXJlIGRvdWJsZSBvcHQtaW4uEoQBChNVbnN1YnNjcmliZUZyb21QYWdlEjUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VbnN1YnNjcmliZUZyb21QYWdlUmVxdWVzdBo2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVW5zdWJzY3JpYmVGcm9tUGFnZVJlc3BvbnNlEn0KD0xpc3RTdWJzY3JpYmVycxIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN1YnNjcmliZXJzUmVxdWVzdBoyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN1YnNjcmliZXJzUmVzcG9uc2UiA5ACARLAAwoUR2V0U3RhdHVzUGFnZUNvbnRlbnQSNi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldFN0YXR1c1BhZ2VDb250ZW50UmVxdWVzdBo3Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZUNvbnRlbnRSZXNwb25zZSK2ApACAbpHrwIarAJSZXR1cm5zIHRoZSBmdWxsIGNvbnRlbnQgb2YgYSBzdGF0dXMgcGFnZSBpbmNsdWRpbmcgaXRzIGNvbXBvbmVudHMsIGNvbXBvbmVudCBncm91cHMsIGFjdGl2ZSBzdGF0dXMgcmVwb3J0cywgYW5kIHNjaGVkdWxlZCBtYWludGVuYW5jZXMuIFN1cHBvcnRzIHR3byBhY2Nlc3MgcGF0aHM6IGJ5IElEIChyZXF1aXJlcyBhdXRoZW50aWNhdGlvbiwgd29ya3NwYWNlLXNjb3BlZCkgb3IgYnkgc2x1ZyAocHVibGljIGFjY2VzcywgcmVxdWlyZXMgdGhlIHBhZ2UgdG8gYmUgcHVibGlzaGVkIHdpdGggYWNjZXNzX3R5cGU9UFVCTElDKS4SqgMKEEdldE92ZXJhbGxTdGF0dXMSMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldE92ZXJhbGxTdGF0dXNSZXF1ZXN0GjMub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5HZXRPdmVyYWxsU3RhdHVzUmVzcG9uc2UirAKQAgG6R6UCGqICUmV0dXJucyB0aGUgb3ZlcmFsbCBzdGF0dXMgb2YgYSBzdGF0dXMgcGFnZSBhbG9uZyB3aXRoIGluZGl2aWR1YWwgY29tcG9uZW50IHN0YXR1c2VzLiBUaGUgb3ZlcmFsbCBzdGF0dXMgaXMgY29tcHV0ZWQgZnJvbSBhY3RpdmUgc3RhdHVzIHJlcG9ydHMgYW5kIG1haW50ZW5hbmNlcyB3aXRoIHRoZSBmb2xsb3dpbmcgcHJpb3JpdHk6IGRlZ3JhZGVkIChmcm9tIGFjdGl2ZSBzdGF0dXMgcmVwb3J0cykgPiBtYWludGVuYW5jZSAoZnJvbSBhY3RpdmUgbWFpbnRlbmFuY2Ugd2luZG93cykgPiBvcGVyYXRpb25hbC4S1QQKHEdldFBhZ2VDb21wb25lbnREYWlseVN1bW1hcnkSPi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldFBhZ2VDb21wb25lbnREYWlseVN1bW1hcnlSZXF1ZXN0Gj8ub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5HZXRQYWdlQ29tcG9uZW50RGFpbHlTdW1tYXJ5UmVzcG9uc2UiswOQAgG6R6wDGqkDUmV0dXJucyBwZXItY29tcG9uZW50IGRhaWx5IHN0YXR1cyBidWNrZXRzIChvay9kZWdyYWRlZC9lcnJvci9jb3VudCBwbHVzIGEgcmVzb2x2ZWQgc3RhdHVzKSBtZXJnZWQgd2l0aCB0aGUgaW5jaWRlbnQsIG1haW50ZW5hbmNlLCBhbmQgc3RhdHVzLXJlcG9ydCB0aW1lbGluZSBvdmVyIHRoZSBsYXN0IE4gZGF5cyAobWF4IDQ1KS4gU3VpdGFibGUgYXMgYSBzaW5nbGUgc291cmNlIG9mIHRydXRoIGZvciBzdGF0dXMtYmFyIGFuZCB1cHRpbWUtY2FsZW5kYXIgcmVuZGVyaW5nLiBTdXBwb3J0cyB0d28gYWNjZXNzIHBhdGhzOiBieSBpZCAoYXV0aGVudGljYXRlZCwgd29ya3NwYWNlLXNjb3BlZCkgb3IgYnkgc2x1ZyAocHVibGljIGFjY2VzcywgcmVxdWlyZXMgdGhlIHBhZ2UgdG8gYmUgcHVibGlzaGVkIHdpdGggYWNjZXNzX3R5cGU9UFVCTElDKS5CWlpYZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxO3N0YXR1c3BhZ2V2MWIGcHJvdG8z", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_maintenance_v1_maintenance, file_openstatus_status_page_v1_page_component, file_openstatus_status_page_v1_page_subscriber, file_openstatus_status_page_v1_status_page, file_openstatus_status_report_v1_status_report]); + fileDesc("CidvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3NlcnZpY2UucHJvdG8SGW9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEivgcKF0NyZWF0ZVN0YXR1c1BhZ2VSZXF1ZXN0EjAKBXRpdGxlGAEgASgJQiG6RxQ6EhIQQWNtZSBDb3JwIFN0YXR1c7pIB3IFEAEYgAISIgoLZGVzY3JpcHRpb24YAiABKAlCCLpIBXIDGIAISACIAQESSQoEc2x1ZxgDIAEoCUI7ukcSOhASDm15LXN0YXR1cy1wYWdlukgjciEQARiAAjIaXlthLXowLTldKyg/Oi1bYS16MC05XSspKiQSOQoMaG9tZXBhZ2VfdXJsGAQgASgJQh66Rxs6GRIXaHR0cHM6Ly93d3cuZXhhbXBsZS5jb21IAYgBARIYCgtjb250YWN0X3VybBgFIAEoCUgCiAEBEj4KDmRlZmF1bHRfbG9jYWxlGAYgASgOMiEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5Mb2NhbGVIA4gBARIyCgdsb2NhbGVzGAcgAygOMiEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5Mb2NhbGUSGwoEaWNvbhgIIAEoCUIIukgFcgMYgAhIBIgBARIkCg1jdXN0b21fZG9tYWluGAkgASgJQgi6SAVyAxiAAkgFiAEBEjgKBXRoZW1lGAogASgOMiQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlVGhlbWVIBogBARJDCgthY2Nlc3NfdHlwZRgLIAEoDjIpLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUFjY2Vzc1R5cGVIB4gBARIhCghwYXNzd29yZBgMIAEoCUIKukgHcgUQARiAAkgIiAEBEhoKEmF1dGhfZW1haWxfZG9tYWlucxgNIAMoCRIYCgthbGxvd19pbmRleBgOIAEoCEgJiAEBEh4KEWFsbG93ZWRfaXBfcmFuZ2VzGA8gASgJSAqIAQESQQoMY3VzdG9tX3RoZW1lGBAgASgLMiYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DdXN0b21UaGVtZUgLiAEBQg4KDF9kZXNjcmlwdGlvbkIPCg1faG9tZXBhZ2VfdXJsQg4KDF9jb250YWN0X3VybEIRCg9fZGVmYXVsdF9sb2NhbGVCBwoFX2ljb25CEAoOX2N1c3RvbV9kb21haW5CCAoGX3RoZW1lQg4KDF9hY2Nlc3NfdHlwZUILCglfcGFzc3dvcmRCDgoMX2FsbG93X2luZGV4QhQKEl9hbGxvd2VkX2lwX3Jhbmdlc0IPCg1fY3VzdG9tX3RoZW1lIlYKGENyZWF0ZVN0YXR1c1BhZ2VSZXNwb25zZRI6CgtzdGF0dXNfcGFnZRgBIAEoCzIlLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuU3RhdHVzUGFnZSIrChRHZXRTdGF0dXNQYWdlUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASJTChVHZXRTdGF0dXNQYWdlUmVzcG9uc2USOgoLc3RhdHVzX3BhZ2UYASABKAsyJS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN0YXR1c1BhZ2UiagoWTGlzdFN0YXR1c1BhZ2VzUmVxdWVzdBIdCgVsaW1pdBgBIAEoBUIJukgGGgQYZCgBSACIAQESHAoGb2Zmc2V0GAIgASgFQge6SAQaAigASAGIAQFCCAoGX2xpbWl0QgkKB19vZmZzZXQicQoXTGlzdFN0YXR1c1BhZ2VzUmVzcG9uc2USQgoMc3RhdHVzX3BhZ2VzGAEgAygLMiwub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5TdGF0dXNQYWdlU3VtbWFyeRISCgp0b3RhbF9zaXplGAIgASgFIqQHChdVcGRhdGVTdGF0dXNQYWdlUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQARIeCgV0aXRsZRgCIAEoCUIKukgHcgUQARiAAkgAiAEBEiIKC2Rlc2NyaXB0aW9uGAMgASgJQgi6SAVyAxiACEgBiAEBEjkKBHNsdWcYBCABKAlCJrpII3IhEAEYgAIyGl5bYS16MC05XSsoPzotW2EtejAtOV0rKSokSAKIAQESGQoMaG9tZXBhZ2VfdXJsGAUgASgJSAOIAQESGAoLY29udGFjdF91cmwYBiABKAlIBIgBARI+Cg5kZWZhdWx0X2xvY2FsZRgHIAEoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlSAWIAQESMgoHbG9jYWxlcxgIIAMoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlEhsKBGljb24YCSABKAlCCLpIBXIDGIAISAaIAQESJAoNY3VzdG9tX2RvbWFpbhgKIAEoCUIIukgFcgMYgAJIB4gBARI4CgV0aGVtZRgLIAEoDjIkLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZVRoZW1lSAiIAQESQwoLYWNjZXNzX3R5cGUYDCABKA4yKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VBY2Nlc3NUeXBlSAmIAQESIQoIcGFzc3dvcmQYDSABKAlCCrpIB3IFEAEYgAJICogBARIaChJhdXRoX2VtYWlsX2RvbWFpbnMYDiADKAkSGAoLYWxsb3dfaW5kZXgYDyABKAhIC4gBARIeChFhbGxvd2VkX2lwX3JhbmdlcxgQIAEoCUgMiAEBEkEKDGN1c3RvbV90aGVtZRgRIAEoCzImLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ3VzdG9tVGhlbWVIDYgBAUIICgZfdGl0bGVCDgoMX2Rlc2NyaXB0aW9uQgcKBV9zbHVnQg8KDV9ob21lcGFnZV91cmxCDgoMX2NvbnRhY3RfdXJsQhEKD19kZWZhdWx0X2xvY2FsZUIHCgVfaWNvbkIQCg5fY3VzdG9tX2RvbWFpbkIICgZfdGhlbWVCDgoMX2FjY2Vzc190eXBlQgsKCV9wYXNzd29yZEIOCgxfYWxsb3dfaW5kZXhCFAoSX2FsbG93ZWRfaXBfcmFuZ2VzQg8KDV9jdXN0b21fdGhlbWUiVgoYVXBkYXRlU3RhdHVzUGFnZVJlc3BvbnNlEjoKC3N0YXR1c19wYWdlGAEgASgLMiUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5TdGF0dXNQYWdlIi4KF0RlbGV0ZVN0YXR1c1BhZ2VSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIisKGERlbGV0ZVN0YXR1c1BhZ2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIu8BChpBZGRNb25pdG9yQ29tcG9uZW50UmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhsKCm1vbml0b3JfaWQYAiABKAlCB7pIBHICEAESGwoEbmFtZRgDIAEoCUIIukgFcgMYgAJIAIgBARIiCgtkZXNjcmlwdGlvbhgEIAEoCUIIukgFcgMYgAhIAYgBARISCgVvcmRlchgFIAEoBUgCiAEBEhUKCGdyb3VwX2lkGAYgASgJSAOIAQFCBwoFX25hbWVCDgoMX2Rlc2NyaXB0aW9uQggKBl9vcmRlckILCglfZ3JvdXBfaWQiWgobQWRkTW9uaXRvckNvbXBvbmVudFJlc3BvbnNlEjsKCWNvbXBvbmVudBgBIAEoCzIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUNvbXBvbmVudCLFAQoZQWRkU3RhdGljQ29tcG9uZW50UmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEYgAISIgoLZGVzY3JpcHRpb24YAyABKAlCCLpIBXIDGIAISACIAQESEgoFb3JkZXIYBCABKAVIAYgBARIVCghncm91cF9pZBgFIAEoCUgCiAEBQg4KDF9kZXNjcmlwdGlvbkIICgZfb3JkZXJCCwoJX2dyb3VwX2lkIlkKGkFkZFN0YXRpY0NvbXBvbmVudFJlc3BvbnNlEjsKCWNvbXBvbmVudBgBIAEoCzIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUNvbXBvbmVudCItChZSZW1vdmVDb21wb25lbnRSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIioKF1JlbW92ZUNvbXBvbmVudFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgi8wEKFlVwZGF0ZUNvbXBvbmVudFJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESGwoEbmFtZRgCIAEoCUIIukgFcgMYgAJIAIgBARIiCgtkZXNjcmlwdGlvbhgDIAEoCUIIukgFcgMYgAhIAYgBARISCgVvcmRlchgEIAEoBUgCiAEBEhUKCGdyb3VwX2lkGAUgASgJSAOIAQESGAoLZ3JvdXBfb3JkZXIYBiABKAVIBIgBAUIHCgVfbmFtZUIOCgxfZGVzY3JpcHRpb25CCAoGX29yZGVyQgsKCV9ncm91cF9pZEIOCgxfZ3JvdXBfb3JkZXIiVgoXVXBkYXRlQ29tcG9uZW50UmVzcG9uc2USOwoJY29tcG9uZW50GAEgASgLMigub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50In0KG0NyZWF0ZUNvbXBvbmVudEdyb3VwUmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEYgAISGQoMZGVmYXVsdF9vcGVuGAMgASgISACIAQFCDwoNX2RlZmF1bHRfb3BlbiJcChxDcmVhdGVDb21wb25lbnRHcm91cFJlc3BvbnNlEjwKBWdyb3VwGAEgASgLMi0ub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50R3JvdXAiMgobRGVsZXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIi8KHERlbGV0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCKGAQobVXBkYXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABEh0KBG5hbWUYAiABKAlCCrpIB3IFEAEYgAJIAIgBARIZCgxkZWZhdWx0X29wZW4YAyABKAhIAYgBAUIHCgVfbmFtZUIPCg1fZGVmYXVsdF9vcGVuIlwKHFVwZGF0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2USPAoFZ3JvdXAYASABKAsyLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnRHcm91cCJhChZTdWJzY3JpYmVUb1BhZ2VSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESLQoFZW1haWwYAiABKAlCHrpHFDoSEhB1c2VyQGV4YW1wbGUuY29tukgEcgJgASJYChdTdWJzY3JpYmVUb1BhZ2VSZXNwb25zZRI9CgpzdWJzY3JpYmVyGAEgASgLMikub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlU3Vic2NyaWJlciImCgxFbWFpbENoYW5uZWwSFgoFZW1haWwYASABKAlCB7pIBHICYAEiOwoUV2ViaG9va0NoYW5uZWxIZWFkZXISFAoDa2V5GAEgASgJQge6SARyAhABEg0KBXZhbHVlGAIgASgJInYKDldlYmhvb2tDaGFubmVsEiIKC3dlYmhvb2tfdXJsGAEgASgJQg26SApyCBABGIAQiAEBEkAKB2hlYWRlcnMYAiADKAsyLy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLldlYmhvb2tDaGFubmVsSGVhZGVyIpACCh1DcmVhdGVQYWdlU3Vic2NyaXB0aW9uUmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEhsKBG5hbWUYAiABKAlCCLpIBXIDGP8BSAGIAQESFQoNY29tcG9uZW50X2lkcxgDIAMoCRJACg1lbWFpbF9jaGFubmVsGAogASgLMicub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5FbWFpbENoYW5uZWxIABJECg93ZWJob29rX2NoYW5uZWwYCyABKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLldlYmhvb2tDaGFubmVsSABCEAoHY2hhbm5lbBIFukgCCAFCBwoFX25hbWUiXwoeQ3JlYXRlUGFnZVN1YnNjcmlwdGlvblJlc3BvbnNlEj0KCnN1YnNjcmliZXIYASABKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VTdWJzY3JpYmVyImMKGlVuc3Vic2NyaWJlRnJvbVBhZ2VSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESDwoFZW1haWwYAiABKAlIABIMCgJpZBgDIAEoCUgAQgwKCmlkZW50aWZpZXIiLgobVW5zdWJzY3JpYmVGcm9tUGFnZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiwAEKFkxpc3RTdWJzY3JpYmVyc1JlcXVlc3QSGAoHcGFnZV9pZBgBIAEoCUIHukgEcgIQARIdCgVsaW1pdBgCIAEoBUIJukgGGgQYZCgBSACIAQESHAoGb2Zmc2V0GAMgASgFQge6SAQaAigASAGIAQESIQoUaW5jbHVkZV91bnN1YnNjcmliZWQYBCABKAhIAogBAUIICgZfbGltaXRCCQoHX29mZnNldEIXChVfaW5jbHVkZV91bnN1YnNjcmliZWQibQoXTGlzdFN1YnNjcmliZXJzUmVzcG9uc2USPgoLc3Vic2NyaWJlcnMYASADKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VTdWJzY3JpYmVyEhIKCnRvdGFsX3NpemUYAiABKAUiSQobR2V0U3RhdHVzUGFnZUNvbnRlbnRSZXF1ZXN0EgwKAmlkGAEgASgJSAASDgoEc2x1ZxgCIAEoCUgAQgwKCmlkZW50aWZpZXIi3wIKHEdldFN0YXR1c1BhZ2VDb250ZW50UmVzcG9uc2USOgoLc3RhdHVzX3BhZ2UYASABKAsyJS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN0YXR1c1BhZ2USPAoKY29tcG9uZW50cxgCIAMoCzIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZUNvbXBvbmVudBI9CgZncm91cHMYAyADKAsyLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnRHcm91cBJBCg5zdGF0dXNfcmVwb3J0cxgEIAMoCzIpLm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5TdGF0dXNSZXBvcnQSQwoMbWFpbnRlbmFuY2VzGAUgAygLMi0ub3BlbnN0YXR1cy5tYWludGVuYW5jZS52MS5NYWludGVuYW5jZVN1bW1hcnkiRQoXR2V0T3ZlcmFsbFN0YXR1c1JlcXVlc3QSDAoCaWQYASABKAlIABIOCgRzbHVnGAIgASgJSABCDAoKaWRlbnRpZmllciJhCg9Db21wb25lbnRTdGF0dXMSFAoMY29tcG9uZW50X2lkGAEgASgJEjgKBnN0YXR1cxgCIAEoDjIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuT3ZlcmFsbFN0YXR1cyKkAQoYR2V0T3ZlcmFsbFN0YXR1c1Jlc3BvbnNlEkAKDm92ZXJhbGxfc3RhdHVzGAEgASgOMigub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5PdmVyYWxsU3RhdHVzEkYKEmNvbXBvbmVudF9zdGF0dXNlcxgCIAMoCzIqLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ29tcG9uZW50U3RhdHVzIjMKHEdldFN0YXR1c1BhZ2VPdmVydmlld1JlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEirwQKHUdldFN0YXR1c1BhZ2VPdmVydmlld1Jlc3BvbnNlEjoKC3N0YXR1c19wYWdlGAEgASgLMiUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5TdGF0dXNQYWdlEkMKDWNvbmZpZ3VyYXRpb24YAiABKAsyLC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb25maWd1cmF0aW9uEjwKCmNvbXBvbmVudHMYAyADKAsyKC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnQSPQoGZ3JvdXBzGAQgAygLMi0ub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50R3JvdXASQQoOc3RhdHVzX3JlcG9ydHMYBSADKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0EkMKDG1haW50ZW5hbmNlcxgGIAMoCzItLm9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEuTWFpbnRlbmFuY2VTdW1tYXJ5EkAKDm92ZXJhbGxfc3RhdHVzGAcgASgOMigub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5PdmVyYWxsU3RhdHVzEkYKEmNvbXBvbmVudF9zdGF0dXNlcxgIIAMoCzIqLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ29tcG9uZW50U3RhdHVzIp0BCiNHZXRQYWdlQ29tcG9uZW50RGFpbHlTdW1tYXJ5UmVxdWVzdBIMCgJpZBgBIAEoCUgAEg4KBHNsdWcYAiABKAlIABIjCg1jb21wb25lbnRfaWRzGAMgAygJQgy6SAmSAQYiBHICEAESHAoEZGF5cxgEIAEoBUIJukgGGgQYLSgBSAGIAQFCDAoKaWRlbnRpZmllckIHCgVfZGF5cyLuAQoSQ29tcG9uZW50RGF5QnVja2V0EgsKA2RheRgBIAEoCRINCgVjb3VudBgCIAEoAxIKCgJvaxgDIAEoAxIQCghkZWdyYWRlZBgEIAEoAxINCgVlcnJvchgFIAEoAxI9CgZzdGF0dXMYBiABKA4yLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNvbXBvbmVudERheVN0YXR1cxJFCgZpbXBhY3QYByABKA4yMC5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuUGFnZUNvbXBvbmVudEltcGFjdEgAiAEBQgkKB19pbXBhY3QioAIKDkNvbXBvbmVudEV2ZW50EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSOwoEdHlwZRgDIAEoDjItLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ29tcG9uZW50RXZlbnRUeXBlEj8KBnN0YXR1cxgEIAEoDjIvLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ29tcG9uZW50RXZlbnRTdGF0dXMSDAoEZnJvbRgFIAEoCRIPCgJ0bxgGIAEoCUgAiAEBEkUKBmltcGFjdBgHIAEoDjIwLm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5QYWdlQ29tcG9uZW50SW1wYWN0SAGIAQFCBQoDX3RvQgkKB19pbXBhY3QingIKGVBhZ2VDb21wb25lbnREYWlseVN1bW1hcnkSFAoMY29tcG9uZW50X2lkGAEgASgJEjoKBHR5cGUYAiABKA4yLC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnRUeXBlEhcKCm1vbml0b3JfaWQYAyABKAlIAIgBARIMCgRuYW1lGAQgASgJEj4KB2J1Y2tldHMYBSADKAsyLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNvbXBvbmVudERheUJ1Y2tldBI5CgZldmVudHMYBiADKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNvbXBvbmVudEV2ZW50Qg0KC19tb25pdG9yX2lkInAKJEdldFBhZ2VDb21wb25lbnREYWlseVN1bW1hcnlSZXNwb25zZRJICgpjb21wb25lbnRzGAEgAygLMjQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50RGFpbHlTdW1tYXJ5KugBChJDb21wb25lbnREYXlTdGF0dXMSJAogQ09NUE9ORU5UX0RBWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIkCiBDT01QT05FTlRfREFZX1NUQVRVU19PUEVSQVRJT05BTBABEiEKHUNPTVBPTkVOVF9EQVlfU1RBVFVTX0RFR1JBREVEEAISHQoZQ09NUE9ORU5UX0RBWV9TVEFUVVNfRE9XThADEiQKIENPTVBPTkVOVF9EQVlfU1RBVFVTX01BSU5URU5BTkNFEAQSHgoaQ09NUE9ORU5UX0RBWV9TVEFUVVNfRU1QVFkQBSqkAQoSQ29tcG9uZW50RXZlbnRUeXBlEiQKIENPTVBPTkVOVF9FVkVOVF9UWVBFX1VOU1BFQ0lGSUVEEAASJAogQ09NUE9ORU5UX0VWRU5UX1RZUEVfTUFJTlRFTkFOQ0UQARIhCh1DT01QT05FTlRfRVZFTlRfVFlQRV9JTkNJREVOVBACEh8KG0NPTVBPTkVOVF9FVkVOVF9UWVBFX1JFUE9SVBADKtQBChRDb21wb25lbnRFdmVudFN0YXR1cxImCiJDT01QT05FTlRfRVZFTlRfU1RBVFVTX1VOU1BFQ0lGSUVEEAASJgoiQ09NUE9ORU5UX0VWRU5UX1NUQVRVU19PUEVSQVRJT05BTBABEiMKH0NPTVBPTkVOVF9FVkVOVF9TVEFUVVNfREVHUkFERUQQAhIfChtDT01QT05FTlRfRVZFTlRfU1RBVFVTX0RPV04QAxImCiJDT01QT05FTlRfRVZFTlRfU1RBVFVTX01BSU5URU5BTkNFEAQyxCcKEVN0YXR1c1BhZ2VTZXJ2aWNlEnsKEENyZWF0ZVN0YXR1c1BhZ2USMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNyZWF0ZVN0YXR1c1BhZ2VSZXF1ZXN0GjMub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DcmVhdGVTdGF0dXNQYWdlUmVzcG9uc2USdwoNR2V0U3RhdHVzUGFnZRIvLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZVJlcXVlc3QaMC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldFN0YXR1c1BhZ2VSZXNwb25zZSIDkAIBEn0KD0xpc3RTdGF0dXNQYWdlcxIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN0YXR1c1BhZ2VzUmVxdWVzdBoyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN0YXR1c1BhZ2VzUmVzcG9uc2UiA5ACARJ7ChBVcGRhdGVTdGF0dXNQYWdlEjIub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VcGRhdGVTdGF0dXNQYWdlUmVxdWVzdBozLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVXBkYXRlU3RhdHVzUGFnZVJlc3BvbnNlEnsKEERlbGV0ZVN0YXR1c1BhZ2USMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkRlbGV0ZVN0YXR1c1BhZ2VSZXF1ZXN0GjMub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5EZWxldGVTdGF0dXNQYWdlUmVzcG9uc2UShAEKE0FkZE1vbml0b3JDb21wb25lbnQSNS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkFkZE1vbml0b3JDb21wb25lbnRSZXF1ZXN0GjYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5BZGRNb25pdG9yQ29tcG9uZW50UmVzcG9uc2USgQEKEkFkZFN0YXRpY0NvbXBvbmVudBI0Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQWRkU3RhdGljQ29tcG9uZW50UmVxdWVzdBo1Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQWRkU3RhdGljQ29tcG9uZW50UmVzcG9uc2USeAoPUmVtb3ZlQ29tcG9uZW50EjEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5SZW1vdmVDb21wb25lbnRSZXF1ZXN0GjIub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5SZW1vdmVDb21wb25lbnRSZXNwb25zZRJ4Cg9VcGRhdGVDb21wb25lbnQSMS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlVwZGF0ZUNvbXBvbmVudFJlcXVlc3QaMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlVwZGF0ZUNvbXBvbmVudFJlc3BvbnNlEocBChRDcmVhdGVDb21wb25lbnRHcm91cBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ3JlYXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DcmVhdGVDb21wb25lbnRHcm91cFJlc3BvbnNlEocBChREZWxldGVDb21wb25lbnRHcm91cBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuRGVsZXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5EZWxldGVDb21wb25lbnRHcm91cFJlc3BvbnNlEocBChRVcGRhdGVDb21wb25lbnRHcm91cBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVXBkYXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VcGRhdGVDb21wb25lbnRHcm91cFJlc3BvbnNlErkECg9TdWJzY3JpYmVUb1BhZ2USMS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN1YnNjcmliZVRvUGFnZVJlcXVlc3QaMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN1YnNjcmliZVRvUGFnZVJlc3BvbnNlIr4Duke6Axq3A0VuZC11c2VyIGVtYWlsIHNlbGYtc2lnbnVwIHdpdGggZG91YmxlIG9wdC1pbiB2ZXJpZmljYXRpb24uIEEgdmVyaWZpY2F0aW9uIGVtYWlsIGlzIHNlbnQgYW5kIHRoZSBzdWJzY3JpcHRpb24gYWN0aXZhdGVzIG9ubHkgYWZ0ZXIgdGhlIHJlY2lwaWVudCBjb25maXJtcy4gSWYgdGhlIGVtYWlsIHdhcyBwcmV2aW91c2x5IHVuc3Vic2NyaWJlZCwgdGhlIGV4aXN0aW5nIHJvdyBpcyByZWFjdGl2YXRlZCBpbnN0ZWFkIG9mIGEgZHVwbGljYXRlIGJlaW5nIGNyZWF0ZWQuIFVzZSB0aGlzIGZvciBzdWJzY3JpYmVyLWluaXRpYXRlZCBzaWdudXBzIG9uIHRoZSBwdWJsaWMgc3RhdHVzIHBhZ2U7IHVzZSBDcmVhdGVQYWdlU3Vic2NyaXB0aW9uIGZvciBvcGVyYXRvci1pbml0aWF0ZWQgKHZlbmRvcikgc3Vic2NyaXB0aW9ucyB0aGF0IHNob3VsZCBza2lwIHZlcmlmaWNhdGlvbi4S/QQKFkNyZWF0ZVBhZ2VTdWJzY3JpcHRpb24SOC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNyZWF0ZVBhZ2VTdWJzY3JpcHRpb25SZXF1ZXN0Gjkub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DcmVhdGVQYWdlU3Vic2NyaXB0aW9uUmVzcG9uc2Ui7QO6R+kDGuYDT3BlcmF0b3ItYWRkZWQgc3Vic2NyaWJlciAoZW1haWwgb3Igd2ViaG9vaykgd2l0aCBubyB2ZXJpZmljYXRpb24gZmxvdyDigJQgdGhlIHBhcnRuZXIgc3RhcnRzIHJlY2VpdmluZyBub3RpZmljYXRpb25zIGltbWVkaWF0ZWx5LiBTdXBwb3J0cyBTbGFjayBhbmQgRGlzY29yZCB3ZWJob29rIFVSTHMgaW4gYWRkaXRpb24gdG8gZW1haWwuIEEgbWFuYWdlbWVudCB0b2tlbiBpcyBzdGlsbCBnZW5lcmF0ZWQgc28gdGhlIHN1YnNjcmliZXIgY2FuIHNlbGYtbWFuYWdlICh1cGRhdGUgc2NvcGUsIHVuc3Vic2NyaWJlKSB3aXRob3V0IG9wZXJhdG9yIGludm9sdmVtZW50LiBVc2UgdGhpcyBmb3IgdmVuZG9yL3BhcnRuZXIgaW50ZWdyYXRpb25zIHdoZXJlIGNvbnNlbnQgaXMgZXN0YWJsaXNoZWQgb3V0LW9mLWJhbmQ7IHVzZSBTdWJzY3JpYmVUb1BhZ2UgZm9yIHN1YnNjcmliZXItaW5pdGlhdGVkIHNpZ251cHMgdGhhdCByZXF1aXJlIGRvdWJsZSBvcHQtaW4uEoQBChNVbnN1YnNjcmliZUZyb21QYWdlEjUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VbnN1YnNjcmliZUZyb21QYWdlUmVxdWVzdBo2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVW5zdWJzY3JpYmVGcm9tUGFnZVJlc3BvbnNlEn0KD0xpc3RTdWJzY3JpYmVycxIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN1YnNjcmliZXJzUmVxdWVzdBoyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTGlzdFN1YnNjcmliZXJzUmVzcG9uc2UiA5ACARLAAwoUR2V0U3RhdHVzUGFnZUNvbnRlbnQSNi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldFN0YXR1c1BhZ2VDb250ZW50UmVxdWVzdBo3Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZUNvbnRlbnRSZXNwb25zZSK2ApACAbpHrwIarAJSZXR1cm5zIHRoZSBmdWxsIGNvbnRlbnQgb2YgYSBzdGF0dXMgcGFnZSBpbmNsdWRpbmcgaXRzIGNvbXBvbmVudHMsIGNvbXBvbmVudCBncm91cHMsIGFjdGl2ZSBzdGF0dXMgcmVwb3J0cywgYW5kIHNjaGVkdWxlZCBtYWludGVuYW5jZXMuIFN1cHBvcnRzIHR3byBhY2Nlc3MgcGF0aHM6IGJ5IElEIChyZXF1aXJlcyBhdXRoZW50aWNhdGlvbiwgd29ya3NwYWNlLXNjb3BlZCkgb3IgYnkgc2x1ZyAocHVibGljIGFjY2VzcywgcmVxdWlyZXMgdGhlIHBhZ2UgdG8gYmUgcHVibGlzaGVkIHdpdGggYWNjZXNzX3R5cGU9UFVCTElDKS4SqgMKEEdldE92ZXJhbGxTdGF0dXMSMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldE92ZXJhbGxTdGF0dXNSZXF1ZXN0GjMub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5HZXRPdmVyYWxsU3RhdHVzUmVzcG9uc2UirAKQAgG6R6UCGqICUmV0dXJucyB0aGUgb3ZlcmFsbCBzdGF0dXMgb2YgYSBzdGF0dXMgcGFnZSBhbG9uZyB3aXRoIGluZGl2aWR1YWwgY29tcG9uZW50IHN0YXR1c2VzLiBUaGUgb3ZlcmFsbCBzdGF0dXMgaXMgY29tcHV0ZWQgZnJvbSBhY3RpdmUgc3RhdHVzIHJlcG9ydHMgYW5kIG1haW50ZW5hbmNlcyB3aXRoIHRoZSBmb2xsb3dpbmcgcHJpb3JpdHk6IGRlZ3JhZGVkIChmcm9tIGFjdGl2ZSBzdGF0dXMgcmVwb3J0cykgPiBtYWludGVuYW5jZSAoZnJvbSBhY3RpdmUgbWFpbnRlbmFuY2Ugd2luZG93cykgPiBvcGVyYXRpb25hbC4SmAQKFUdldFN0YXR1c1BhZ2VPdmVydmlldxI3Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZU92ZXJ2aWV3UmVxdWVzdBo4Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZU92ZXJ2aWV3UmVzcG9uc2UiiwOQAgG6R4QDGoEDUmV0dXJucyBldmVyeXRoaW5nIGFib3V0IGEgc2luZ2xlIHN0YXR1cyBwYWdlIGluIG9uZSBhdXRoZW50aWNhdGVkIGNhbGw6IHRoZSBwYWdlLCBpdHMgcmljaCByZW5kZXJpbmcgY29uZmlndXJhdGlvbiwgY29tcG9uZW50cywgY29tcG9uZW50IGdyb3VwcywgYWN0aXZlL3JlY2VudCBzdGF0dXMgcmVwb3J0cywgbWFpbnRlbmFuY2VzLCBhbmQgdGhlIGNvbXB1dGVkIG92ZXJhbGwgKyBwZXItY29tcG9uZW50IHN0YXR1c2VzLiBXb3Jrc3BhY2Utc2NvcGVkIGJ5IGlkOyB0aGVyZSBpcyBubyBwdWJsaWMgc2x1ZyBhY2Nlc3MgcGF0aC4gRG9lcyBub3QgaW5jbHVkZSB1cHRpbWUgdGltZS1zZXJpZXMg4oCUIHVzZSBHZXRQYWdlQ29tcG9uZW50RGFpbHlTdW1tYXJ5IGZvciB0aGF0LhLVBAocR2V0UGFnZUNvbXBvbmVudERhaWx5U3VtbWFyeRI+Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0UGFnZUNvbXBvbmVudERhaWx5U3VtbWFyeVJlcXVlc3QaPy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldFBhZ2VDb21wb25lbnREYWlseVN1bW1hcnlSZXNwb25zZSKzA5ACAbpHrAMaqQNSZXR1cm5zIHBlci1jb21wb25lbnQgZGFpbHkgc3RhdHVzIGJ1Y2tldHMgKG9rL2RlZ3JhZGVkL2Vycm9yL2NvdW50IHBsdXMgYSByZXNvbHZlZCBzdGF0dXMpIG1lcmdlZCB3aXRoIHRoZSBpbmNpZGVudCwgbWFpbnRlbmFuY2UsIGFuZCBzdGF0dXMtcmVwb3J0IHRpbWVsaW5lIG92ZXIgdGhlIGxhc3QgTiBkYXlzIChtYXggNDUpLiBTdWl0YWJsZSBhcyBhIHNpbmdsZSBzb3VyY2Ugb2YgdHJ1dGggZm9yIHN0YXR1cy1iYXIgYW5kIHVwdGltZS1jYWxlbmRhciByZW5kZXJpbmcuIFN1cHBvcnRzIHR3byBhY2Nlc3MgcGF0aHM6IGJ5IGlkIChhdXRoZW50aWNhdGVkLCB3b3Jrc3BhY2Utc2NvcGVkKSBvciBieSBzbHVnIChwdWJsaWMgYWNjZXNzLCByZXF1aXJlcyB0aGUgcGFnZSB0byBiZSBwdWJsaXNoZWQgd2l0aCBhY2Nlc3NfdHlwZT1QVUJMSUMpLkJaWlhnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvc3RhdHVzX3BhZ2UvdjE7c3RhdHVzcGFnZXYxYgZwcm90bzM", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_maintenance_v1_maintenance, file_openstatus_status_page_v1_page_component, file_openstatus_status_page_v1_page_subscriber, file_openstatus_status_page_v1_status_page, file_openstatus_status_report_v1_status_report]); /** * CreateStatusPageRequest is the request to create a new status page. @@ -1398,6 +1398,97 @@ export type GetOverallStatusResponse = Message<"openstatus.status_page.v1.GetOve export const GetOverallStatusResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_openstatus_status_page_v1_service, 39); +/** + * GetStatusPageOverviewRequest requests the full overview of a status page by id. + * + * @generated from message openstatus.status_page.v1.GetStatusPageOverviewRequest + */ +export type GetStatusPageOverviewRequest = Message<"openstatus.status_page.v1.GetStatusPageOverviewRequest"> & { + /** + * ID of the status page (required, workspace-scoped). + * + * @generated from field: string id = 1; + */ + id: string; +}; + +/** + * Describes the message openstatus.status_page.v1.GetStatusPageOverviewRequest. + * Use `create(GetStatusPageOverviewRequestSchema)` to create a new message. + */ +export const GetStatusPageOverviewRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_openstatus_status_page_v1_service, 40); + +/** + * GetStatusPageOverviewResponse bundles all data for a single status page. + * + * @generated from message openstatus.status_page.v1.GetStatusPageOverviewResponse + */ +export type GetStatusPageOverviewResponse = Message<"openstatus.status_page.v1.GetStatusPageOverviewResponse"> & { + /** + * The status page details. + * + * @generated from field: openstatus.status_page.v1.StatusPage status_page = 1; + */ + statusPage?: StatusPage | undefined; + + /** + * Rich rendering configuration of the page. + * + * @generated from field: openstatus.status_page.v1.PageConfiguration configuration = 2; + */ + configuration?: PageConfiguration | undefined; + + /** + * Components on the status page. + * + * @generated from field: repeated openstatus.status_page.v1.PageComponent components = 3; + */ + components: PageComponent[]; + + /** + * Component groups on the status page. + * + * @generated from field: repeated openstatus.status_page.v1.PageComponentGroup groups = 4; + */ + groups: PageComponentGroup[]; + + /** + * Active and recent status reports. + * + * @generated from field: repeated openstatus.status_report.v1.StatusReport status_reports = 5; + */ + statusReports: StatusReport[]; + + /** + * Scheduled maintenances. + * + * @generated from field: repeated openstatus.maintenance.v1.MaintenanceSummary maintenances = 6; + */ + maintenances: MaintenanceSummary[]; + + /** + * Aggregated status across all components. + * + * @generated from field: openstatus.status_page.v1.OverallStatus overall_status = 7; + */ + overallStatus: OverallStatus; + + /** + * Status of individual components. + * + * @generated from field: repeated openstatus.status_page.v1.ComponentStatus component_statuses = 8; + */ + componentStatuses: ComponentStatus[]; +}; + +/** + * Describes the message openstatus.status_page.v1.GetStatusPageOverviewResponse. + * Use `create(GetStatusPageOverviewResponseSchema)` to create a new message. + */ +export const GetStatusPageOverviewResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_openstatus_status_page_v1_service, 41); + /** * GetPageComponentDailySummaryRequest is the request for per-component daily summaries. * @@ -1447,7 +1538,7 @@ export type GetPageComponentDailySummaryRequest = Message<"openstatus.status_pag * Use `create(GetPageComponentDailySummaryRequestSchema)` to create a new message. */ export const GetPageComponentDailySummaryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_openstatus_status_page_v1_service, 40); + messageDesc(file_openstatus_status_page_v1_service, 42); /** * ComponentDayBucket is one day of status data for a component. @@ -1510,7 +1601,7 @@ export type ComponentDayBucket = Message<"openstatus.status_page.v1.ComponentDay * Use `create(ComponentDayBucketSchema)` to create a new message. */ export const ComponentDayBucketSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_openstatus_status_page_v1_service, 41); + messageDesc(file_openstatus_status_page_v1_service, 43); /** * ComponentEvent is a single incident / maintenance / report affecting a component. @@ -1573,7 +1664,7 @@ export type ComponentEvent = Message<"openstatus.status_page.v1.ComponentEvent"> * Use `create(ComponentEventSchema)` to create a new message. */ export const ComponentEventSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_openstatus_status_page_v1_service, 42); + messageDesc(file_openstatus_status_page_v1_service, 44); /** * PageComponentDailySummary is the per-day buckets + event timeline for one component. @@ -1629,7 +1720,7 @@ export type PageComponentDailySummary = Message<"openstatus.status_page.v1.PageC * Use `create(PageComponentDailySummarySchema)` to create a new message. */ export const PageComponentDailySummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_openstatus_status_page_v1_service, 43); + messageDesc(file_openstatus_status_page_v1_service, 45); /** * GetPageComponentDailySummaryResponse is the response containing per-component summaries. @@ -1650,7 +1741,7 @@ export type GetPageComponentDailySummaryResponse = Message<"openstatus.status_pa * Use `create(GetPageComponentDailySummaryResponseSchema)` to create a new message. */ export const GetPageComponentDailySummaryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_openstatus_status_page_v1_service, 44); + messageDesc(file_openstatus_status_page_v1_service, 46); /** * ComponentDayStatus is the resolved status of a component on a given day. @@ -1971,6 +2062,18 @@ export const StatusPageService: GenService<{ input: typeof GetOverallStatusRequestSchema; output: typeof GetOverallStatusResponseSchema; }, + /** + * GetStatusPageOverview returns everything about a single status page in one + * authenticated call: page, rich configuration, components, groups, active + * reports, maintenances, and computed statuses. + * + * @generated from rpc openstatus.status_page.v1.StatusPageService.GetStatusPageOverview + */ + getStatusPageOverview: { + methodKind: "unary"; + input: typeof GetStatusPageOverviewRequestSchema; + output: typeof GetStatusPageOverviewResponseSchema; + }, /** * GetPageComponentDailySummary returns per-component daily uptime buckets merged * with the incident / maintenance / report timeline — the single source of truth diff --git a/packages/proto/gen/ts/openstatus/status_page/v1/status_page_pb.ts b/packages/proto/gen/ts/openstatus/status_page/v1/status_page_pb.ts index 1c4aba89e..798226a2a 100644 --- a/packages/proto/gen/ts/openstatus/status_page/v1/status_page_pb.ts +++ b/packages/proto/gen/ts/openstatus/status_page/v1/status_page_pb.ts @@ -11,7 +11,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/status_page/v1/status_page.proto. */ export const file_openstatus_status_page_v1_status_page: GenFile = /*@__PURE__*/ - fileDesc("CitvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3N0YXR1c19wYWdlLnByb3RvEhlvcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxIuoBCgtDdXN0b21UaGVtZRJACgVsaWdodBgBIAMoCzIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ3VzdG9tVGhlbWUuTGlnaHRFbnRyeRI+CgRkYXJrGAIgAygLMjAub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DdXN0b21UaGVtZS5EYXJrRW50cnkaLAoKTGlnaHRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGisKCURhcmtFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBItIFCgpTdGF0dXNQYWdlEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEh4KBHNsdWcYBCABKAlCELpHDToLEglhY21lLWNvcnASMAoNY3VzdG9tX2RvbWFpbhgFIAEoCUIZukcWOhQSEnN0YXR1cy5leGFtcGxlLmNvbRIRCglwdWJsaXNoZWQYBiABKAgSPgoLYWNjZXNzX3R5cGUYByABKA4yKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VBY2Nlc3NUeXBlEjMKBXRoZW1lGAggASgOMiQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlVGhlbWUSFAoMaG9tZXBhZ2VfdXJsGAkgASgJEhMKC2NvbnRhY3RfdXJsGAogASgJEgwKBGljb24YCyABKAkSLwoKY3JlYXRlZF9hdBgMIAEoCUIbukcYOhYSFDIwMjQtMDEtMTVUMDk6MDA6MDBaEi8KCnVwZGF0ZWRfYXQYDSABKAlCG7pHGDoWEhQyMDI0LTA2LTIwVDE0OjMwOjAwWhI5Cg5kZWZhdWx0X2xvY2FsZRgOIAEoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlEjIKB2xvY2FsZXMYDyADKA4yIS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkxvY2FsZRIQCghwYXNzd29yZBgQIAEoCRIaChJhdXRoX2VtYWlsX2RvbWFpbnMYESADKAkSEwoLYWxsb3dfaW5kZXgYEiABKAgSGQoRYWxsb3dlZF9pcF9yYW5nZXMYEyABKAkSQQoMY3VzdG9tX3RoZW1lGBQgASgLMiYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DdXN0b21UaGVtZUgAiAEBQg8KDV9jdXN0b21fdGhlbWUiqQEKEVN0YXR1c1BhZ2VTdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEgwKBHNsdWcYAyABKAkSEQoJcHVibGlzaGVkGAQgASgIEhIKCmNyZWF0ZWRfYXQYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoCRIwCg1jdXN0b21fZG9tYWluGAcgASgJQhm6RxY6FBISc3RhdHVzLmV4YW1wbGUuY29tKsABCg5QYWdlQWNjZXNzVHlwZRIgChxQQUdFX0FDQ0VTU19UWVBFX1VOU1BFQ0lGSUVEEAASGwoXUEFHRV9BQ0NFU1NfVFlQRV9QVUJMSUMQARInCiNQQUdFX0FDQ0VTU19UWVBFX1BBU1NXT1JEX1BST1RFQ1RFRBACEiIKHlBBR0VfQUNDRVNTX1RZUEVfQVVUSEVOVElDQVRFRBADEiIKHlBBR0VfQUNDRVNTX1RZUEVfSVBfUkVTVFJJQ1RFRBAEKmkKCVBhZ2VUaGVtZRIaChZQQUdFX1RIRU1FX1VOU1BFQ0lGSUVEEAASFQoRUEFHRV9USEVNRV9TWVNURU0QARIUChBQQUdFX1RIRU1FX0xJR0hUEAISEwoPUEFHRV9USEVNRV9EQVJLEAMqTQoGTG9jYWxlEhYKEkxPQ0FMRV9VTlNQRUNJRklFRBAAEg0KCUxPQ0FMRV9FThABEg0KCUxPQ0FMRV9GUhACEg0KCUxPQ0FMRV9ERRADKuwBCg1PdmVyYWxsU3RhdHVzEh4KGk9WRVJBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaT1ZFUkFMTF9TVEFUVVNfT1BFUkFUSU9OQUwQARIbChdPVkVSQUxMX1NUQVRVU19ERUdSQURFRBACEiEKHU9WRVJBTExfU1RBVFVTX1BBUlRJQUxfT1VUQUdFEAMSHwobT1ZFUkFMTF9TVEFUVVNfTUFKT1JfT1VUQUdFEAQSHgoaT1ZFUkFMTF9TVEFUVVNfTUFJTlRFTkFOQ0UQBRIaChZPVkVSQUxMX1NUQVRVU19VTktOT1dOEAZCWlpYZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxO3N0YXR1c3BhZ2V2MWIGcHJvdG8z", [file_gnostic_openapi_v3_annotations]); + fileDesc("CitvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3N0YXR1c19wYWdlLnByb3RvEhlvcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxIuoBCgtDdXN0b21UaGVtZRJACgVsaWdodBgBIAMoCzIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ3VzdG9tVGhlbWUuTGlnaHRFbnRyeRI+CgRkYXJrGAIgAygLMjAub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DdXN0b21UaGVtZS5EYXJrRW50cnkaLAoKTGlnaHRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGisKCURhcmtFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIsMBChFQYWdlQ29uZmlndXJhdGlvbhI+CgttZXRyaWNfdHlwZRgBIAEoDjIpLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuUGFnZU1ldHJpY1R5cGUSOAoIYmFyX3R5cGUYAiABKA4yJi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VCYXJUeXBlEhMKC3Nob3dfdXB0aW1lGAMgASgIEhEKCXRoZW1lX2tleRgEIAEoCRIMCgRkYXlzGAUgASgFItIFCgpTdGF0dXNQYWdlEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEh4KBHNsdWcYBCABKAlCELpHDToLEglhY21lLWNvcnASMAoNY3VzdG9tX2RvbWFpbhgFIAEoCUIZukcWOhQSEnN0YXR1cy5leGFtcGxlLmNvbRIRCglwdWJsaXNoZWQYBiABKAgSPgoLYWNjZXNzX3R5cGUYByABKA4yKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VBY2Nlc3NUeXBlEjMKBXRoZW1lGAggASgOMiQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlVGhlbWUSFAoMaG9tZXBhZ2VfdXJsGAkgASgJEhMKC2NvbnRhY3RfdXJsGAogASgJEgwKBGljb24YCyABKAkSLwoKY3JlYXRlZF9hdBgMIAEoCUIbukcYOhYSFDIwMjQtMDEtMTVUMDk6MDA6MDBaEi8KCnVwZGF0ZWRfYXQYDSABKAlCG7pHGDoWEhQyMDI0LTA2LTIwVDE0OjMwOjAwWhI5Cg5kZWZhdWx0X2xvY2FsZRgOIAEoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlEjIKB2xvY2FsZXMYDyADKA4yIS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkxvY2FsZRIQCghwYXNzd29yZBgQIAEoCRIaChJhdXRoX2VtYWlsX2RvbWFpbnMYESADKAkSEwoLYWxsb3dfaW5kZXgYEiABKAgSGQoRYWxsb3dlZF9pcF9yYW5nZXMYEyABKAkSQQoMY3VzdG9tX3RoZW1lGBQgASgLMiYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DdXN0b21UaGVtZUgAiAEBQg8KDV9jdXN0b21fdGhlbWUiqQEKEVN0YXR1c1BhZ2VTdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEgwKBHNsdWcYAyABKAkSEQoJcHVibGlzaGVkGAQgASgIEhIKCmNyZWF0ZWRfYXQYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoCRIwCg1jdXN0b21fZG9tYWluGAcgASgJQhm6RxY6FBISc3RhdHVzLmV4YW1wbGUuY29tKsABCg5QYWdlQWNjZXNzVHlwZRIgChxQQUdFX0FDQ0VTU19UWVBFX1VOU1BFQ0lGSUVEEAASGwoXUEFHRV9BQ0NFU1NfVFlQRV9QVUJMSUMQARInCiNQQUdFX0FDQ0VTU19UWVBFX1BBU1NXT1JEX1BST1RFQ1RFRBACEiIKHlBBR0VfQUNDRVNTX1RZUEVfQVVUSEVOVElDQVRFRBADEiIKHlBBR0VfQUNDRVNTX1RZUEVfSVBfUkVTVFJJQ1RFRBAEKmkKCVBhZ2VUaGVtZRIaChZQQUdFX1RIRU1FX1VOU1BFQ0lGSUVEEAASFQoRUEFHRV9USEVNRV9TWVNURU0QARIUChBQQUdFX1RIRU1FX0xJR0hUEAISEwoPUEFHRV9USEVNRV9EQVJLEAMqTQoGTG9jYWxlEhYKEkxPQ0FMRV9VTlNQRUNJRklFRBAAEg0KCUxPQ0FMRV9FThABEg0KCUxPQ0FMRV9GUhACEg0KCUxPQ0FMRV9ERRADKuwBCg1PdmVyYWxsU3RhdHVzEh4KGk9WRVJBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaT1ZFUkFMTF9TVEFUVVNfT1BFUkFUSU9OQUwQARIbChdPVkVSQUxMX1NUQVRVU19ERUdSQURFRBACEiEKHU9WRVJBTExfU1RBVFVTX1BBUlRJQUxfT1VUQUdFEAMSHwobT1ZFUkFMTF9TVEFUVVNfTUFKT1JfT1VUQUdFEAQSHgoaT1ZFUkFMTF9TVEFUVVNfTUFJTlRFTkFOQ0UQBRIaChZPVkVSQUxMX1NUQVRVU19VTktOT1dOEAYqjQEKDlBhZ2VNZXRyaWNUeXBlEiAKHFBBR0VfTUVUUklDX1RZUEVfVU5TUEVDSUZJRUQQABIdChlQQUdFX01FVFJJQ19UWVBFX0RVUkFUSU9OEAESHQoZUEFHRV9NRVRSSUNfVFlQRV9SRVFVRVNUUxACEhsKF1BBR0VfTUVUUklDX1RZUEVfTUFOVUFMEAMqYgoLUGFnZUJhclR5cGUSHQoZUEFHRV9CQVJfVFlQRV9VTlNQRUNJRklFRBAAEhoKFlBBR0VfQkFSX1RZUEVfQUJTT0xVVEUQARIYChRQQUdFX0JBUl9UWVBFX01BTlVBTBACQlpaWGdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9zdGF0dXNfcGFnZS92MTtzdGF0dXNwYWdldjFiBnByb3RvMw", [file_gnostic_openapi_v3_annotations]); /** * CustomTheme holds per-mode CSS variable overrides merged over the page theme. @@ -41,6 +41,56 @@ export type CustomTheme = Message<"openstatus.status_page.v1.CustomTheme"> & { export const CustomThemeSchema: GenMessage = /*@__PURE__*/ messageDesc(file_openstatus_status_page_v1_status_page, 0); +/** + * PageConfiguration is the rich rendering config stored in page.configuration. + * + * @generated from message openstatus.status_page.v1.PageConfiguration + */ +export type PageConfiguration = Message<"openstatus.status_page.v1.PageConfiguration"> & { + /** + * Which metric the status bars represent (configuration.value). + * + * @generated from field: openstatus.status_page.v1.PageMetricType metric_type = 1; + */ + metricType: PageMetricType; + + /** + * How the status bar is computed (configuration.type). + * + * @generated from field: openstatus.status_page.v1.PageBarType bar_type = 2; + */ + barType: PageBarType; + + /** + * Whether to show the uptime percentage (configuration.uptime). + * + * @generated from field: bool show_uptime = 3; + */ + showUptime: boolean; + + /** + * Theme key from the theme store (configuration.theme), e.g. "default". + * Free-form string rather than an enum because the theme catalog is dynamic. + * + * @generated from field: string theme_key = 4; + */ + themeKey: string; + + /** + * Number of uptime-bar days rendered on the page (configuration.days): 30 or 45. + * + * @generated from field: int32 days = 5; + */ + days: number; +}; + +/** + * Describes the message openstatus.status_page.v1.PageConfiguration. + * Use `create(PageConfigurationSchema)` to create a new message. + */ +export const PageConfigurationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_openstatus_status_page_v1_status_page, 1); + /** * StatusPage represents a full status page with all details. * @@ -193,7 +243,7 @@ export type StatusPage = Message<"openstatus.status_page.v1.StatusPage"> & { * Use `create(StatusPageSchema)` to create a new message. */ export const StatusPageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_openstatus_status_page_v1_status_page, 1); + messageDesc(file_openstatus_status_page_v1_status_page, 2); /** * StatusPageSummary represents metadata for a status page (used in list responses). @@ -256,7 +306,7 @@ export type StatusPageSummary = Message<"openstatus.status_page.v1.StatusPageSum * Use `create(StatusPageSummarySchema)` to create a new message. */ export const StatusPageSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_openstatus_status_page_v1_status_page, 2); + messageDesc(file_openstatus_status_page_v1_status_page, 3); /** * PageAccessType defines who can access the status page. @@ -410,3 +460,64 @@ export enum OverallStatus { export const OverallStatusSchema: GenEnum = /*@__PURE__*/ enumDesc(file_openstatus_status_page_v1_status_page, 3); +/** + * PageMetricType mirrors page.configuration.value (which metric the status bars represent). + * + * @generated from enum openstatus.status_page.v1.PageMetricType + */ +export enum PageMetricType { + /** + * @generated from enum value: PAGE_METRIC_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: PAGE_METRIC_TYPE_DURATION = 1; + */ + DURATION = 1, + + /** + * @generated from enum value: PAGE_METRIC_TYPE_REQUESTS = 2; + */ + REQUESTS = 2, + + /** + * @generated from enum value: PAGE_METRIC_TYPE_MANUAL = 3; + */ + MANUAL = 3, +} + +/** + * Describes the enum openstatus.status_page.v1.PageMetricType. + */ +export const PageMetricTypeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_openstatus_status_page_v1_status_page, 4); + +/** + * PageBarType mirrors page.configuration.type (how the status bar is computed). + * + * @generated from enum openstatus.status_page.v1.PageBarType + */ +export enum PageBarType { + /** + * @generated from enum value: PAGE_BAR_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: PAGE_BAR_TYPE_ABSOLUTE = 1; + */ + ABSOLUTE = 1, + + /** + * @generated from enum value: PAGE_BAR_TYPE_MANUAL = 2; + */ + MANUAL = 2, +} + +/** + * Describes the enum openstatus.status_page.v1.PageBarType. + */ +export const PageBarTypeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_openstatus_status_page_v1_status_page, 5); + diff --git a/packages/proto/plan/api.md b/packages/proto/plan/api.md index 3fefa5b56..c115fb4ca 100644 --- a/packages/proto/plan/api.md +++ b/packages/proto/plan/api.md @@ -71,6 +71,7 @@ service StatusPageService { // Full Content & Status rpc GetStatusPageContent(GetStatusPageContentRequest) returns (GetStatusPageContentResponse); rpc GetOverallStatus(GetOverallStatusRequest) returns (GetOverallStatusResponse); + rpc GetStatusPageOverview(GetStatusPageOverviewRequest) returns (GetStatusPageOverviewResponse); } ``` @@ -107,11 +108,12 @@ service StatusPageService { | `UnsubscribeFromPage` | page_id, email or token | success | | `ListSubscribers` | page_id, limit?, offset?, include_unsubscribed? | PageSubscriber[], total_size | -### Full Content & Status (2 methods) +### Full Content & Status (3 methods) | Method | Request Fields | Response | |--------|---------------|----------| | `GetStatusPageContent` | id or slug | StatusPage, components[], groups[], status_reports[], maintenances[] | | `GetOverallStatus` | id or slug | overall_status, component_statuses[] | +| `GetStatusPageOverview` | id (auth only) | StatusPage, configuration, components[], groups[], status_reports[], maintenances[], overall_status, component_statuses[] | ## Validation Rules (using buf.validate) diff --git a/packages/services/src/page/get-content.ts b/packages/services/src/page/get-content.ts new file mode 100644 index 000000000..f45d7658d --- /dev/null +++ b/packages/services/src/page/get-content.ts @@ -0,0 +1,177 @@ +import { and, db as defaultDb, desc, eq, inArray } from "@openstatus/db"; +import { + type PageComponentImpact, + maintenance, + maintenancesToPageComponents, + pageComponent, + pageComponentGroup, + statusReport, + statusReportUpdate, + statusReportUpdateToPageComponents, + statusReportsToPageComponents, +} from "@openstatus/db/src/schema"; + +import type { DB } from "../context"; + +type ComponentRow = typeof pageComponent.$inferSelect; +type GroupRow = typeof pageComponentGroup.$inferSelect; +type ReportRow = typeof statusReport.$inferSelect; +type UpdateRow = typeof statusReportUpdate.$inferSelect; +type MaintenanceRow = typeof maintenance.$inferSelect; + +export type StatusReportContent = ReportRow & { + pageComponentIds: number[]; + updates: Array< + UpdateRow & { + componentImpacts: { + pageComponentId: number; + impact: PageComponentImpact; + }[]; + } + >; +}; + +export type MaintenanceContent = MaintenanceRow & { + pageComponentIds: number[]; +}; + +export type StatusPageContent = { + components: ComponentRow[]; + groups: GroupRow[]; + statusReports: StatusReportContent[]; + maintenances: MaintenanceContent[]; +}; + +const ACTIVE_REPORT_STATUSES = [ + "investigating", + "identified", + "monitoring", +] as const; + +/** + * Shared read for a status page's renderable content (components, groups, + * active reports with per-update impacts, maintenances), transport-agnostic so + * both the Connect handlers and the tRPC router can consume it. Uses `ctx.db` + * so callers can run it inside a transaction. + */ +export async function getStatusPageContent(args: { + ctx?: { db?: DB }; + pageId: number; +}): Promise { + const db = args.ctx?.db ?? defaultDb; + const { pageId } = args; + + const [components, groups, activeReports, pageMaintenances] = + await Promise.all([ + db + .select() + .from(pageComponent) + .where(eq(pageComponent.pageId, pageId)) + .orderBy(pageComponent.order) + .all(), + db + .select() + .from(pageComponentGroup) + .where(eq(pageComponentGroup.pageId, pageId)) + .all(), + db + .select() + .from(statusReport) + .where( + and( + eq(statusReport.pageId, pageId), + inArray(statusReport.status, [...ACTIVE_REPORT_STATUSES]), + ), + ) + .orderBy(desc(statusReport.createdAt)) + .all(), + db + .select() + .from(maintenance) + .where(eq(maintenance.pageId, pageId)) + .orderBy(desc(maintenance.from)) + .all(), + ]); + + const reportIds = activeReports.map((r) => r.id); + const [reportUpdates, reportComponents] = await Promise.all([ + reportIds.length > 0 + ? db + .select() + .from(statusReportUpdate) + .where(inArray(statusReportUpdate.statusReportId, reportIds)) + .orderBy(desc(statusReportUpdate.date)) + .all() + : Promise.resolve([] as UpdateRow[]), + reportIds.length > 0 + ? db + .select() + .from(statusReportsToPageComponents) + .where( + inArray(statusReportsToPageComponents.statusReportId, reportIds), + ) + .all() + : Promise.resolve( + [] as (typeof statusReportsToPageComponents.$inferSelect)[], + ), + ]); + + const updateIds = reportUpdates.map((u) => u.id); + const updateImpacts = + updateIds.length > 0 + ? await db + .select() + .from(statusReportUpdateToPageComponents) + .where( + inArray( + statusReportUpdateToPageComponents.statusReportUpdateId, + updateIds, + ), + ) + .all() + : []; + + const impactsByUpdate = new Map(); + for (const row of updateImpacts) { + const arr = impactsByUpdate.get(row.statusReportUpdateId); + if (arr) arr.push(row); + else impactsByUpdate.set(row.statusReportUpdateId, [row]); + } + + const statusReports: StatusReportContent[] = activeReports.map((report) => ({ + ...report, + pageComponentIds: reportComponents + .filter((rc) => rc.statusReportId === report.id) + .map((rc) => rc.pageComponentId), + updates: reportUpdates + .filter((u) => u.statusReportId === report.id) + .map((u) => ({ + ...u, + componentImpacts: (impactsByUpdate.get(u.id) ?? []).map((row) => ({ + pageComponentId: row.pageComponentId, + impact: row.impact, + })), + })), + })); + + const maintenanceIds = pageMaintenances.map((m) => m.id); + const maintenanceComponents = + maintenanceIds.length > 0 + ? await db + .select() + .from(maintenancesToPageComponents) + .where( + inArray(maintenancesToPageComponents.maintenanceId, maintenanceIds), + ) + .all() + : []; + + const maintenances: MaintenanceContent[] = pageMaintenances.map((m) => ({ + ...m, + pageComponentIds: maintenanceComponents + .filter((mc) => mc.maintenanceId === m.id) + .map((mc) => mc.pageComponentId), + })); + + return { components, groups, statusReports, maintenances }; +} diff --git a/packages/services/src/page/index.ts b/packages/services/src/page/index.ts index 80f971185..83f67cd16 100644 --- a/packages/services/src/page/index.ts +++ b/packages/services/src/page/index.ts @@ -1,5 +1,11 @@ export { createPage, newPage } from "./create"; export { deletePage } from "./delete"; +export { + type MaintenanceContent, + type StatusPageContent, + type StatusReportContent, + getStatusPageContent, +} from "./get-content"; export { getPage, getPageBySlug, -- 2.54.0