import { useQuery } from "@tanstack/react-query" import Chart from "chart.js/auto" import { Fragment, useEffect, useId, useLayoutEffect, useRef, useState } from "react" import { beszelSystemsQuery } from "./beszel" import cn from "./components/lib/cn" import { StatusSeverity, formatLineName, getLineColor, getStatusBorderColor, tflDisruptionsQuery } from "./tfl" import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE, currentWeatherQuery, dailyForecastQuery, getWeatherIcon, weatherDescriptionQuery, } from "./weather" function App() { return (
) } function Tile({ decorations = true, children, className, }: { decorations?: boolean; children: React.ReactNode; className?: string }) { return (
{decorations && ( <>
)} {children}
) } function DateTimeTile() { const [time, setTime] = useState(new Date()) const formattedDate = time.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", }) const formattedTime = time.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }) useEffect(() => { const interval = setInterval(() => { setTime(new Date()) }, 1000) return () => clearInterval(interval) }, []) return (

{formattedDate}

{formattedTime}

) } function WeatherTile() { const { data: currentWeatherData, isLoading: isLoadingCurrentWeather, error: errorCurrentWeather, } = useQuery({ ...currentWeatherQuery(DEFAULT_LATITUDE, DEFAULT_LONGITUDE), refetchInterval: 5 * 60 * 1000, // 5 minutes refetchIntervalInBackground: true, }) const { data: dailyForecastData, isLoading: isLoadingDailyForecast, error: errorDailyForecast, } = useQuery({ ...dailyForecastQuery(DEFAULT_LATITUDE, DEFAULT_LONGITUDE), refetchInterval: 5 * 60 * 1000, // 5 minutes refetchIntervalInBackground: true, }) const { data: weatherDescriptionData, isLoading: isLoadingWeatherDescription, error: errorWeatherDescription, } = useQuery({ ...weatherDescriptionQuery(DEFAULT_LATITUDE, DEFAULT_LONGITUDE), refetchInterval: 60 * 60 * 1000, // 1 hour refetchIntervalInBackground: true, }) const isLoading = isLoadingCurrentWeather || isLoadingDailyForecast const error = errorCurrentWeather || errorDailyForecast if (isLoading) { return (

Loading weather

) } if (error || !currentWeatherData?.currentWeather) { return (

Error loading weather

{error?.message ?? "Unknown error"}

) } const currentWeather = currentWeatherData.currentWeather const temperature = Math.round(currentWeather.temperature) const lowTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMin ?? 0) const highTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMax ?? 0) const percentage = lowTemp && highTemp ? (temperature - lowTemp) / (highTemp - lowTemp) : 0 const highlightIndexStart = Math.floor((1 - percentage) * 23) const WeatherIcon = getWeatherIcon(currentWeather.conditionCode) let weatherDescriptionContent: string if (isLoadingWeatherDescription) { weatherDescriptionContent = "Loading weather description" } else if (errorWeatherDescription) { weatherDescriptionContent = `Error: ${errorWeatherDescription.message}` } else if (!weatherDescriptionData?.description) { weatherDescriptionContent = "No weather description available" } else { weatherDescriptionContent = weatherDescriptionData.description } return (

H:{highTemp}°

L:{lowTemp}°

{Array.from({ length: 24 }).map((_, index) => { if (index === highlightIndexStart) { return (
key={index} className={cn("w-10 bg-teal-400 h-[2px]")} />

{temperature}°

) } return (
key={index} className={cn( "w-4", index >= highlightIndexStart ? "bg-teal-400 w-8 h-[2px]" : "bg-neutral-400 w-4 h-[1px]", )} /> ) })}

{weatherDescriptionContent}

) } function TFLTile() { const { data: tflData, isLoading: isLoadingTFL, error: errorTFL, } = useQuery({ ...tflDisruptionsQuery(), select: (data) => { data.disruptions.sort((a, b) => { if (a.lineName.match(/northern/i)) return -1 return a.statusSeverity - b.statusSeverity }) return data }, refetchInterval: 5 * 60 * 1000, // 5 minutes refetchIntervalInBackground: true, }) if (isLoadingTFL) { return (

Loading tube status

) } if (errorTFL) { return (

Error loading from TfL

{errorTFL?.message}

) } if (!tflData) { return (

No TfL data available

) } return ( {tflData.goodService.includes("Northern") && ( )} {tflData.disruptions.map((disruption) => (
))}
) } function TFLDistruptionItem({ lineId, reason, severity }: { lineId: string; reason: string; severity: number }) { const lineName = formatLineName(lineId) return ( <>

{lineName}

{reason}

) } function SystemTile({ className, systemName, displayName, }: { className?: string; systemName: string; displayName: string }) { const { data } = useQuery({ ...beszelSystemsQuery(), refetchInterval: 1000, refetchIntervalInBackground: true, }) const chartRef = useRef(null) const beszelSystemsData = data?.systems.find((system) => system.name === systemName) const onCanvasRef = (elem: HTMLCanvasElement | null) => { if (!elem || chartRef.current) return const fillGradient = elem?.getContext("2d")?.createLinearGradient(0, 0, 0, elem.height) fillGradient?.addColorStop(0, "#2dd4bf") fillGradient?.addColorStop(0.5, "rgba(45, 212, 191, 0)") fillGradient?.addColorStop(1, "rgba(45, 212, 191, 0)") chartRef.current = new Chart(elem, { type: "line", data: { labels: Array.from({ length: 20 }, (_, index) => index), datasets: [ { data: Array.from({ length: 20 }, (_, i) => null), fill: true, backgroundColor: fillGradient, borderColor: "#2dd4bf", tension: 0.1, }, ], }, options: { responsive: true, scales: { x: { display: false }, y: { display: false, min: 0, max: 100 }, }, maintainAspectRatio: false, elements: { point: { radius: 0 }, line: { backgroundColor: "rgba(255, 255, 255, 0.5)", }, }, plugins: { legend: { display: false, }, }, }, }) } useLayoutEffect(() => { const cpu = beszelSystemsData?.info.cpu if (!chartRef.current || cpu === undefined) return const dataset = chartRef.current.data.datasets[0] const nextData = Array.from({ length: 20 }, (_, i) => { if (i === 19) { return null } return dataset.data[i + 1] }) nextData[19] = cpu dataset.data = nextData chartRef.current.update() }) if (!beszelSystemsData) { return (

No system status available

) } return (

{displayName}

CPU

{beszelSystemsData.info.cpu.toFixed(0).padStart(3, "0")}

RAM

{beszelSystemsData.info.ram.toFixed(0).padStart(3, "0")}

DSK

{beszelSystemsData.info.disk.toFixed(0).padStart(3, "0")}

) } export default App