Openstatus www.openstatus.dev

Docs/status widget (#356)

* chore: status widget

* chore: include maintenance

authored by

Maximilian Kaske and committed by
GitHub
efb7d027 7140e852

+246 -60
+1 -1
apps/docs/pages/_meta.json
··· 3 3 "getting-started": "Getting started", 4 4 "integrations": "Integrations", 5 5 "developer-guide": "Developer guide", 6 - "rest-api": "API", 6 + "api-server": "API", 7 7 "contact": { 8 8 "title": "🌐 Website", 9 9 "type": "page",
+5
apps/docs/pages/api-server/_meta.json
··· 1 + { 2 + "auth": "Auth Token", 3 + "openapi": "OpenAPI Specs", 4 + "status-widget": "Status Widget" 5 + }
+136
apps/docs/pages/api-server/status-widget.mdx
··· 1 + # Public Status Widget 2 + 3 + We have added a public endpoint where you can access the status of your status 4 + page. To access it, you only need the unique `:slug` you have chosen for your 5 + page. 6 + 7 + ```bash 8 + curl https://api.openstatus.dev/public/status/:slug 9 + ``` 10 + 11 + The response is a JSON object with the following structure: 12 + 13 + ```json 14 + { "status": "operational" } 15 + ``` 16 + 17 + in which the status can be one of the following values: 18 + 19 + ```ts 20 + enum Status { 21 + Operational = "operational", 22 + DegradedPerformance = "degraded_performance", 23 + PartialOutage = "partial_outage", 24 + MajorOutage = "major_outage", 25 + UnderMaintenance = "under_maintenance", // currently not in use 26 + Unknown = "unknown", 27 + } 28 + ``` 29 + 30 + ### How does it work? 31 + 32 + The status is calculated by the ratio `uptime / (uptime + downtime)` of the last 33 + 5 cron jobs of each monitor which we accumulate together. It is a simple 34 + calculation that gives us a good overview of the status of your services. 35 + 36 + ```ts 37 + function getStatus(ratio: number) { 38 + if (isNaN(ratio)) return Status.Unknown; 39 + if (ratio >= 0.98) return Status.Operational; 40 + if (ratio >= 0.6) return Status.DegradedPerformance; 41 + if (ratio >= 0.3) return Status.PartialOutage; 42 + if (ratio >= 0) return Status.MajorOutage; 43 + return Status.Unknown; 44 + } 45 + ``` 46 + 47 + We are caching the result for `30 seconds` to reduce the load on our database. 48 + 49 + > If you have a doubt about the above calculation, feel free to contact us via 50 + > [ping@openstatus.dev](mailto:ping@openstatus.dev) or 51 + > [discord](https://openstatus.dev/discord). 52 + 53 + ### React Widget 54 + 55 + We currently do not provide any SDK or package to integrate the status into your 56 + stack. But will in the future. 57 + 58 + If you are using Nextjs and RSC with the `/app` directory, feel free to use that 59 + Component. Small reminder that we are using shadcn ui and tailwindcss. You might 60 + want to update the `bg-muted` and `text-foreground` classes to your needs. 61 + 62 + ![Status Widget](/img/status-widget/widget-example.png) 63 + 64 + We are using `zod` to validate the response. You can use any other library if 65 + you want or just remove it. But better be safe than sorry. 66 + 67 + ```tsx 68 + import * as z from "zod"; 69 + 70 + const statusEnum = z.enum([ 71 + "operational", 72 + "degraded_performance", 73 + "partial_outage", 74 + "major_outage", 75 + "under_maintenance", // currently not in use 76 + "unknown", 77 + ]); 78 + 79 + const statusSchema = z.object({ status: statusEnum }); 80 + 81 + const dictionary = { 82 + operational: { 83 + label: "Operational", 84 + color: "bg-green-500", 85 + }, 86 + degraded_performance: { 87 + label: "Degraded Performance", 88 + color: "bg-yellow-500", 89 + }, 90 + partial_outage: { 91 + label: "Partial Outage", 92 + color: "bg-yellow-500", 93 + }, 94 + major_outage: { 95 + label: "Major Outage", 96 + color: "bg-red-500", 97 + }, 98 + unknown: { 99 + label: "Unknown", 100 + color: "bg-gray-500", 101 + }, 102 + under_maintenance: { 103 + label: "Under Maintenance", 104 + color: "bg-gray-500", 105 + }, 106 + } as const; 107 + 108 + export async function StatusWidget({ slug }: { slug: string }) { 109 + const res = await fetch(`https://api.openstatus.dev/public/status/${slug}`, { 110 + next: { revalidate: 60 }, // cache request for 60 seconds 111 + }); 112 + const data = await res.json(); 113 + const parsed = statusSchema.safeParse(data); 114 + 115 + let label = "Unknown"; 116 + let color = "bg-gray-500"; 117 + 118 + if (parsed.success) { 119 + const status = dictionary[parsed.data.status]; 120 + label = status.label; 121 + color = status.color; 122 + } 123 + 124 + return ( 125 + <a 126 + className="border-border text-foreground/70 hover:bg-muted hover:text-foreground inline-flex max-w-fit items-center gap-2 rounded-md border px-3 py-1 text-sm" 127 + href={`https://${slug}.openstatus.dev`} 128 + target="_blank" 129 + rel="noreferrer" 130 + > 131 + {label} 132 + <span className={`inline-block h-2 w-2 rounded-full ${color}`} /> 133 + </a> 134 + ); 135 + } 136 + ```
-4
apps/docs/pages/rest-api/_meta.json
··· 1 - { 2 - "auth": "Auth Token", 3 - "openapi": "OpenAPI Specs" 4 - }
+1 -1
apps/docs/pages/rest-api/auth.mdx apps/docs/pages/api-server/auth.mdx
··· 19 19 20 20 Use the above snippet to try it out. 21 21 22 - Learn more about [all the endpoints](/rest-api/openapi) we provide. 22 + Learn more about [all the endpoints](/api-server/openapi) we provide. 23 23 24 24 We currently do not have an SDK to make the best out of it. Any contributions 25 25 are welcome.
apps/docs/pages/rest-api/openapi.mdx apps/docs/pages/api-server/openapi.mdx
apps/docs/public/img/status-widget/widget-example.png

This is a binary file and will not be displayed.

+2 -21
apps/docs/public/sitemap-0.xml
··· 1 1 <?xml version="1.0" encoding="UTF-8"?> 2 2 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"> 3 - <url><loc>https://docs.openstatus.dev</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 4 - <url><loc>https://docs.openstatus.dev/developer-guide/get-started</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 5 - <url><loc>https://docs.openstatus.dev/developer-guide/requirements</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 6 - <url><loc>https://docs.openstatus.dev/developer-guide/setup</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 7 - <url><loc>https://docs.openstatus.dev/developer-guide/setup-env</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 8 - <url><loc>https://docs.openstatus.dev/getting-started</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 9 - <url><loc>https://docs.openstatus.dev/getting-started/alerting</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 10 - <url><loc>https://docs.openstatus.dev/getting-started/heartbeat</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 11 - <url><loc>https://docs.openstatus.dev/getting-started/monitor</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 12 - <url><loc>https://docs.openstatus.dev/getting-started/status-page</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 13 - <url><loc>https://docs.openstatus.dev/integrations</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 14 - <url><loc>https://docs.openstatus.dev/integrations/discord</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 15 - <url><loc>https://docs.openstatus.dev/integrations/fly</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 16 - <url><loc>https://docs.openstatus.dev/integrations/otel</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 17 - <url><loc>https://docs.openstatus.dev/integrations/phone-call</loc><lastmod>2023-10-02T15:53:57.199Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 18 - <url><loc>https://docs.openstatus.dev/integrations/slack</loc><lastmod>2023-10-02T15:53:57.200Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 19 - <url><loc>https://docs.openstatus.dev/integrations/sms</loc><lastmod>2023-10-02T15:53:57.200Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 20 - <url><loc>https://docs.openstatus.dev/integrations/telegram</loc><lastmod>2023-10-02T15:53:57.200Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 21 - <url><loc>https://docs.openstatus.dev/integrations/vercel</loc><lastmod>2023-10-02T15:53:57.200Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 22 - <url><loc>https://docs.openstatus.dev/rest-api/auth</loc><lastmod>2023-10-02T15:53:57.200Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 23 - <url><loc>https://docs.openstatus.dev/rest-api/openapi</loc><lastmod>2023-10-02T15:53:57.200Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 3 + <url><loc>https://docs.openstatus.dev</loc><lastmod>2023-10-04T17:44:38.856Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 4 + <url><loc>https://docs.openstatus.dev/api-server/status-widget</loc><lastmod>2023-10-04T17:44:38.856Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> 24 5 </urlset>
+1 -1
apps/server/src/public/status.ts
··· 41 41 monitorData.map(async ({ monitors_to_pages }) => { 42 42 return await getMonitorList(tb)({ 43 43 monitorId: String(monitors_to_pages.monitorId), 44 - limit: 3, // limits the grouped cronTimestamps 44 + limit: 5, // limits the grouped cronTimestamps 45 45 }); 46 46 }), 47 47 );
+30 -26
apps/web/src/components/layout/marketing-footer.tsx
··· 1 1 import Link from "next/link"; 2 2 import { ArrowUpRight } from "lucide-react"; 3 3 4 + import { StatusWidget } from "@/components/status-widget"; 4 5 import { cn } from "@/lib/utils"; 5 6 import { Shell } from "../dashboard/shell"; 6 7 ··· 11 12 export function MarketingFooter({ className }: Props) { 12 13 return ( 13 14 <footer className={cn("w-full", className)}> 14 - <Shell className="grid grid-cols-2 gap-6 md:grid-cols-4"> 15 - <div className="flex flex-col gap-3 text-sm"> 16 - <p className="text-foreground font-semibold">People</p> 17 - <FooterLink 18 - href="https://twitter.com/thibaultleouay" 19 - label="@thibaultleouay" 20 - /> 21 - <FooterLink href="https://twitter.com/mxkaske" label="@mxkaske" /> 22 - </div> 23 - <div className="flex flex-col gap-3 text-sm"> 24 - <p className="text-foreground font-semibold">Community</p> 25 - <FooterLink href="/github" label="GitHub" external /> 26 - <FooterLink href="/discord" label="Discord" external /> 27 - <FooterLink href="https://twitter.com/openstatusHQ" label="X" /> 28 - </div> 29 - <div className="flex flex-col gap-3 text-sm"> 30 - <p className="text-foreground font-semibold">Resources</p> 31 - <FooterLink href="https://status.openstatus.dev" label="Status" /> 32 - <FooterLink href="/blog" label="Blog" /> 33 - <FooterLink href="https://docs.openstatus.dev" label="Docs" /> 34 - <FooterLink href="/oss-friends" label="OSS Friends" /> 35 - </div> 36 - <div className="flex flex-col gap-3 text-sm"> 37 - <p className="text-foreground font-semibold">Legal</p> 38 - <FooterLink href="/legal/terms" label="Terms" /> 39 - <FooterLink href="/legal/privacy" label="Privacy" /> 15 + <Shell className="grid gap-6"> 16 + <div className="grid grid-cols-2 gap-6 md:grid-cols-4"> 17 + <div className="flex flex-col gap-3 text-sm"> 18 + <p className="text-foreground font-semibold">People</p> 19 + <FooterLink 20 + href="https://twitter.com/thibaultleouay" 21 + label="@thibaultleouay" 22 + /> 23 + <FooterLink href="https://twitter.com/mxkaske" label="@mxkaske" /> 24 + </div> 25 + <div className="flex flex-col gap-3 text-sm"> 26 + <p className="text-foreground font-semibold">Community</p> 27 + <FooterLink href="/github" label="GitHub" external /> 28 + <FooterLink href="/discord" label="Discord" external /> 29 + <FooterLink href="https://twitter.com/openstatusHQ" label="X" /> 30 + </div> 31 + <div className="flex flex-col gap-3 text-sm"> 32 + <p className="text-foreground font-semibold">Resources</p> 33 + <FooterLink href="https://status.openstatus.dev" label="Status" /> 34 + <FooterLink href="/blog" label="Blog" /> 35 + <FooterLink href="https://docs.openstatus.dev" label="Docs" /> 36 + <FooterLink href="/oss-friends" label="OSS Friends" /> 37 + </div> 38 + <div className="flex flex-col gap-3 text-sm"> 39 + <p className="text-foreground font-semibold">Legal</p> 40 + <FooterLink href="/legal/terms" label="Terms" /> 41 + <FooterLink href="/legal/privacy" label="Privacy" /> 42 + </div> 40 43 </div> 44 + <StatusWidget slug="status" /> 41 45 </Shell> 42 46 </footer> 43 47 );
+3 -6
apps/web/src/components/marketing/partners.tsx
··· 2 2 3 3 export function Partners() { 4 4 return ( 5 - <div className="grid gap-2"> 5 + <div className="grid gap-4"> 6 6 <h3 className="text-muted-foreground font-cal text-center text-sm"> 7 7 Trusted By 8 8 </h3> 9 - <div className="grid grid-cols-1 gap-8 sm:grid-cols-3 sm:gap-16"> 9 + <div className="grid grid-cols-1 gap-8 sm:grid-cols-4 sm:gap-16"> 10 10 <div className="flex items-center justify-center"> 11 11 <a 12 12 href="https://status.hanko.io" ··· 53 53 </a> 54 54 </div> 55 55 <div className="flex items-center justify-center"> 56 - {/* <p className="text-muted-foreground text-xs">You</p> */} 57 - </div> 58 - <div className="flex items-center justify-center"> 59 - <p className="text-muted-foreground text-md">You</p> 56 + <p className="text-muted-foreground text-sm">You</p> 60 57 </div> 61 58 </div> 62 59 </div>
+67
apps/web/src/components/status-widget.tsx
··· 1 + import * as z from "zod"; 2 + 3 + const statusEnum = z.enum([ 4 + "operational", 5 + "degraded_performance", 6 + "partial_outage", 7 + "major_outage", 8 + "unknown", 9 + ]); 10 + 11 + const statusSchema = z.object({ status: statusEnum }); 12 + 13 + const dictionary = { 14 + operational: { 15 + label: "Operational", 16 + color: "bg-green-500", 17 + }, 18 + degraded_performance: { 19 + label: "Degraded Performance", 20 + color: "bg-yellow-500", 21 + }, 22 + partial_outage: { 23 + label: "Partial Outage", 24 + color: "bg-yellow-500", 25 + }, 26 + major_outage: { 27 + label: "Major Outage", 28 + color: "bg-red-500", 29 + }, 30 + unknown: { 31 + label: "Unknown", 32 + color: "bg-gray-500", 33 + }, 34 + under_maintenance: { 35 + label: "Under Maintenance", 36 + color: "bg-gray-500", 37 + }, 38 + } as const; 39 + 40 + export async function StatusWidget({ slug }: { slug: string }) { 41 + const res = await fetch(`https://api.openstatus.dev/public/status/${slug}`, { 42 + next: { revalidate: 60 }, // cache request for 60 seconds 43 + }); 44 + const data = await res.json(); 45 + const parsed = statusSchema.safeParse(data); 46 + 47 + let label = "Unknown"; 48 + let color = "bg-gray-500"; 49 + 50 + if (parsed.success) { 51 + const status = dictionary[parsed.data.status]; 52 + label = status.label; 53 + color = status.color; 54 + } 55 + 56 + return ( 57 + <a 58 + className="border-border text-foreground/70 hover:bg-muted hover:text-foreground inline-flex max-w-fit items-center gap-2 rounded-md border px-3 py-1 text-sm" 59 + href={`https://${slug}.openstatus.dev`} 60 + target="_blank" 61 + rel="noreferrer" 62 + > 63 + {label} 64 + <span className={`inline-block h-2 w-2 rounded-full ${color}`} /> 65 + </a> 66 + ); 67 + }