Openstatus www.openstatus.dev

status page as json (#1575)

* return update as json

* ci: apply automated fixes

* add ui

* ci: apply automated fixes

* add status

* small stuff

* chore: rework tracker and add monitors

* ci: apply automated fixes

* chore: add title and description

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Maximilian Kaske <maximilian@kaske.org>

authored by

Thibault Le Ouay
autofix-ci[bot]
Maximilian Kaske
and committed by
GitHub
faccb978 4b7161e2

+109 -5
-1
apps/server/src/routes/public/status.ts
··· 157 157 ]); 158 158 159 159 return { 160 - // monitorData, 161 160 pageStatusReportData, 162 161 monitorStatusReportData, 163 162 maintenanceData,
+88
apps/status-page/src/app/(status-page)/[domain]/(public)/feed/json/route.ts
··· 1 + import { getQueryClient, trpc } from "@/lib/trpc/server"; 2 + import { notFound, unauthorized } from "next/navigation"; 3 + 4 + const _STATUS_LABELS = { 5 + investigating: "Investigating", 6 + identified: "Identified", 7 + monitoring: "Monitoring", 8 + resolved: "Resolved", 9 + maintenance: "Maintenance", 10 + } as const; 11 + 12 + export const revalidate = 60; 13 + 14 + export async function GET( 15 + _request: Request, 16 + props: { params: Promise<{ domain: string }> }, 17 + ) { 18 + try { 19 + const queryClient = getQueryClient(); 20 + const { domain } = await props.params; 21 + 22 + const _page = await queryClient.fetchQuery( 23 + trpc.page.getPageBySlug.queryOptions({ slug: domain }), 24 + ); 25 + 26 + if (!_page) return notFound(); 27 + 28 + if (_page.passwordProtected) { 29 + const url = new URL(_request.url); 30 + const password = url.searchParams.get("pw"); 31 + console.log({ url, _page, password }); 32 + if (password !== _page.password) return unauthorized(); 33 + } 34 + 35 + const page = await queryClient.fetchQuery( 36 + trpc.statusPage.get.queryOptions({ slug: domain }), 37 + ); 38 + 39 + if (!page) return notFound(); 40 + 41 + const res = { 42 + title: page.title, 43 + description: page.description, 44 + status: page.status, 45 + updatedAt: new Date(), 46 + monitors: page.monitors.map((monitor) => ({ 47 + id: monitor.id, 48 + name: monitor.name, 49 + description: monitor.description, 50 + status: monitor.status, 51 + })), 52 + maintenances: page.maintenances.map((maintenance) => ({ 53 + id: maintenance.id, 54 + name: maintenance.title, 55 + message: maintenance.message, 56 + from: maintenance.from, 57 + to: maintenance.to, 58 + updatedAt: maintenance.updatedAt, 59 + monitors: maintenance.maintenancesToMonitors.map( 60 + (item) => item.monitor.id, 61 + ), 62 + })), 63 + statusReports: page.statusReports.map((report) => ({ 64 + id: report.id, 65 + title: report.title, 66 + updateAt: report.updatedAt, 67 + status: report.status, 68 + monitors: report.monitorsToStatusReports.map((item) => item.monitor.id), 69 + statusReportUpdates: report.statusReportUpdates.map((update) => ({ 70 + id: update.id, 71 + status: update.status, 72 + message: update.message, 73 + date: update.date, 74 + updatedAt: update.updatedAt, 75 + })), 76 + })), 77 + }; 78 + 79 + return new Response(JSON.stringify(res), { 80 + headers: { 81 + "Content-Type": "application/json; charset=utf-8", 82 + }, 83 + }); 84 + } catch (error) { 85 + console.error("Error generating feed:", error); 86 + throw error; 87 + } 88 + }
+4 -2
apps/status-page/src/components/nav/header.tsx
··· 83 83 ); 84 84 85 85 const types = ( 86 - page?.workspacePlan === "free" ? ["rss", "ssh"] : ["email", "rss", "ssh"] 87 - ) satisfies ("email" | "rss" | "ssh")[]; 86 + page?.workspacePlan === "free" 87 + ? ["rss", "ssh", "json"] 88 + : ["email", "rss", "json", "ssh"] 89 + ) satisfies ("email" | "rss" | "ssh" | "json")[]; 88 90 89 91 return ( 90 92 <header {...props}>
+17 -2
apps/status-page/src/components/status-page/status-updates.tsx
··· 17 17 import { Check, Copy, Inbox } from "lucide-react"; 18 18 import { useState } from "react"; 19 19 20 - type StatusUpdateType = "email" | "rss" | "ssh"; 20 + type StatusUpdateType = "email" | "rss" | "ssh" | "json"; 21 21 22 22 type Page = NonNullable<RouterOutputs["statusPage"]["get"]>; 23 23 ··· 31 31 32 32 export function StatusUpdates({ 33 33 className, 34 - types = ["rss", "ssh"], 34 + types = ["rss", "ssh", "json"], 35 35 page, 36 36 onSubscribe, 37 37 ...props ··· 63 63 {types.includes("rss") ? ( 64 64 <TabsTrigger value="rss">RSS</TabsTrigger> 65 65 ) : null} 66 + {types.includes("json") ? ( 67 + <TabsTrigger value="json">JSON</TabsTrigger> 68 + ) : null} 66 69 {types.includes("ssh") ? ( 67 70 <TabsTrigger value="ssh">SSH</TabsTrigger> 68 71 ) : null} ··· 111 114 className="w-full" 112 115 id="atom" 113 116 value={`${baseUrl}/feed/atom${ 117 + page?.passwordProtected ? `?pw=${page?.password}` : "" 118 + }`} 119 + /> 120 + </div> 121 + </TabsContent> 122 + <TabsContent value="json" className="flex flex-col gap-2"> 123 + <div className="flex flex-col gap-2 px-2 pb-2"> 124 + <p className="text-sm">Get the JSON updates</p> 125 + <CopyInputButton 126 + className="w-full" 127 + id="json" 128 + value={`${baseUrl}/feed/json${ 114 129 page?.passwordProtected ? `?pw=${page?.password}` : "" 115 130 }`} 116 131 />