Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions components/OutageTable.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatDateTime } from "lib/format-date"
import type { Outage } from "lib/types"

function formatDuration(milliseconds: number): string {
Expand Down Expand Up @@ -47,10 +48,28 @@ export const OutageTable = ({
{outage.service}
</td>
<td className="py-2 px-4 whitespace-nowrap">
{outage.start.toLocaleString()}
<span
className="timestamp"
data-format="datetime"
data-timestamp={outage.start.toISOString()}
>
{formatDateTime(outage.start, {
timeZone: "UTC",
hour12: false,
})}
</span>
</td>
<td className="py-2 px-4 whitespace-nowrap">
{outage.end.toLocaleString()}
<span
className="timestamp"
data-format="datetime"
data-timestamp={outage.end.toISOString()}
>
{formatDateTime(outage.end, {
timeZone: "UTC",
hour12: false,
})}
</span>
</td>
<td className="py-2 px-4 whitespace-nowrap">
{formatDuration(outage.duration)}
Expand Down
46 changes: 40 additions & 6 deletions components/UptimeGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatDateTime } from "lib/format-date"
import type { StatusCheck } from "lib/types"

export function UptimeGraph({ checks }: { checks: StatusCheck[] }) {
Expand All @@ -10,8 +11,9 @@ export function UptimeGraph({ checks }: { checks: StatusCheck[] }) {

// Build the map and hours array in a single pass through checks
for (const check of checks) {
const date = new Date(check.timestamp)
const hourKey = `${date.toLocaleDateString()} ${date.getHours()}:00`
const hourStart = new Date(check.timestamp)
hourStart.setMinutes(0, 0, 0)
const hourKey = hourStart.toISOString()

if (!hourChecksMap.has(hourKey)) {
hourChecksMap.set(hourKey, [])
Expand Down Expand Up @@ -44,7 +46,12 @@ export function UptimeGraph({ checks }: { checks: StatusCheck[] }) {
<div
key={hour}
className="h-8 w-full bg-gray-200"
title={`${hour}\nNo data available for this period`}
data-hour-start={hour}
data-has-data="false"
title={`${formatDateTime(hour, {
timeZone: "UTC",
hour12: false,
})}\nNo data available for this period`}
/>
)
}
Expand All @@ -56,6 +63,24 @@ export function UptimeGraph({ checks }: { checks: StatusCheck[] }) {
(check) => check.status === "error",
)

const tooltipChecks: Array<{
timestamp: string
status: "ok" | "error"
}> = hourChecks.flatMap((check) => {
const serviceCheck = check.checks.find(
(c) => c.service === service,
)
if (!serviceCheck) {
return []
}
return [
{
timestamp: check.timestamp,
status: serviceCheck.status,
},
]
})

return (
<div
key={hour}
Expand All @@ -66,10 +91,19 @@ export function UptimeGraph({ checks }: { checks: StatusCheck[] }) {
? "bg-yellow-200"
: "bg-green-200"
}`}
title={`${hour}\n${hourChecks
data-hour-start={hour}
data-has-data="true"
data-tooltip-checks={JSON.stringify(tooltipChecks)}
title={`${formatDateTime(hour, {
timeZone: "UTC",
hour12: false,
})}\n${tooltipChecks
.map(
(check) =>
`${new Date(check.timestamp).toLocaleString()}: ${check.checks.find((c) => c.service === service)?.status === "error" ? "Issues Detected" : "Operational"}`,
(entry) =>
`${formatDateTime(entry.timestamp, {
timeZone: "UTC",
hour12: false,
})}: ${entry.status === "error" ? "Issues Detected" : "Operational"}`,
)
.join("\n")}`}
/>
Expand Down
13 changes: 13 additions & 0 deletions lib/format-date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function formatDateTime(
value: Date | string,
{ timeZone = "UTC", hour12 }: { timeZone?: string; hour12?: boolean } = {},
) {
const date = typeof value === "string" ? new Date(value) : value

return new Intl.DateTimeFormat("en-US", {
timeZone,
dateStyle: "medium",
timeStyle: "short",
...(hour12 !== undefined ? { hour12 } : {}),
}).format(date)
}
146 changes: 146 additions & 0 deletions scripts/generate-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,156 @@ async function generateSite() {
<body className="bg-gray-100 min-h-screen p-8">
<div className="max-w-6xl mx-auto">
<h1 className="text-3xl font-bold mb-8">tscircuit Status</h1>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-end gap-2 mb-6">
<label
htmlFor="timezone-selector"
className="text-sm font-medium text-gray-700"
>
Timezone
</label>
<select
id="timezone-selector"
className="border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="UTC">UTC</option>
</select>
</div>
<StatusGrid checks={checks} />
<UptimeGraph checks={checks} />
<OutageTable outages={outages} />
</div>
<script
dangerouslySetInnerHTML={{
__html: `
;(() => {
const defaultTimezone = "UTC"
const formatterCache = new Map()

const getFormatter = (timezone, hour12) => {
const key =
timezone +
"|" +
(hour12 === undefined ? "auto" : hour12 ? "12" : "24")
if (!formatterCache.has(key)) {
formatterCache.set(
key,
new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
dateStyle: "medium",
timeStyle: "short",
...(hour12 !== undefined ? { hour12 } : {}),
}),
)
}
return formatterCache.get(key)
}

const formatDate = (value, timezone) => {
const formatter = getFormatter(
timezone,
timezone === "UTC" ? false : undefined,
)
return formatter.format(new Date(value))
}

const updateTimestamps = (timezone) => {
document.querySelectorAll("[data-timestamp]").forEach((element) => {
const timestamp = element.getAttribute("data-timestamp")
if (!timestamp) return
element.textContent = formatDate(timestamp, timezone)
})
}

const updateTitles = (timezone) => {
document.querySelectorAll("[data-hour-start]").forEach((element) => {
const hourStart = element.getAttribute("data-hour-start")
if (!hourStart) return
const hasData = element.getAttribute("data-has-data") === "true"
const formattedHour = formatDate(hourStart, timezone)

if (!hasData) {
element.setAttribute(
"title",
formattedHour + "\\nNo data available for this period",
)
return
}

const checksData = element.getAttribute("data-tooltip-checks")
if (!checksData) {
element.setAttribute("title", formattedHour)
return
}

let checks
try {
checks = JSON.parse(checksData)
} catch (error) {
console.error("Failed to parse tooltip data", error)
element.setAttribute("title", formattedHour)
return
}

const lines = checks.map((entry) => {
const formattedTimestamp = formatDate(entry.timestamp, timezone)
const status =
entry.status === "error"
? "Issues Detected"
: "Operational"
return formattedTimestamp + ": " + status
})

element.setAttribute(
"title",
[formattedHour, ...lines].join("\\n"),
)
})
}

const applyTimezone = (timezone) => {
updateTimestamps(timezone)
updateTitles(timezone)
}

document.addEventListener("DOMContentLoaded", () => {
const selector = document.getElementById("timezone-selector")
if (selector) {
const existingValues = new Set(
Array.from(selector.options).map((option) => option.value),
)

const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (localTimezone && !existingValues.has(localTimezone)) {
const option = document.createElement("option")
option.value = localTimezone
option.textContent = localTimezone + " (Local)"
selector.insertBefore(option, selector.options[1] ?? null)
existingValues.add(localTimezone)
}

if (typeof Intl.supportedValuesOf === "function") {
Intl.supportedValuesOf("timeZone").forEach((timezone) => {
if (existingValues.has(timezone)) return
const option = document.createElement("option")
option.value = timezone
option.textContent = timezone
selector.append(option)
existingValues.add(timezone)
})
}

selector.value = defaultTimezone
selector.addEventListener("change", () => {
applyTimezone(selector.value)
})
}

applyTimezone(selector ? selector.value : defaultTimezone)
})
})()
`,
}}
/>
</body>
</html>,
)
Expand Down