Mirror.jsx
2.6 kB · jsx · 98 lines
1import { Fragment, useEffect, useState } from "react";23const EVERY = 60 * 1000;45const LINE = /^(\S+)\s+(DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+(.*)$/;67export const when = (seconds) => (seconds ? new Date(seconds * 1000).toISOString().replace("T", " ").slice(0, 19) + " UTC" : "never");89export const ago = (seconds) => {10 if (!seconds) return "never";11 const gap = Math.max(0, Math.floor(Date.now() / 1000 - seconds));12 if (gap < 90) return `${gap} s ago`;13 if (gap < 5400) return `${Math.round(gap / 60)} min ago`;14 if (gap < 172800) return `${(gap / 3600).toFixed(1)} h ago`;15 return `${Math.round(gap / 86400)} d ago`;16};1718export const bytes = (n) => (n == null ? "" : n < 1e6 ? `${(n / 1e3).toFixed(0)} kB` : n < 1e9 ? `${(n / 1e6).toFixed(1)} MB` : `${(n / 1e9).toFixed(2)} GB`);1920async function grab(path) {21 try {22 const reply = await fetch(path, { cache: "no-store" });23 return reply.ok ? await reply.json() : null;24 } catch {25 return null;26 }27}2829export function useMirror(path) {30 const [seen, setSeen] = useState(null);3132 useEffect(() => {33 let live = true;34 let timer = 0;35 const refresh = async () => {36 const data = await grab(path);37 if (live) setSeen({ data });38 };39 const run = () => {40 clearInterval(timer);41 if (document.hidden) return;42 void refresh();43 timer = setInterval(refresh, EVERY);44 };45 document.addEventListener("visibilitychange", run);46 run();47 return () => {48 live = false;49 clearInterval(timer);50 document.removeEventListener("visibilitychange", run);51 };52 }, [path]);5354 return seen;55}5657export function Waiting({ seen }) {58 return <p className="fine">{seen ? "No data yet." : "Loading"}</p>;59}6061export function Rows({ list }) {62 return (63 <table>64 <tbody>65 {list.map(([name, value]) => (66 <tr key={name}>67 <th>{name}</th>68 <td>{String(value ?? "")}</td>69 </tr>70 ))}71 </tbody>72 </table>73 );74}7576export function Code({ lines }) {77 return (78 <pre>79 <code>80 {lines.map((line, i) => {81 const hit = LINE.exec(line);82 return (83 <Fragment key={i}>84 {hit ? (85 <span className={hit[2].toLowerCase()}>86 <span className="time">{hit[1]}</span> <span className="level">{hit[2].padEnd(7)}</span> <span className="text">{hit[3]}</span>87 </span>88 ) : (89 <span className="text">{line}</span>90 )}91 {i < lines.length - 1 ? "\n" : null}92 </Fragment>93 );94 })}95 </code>96 </pre>97 );98}