eny.space Landingpage
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(dashboard): add pds health check

+120
+4
app/dashboard/page.tsx
··· 17 17 import { getPriceIdForPlan } from "@/lib/stripe-plans"; 18 18 import { getPdsServiceForCurrentUser } from "../api/pds/atproto/helpers"; 19 19 import { pdsStateLabel } from "@/lib/pds-state"; 20 + import { PdsHealthClient } from "./pds-health-client"; 20 21 21 22 type DashboardPageProps = { 22 23 searchParams?: Promise<{ ··· 104 105 </ButtonLink> 105 106 )} 106 107 </div> 108 + {pdsHostname && ( 109 + <PdsHealthClient pdsHost={`https://${pdsHostname}`} /> 110 + )} 107 111 </CardContent> 108 112 </Card> 109 113
+116
app/dashboard/pds-health-client.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useState } from "react"; 4 + import { Paragraph } from "@/components/paragraph"; 5 + 6 + type HealthStatus = "checking" | "reachable" | "unreachable"; 7 + 8 + type DescribeServer = { 9 + did?: string; 10 + availableUserDomains?: string[]; 11 + inviteCodeRequired?: boolean; 12 + links?: { 13 + privacyPolicy?: string; 14 + termsOfService?: string; 15 + }; 16 + }; 17 + 18 + function StatusDot({ status }: { status: HealthStatus }) { 19 + if (status === "checking") 20 + return <span className="inline-block h-2 w-2 rounded-full bg-white/30 animate-pulse" />; 21 + if (status === "reachable") 22 + return <span className="inline-block h-2 w-2 rounded-full bg-emerald-400" />; 23 + return <span className="inline-block h-2 w-2 rounded-full bg-rose-400" />; 24 + } 25 + 26 + export function PdsHealthClient({ pdsHost }: { pdsHost: string }) { 27 + const [status, setStatus] = useState<HealthStatus>("checking"); 28 + const [version, setVersion] = useState<string | null>(null); 29 + const [describe, setDescribe] = useState<DescribeServer | null>(null); 30 + const [checkedAt, setCheckedAt] = useState<Date | null>(null); 31 + 32 + const check = async () => { 33 + setStatus("checking"); 34 + const base = pdsHost.replace(/\/$/, ""); 35 + try { 36 + const [healthRes, describeRes] = await Promise.all([ 37 + fetch(`${base}/xrpc/_health`, { cache: "no-store" }), 38 + fetch(`${base}/xrpc/com.atproto.server.describeServer`, { cache: "no-store" }), 39 + ]); 40 + 41 + if (healthRes.ok) { 42 + const data = await healthRes.json().catch(() => ({})); 43 + setVersion(data?.version ?? null); 44 + setStatus("reachable"); 45 + } else { 46 + setStatus("unreachable"); 47 + } 48 + 49 + if (describeRes.ok) { 50 + setDescribe(await describeRes.json().catch(() => null)); 51 + } 52 + } catch { 53 + setStatus("unreachable"); 54 + } 55 + setCheckedAt(new Date()); 56 + }; 57 + 58 + useEffect(() => { 59 + check(); 60 + }, [pdsHost]); 61 + 62 + return ( 63 + <div className="space-y-3 rounded-md border border-white/10 bg-white/5 p-4 text-white backdrop-blur-xl"> 64 + <div className="flex items-center justify-between"> 65 + <div className="flex items-center gap-2"> 66 + <StatusDot status={status} /> 67 + <Paragraph className="text-sm font-semibold"> 68 + {status === "checking" && "Checking…"} 69 + {status === "reachable" && "Reachable"} 70 + {status === "unreachable" && "Unreachable"} 71 + </Paragraph> 72 + {version && ( 73 + <Paragraph className="text-xs text-white/50 font-mono">v{version}</Paragraph> 74 + )} 75 + </div> 76 + <button 77 + onClick={check} 78 + className="text-xs text-white/40 hover:text-white/80 transition-colors" 79 + > 80 + Refresh 81 + </button> 82 + </div> 83 + 84 + {checkedAt && ( 85 + <Paragraph className="text-xs text-white/40"> 86 + Last checked {checkedAt.toLocaleTimeString()} 87 + </Paragraph> 88 + )} 89 + 90 + {describe && ( 91 + <div className="grid gap-2 text-sm md:grid-cols-2"> 92 + {describe.did && ( 93 + <div className="space-y-0.5"> 94 + <Paragraph className="text-xs font-medium text-white/60">DID</Paragraph> 95 + <Paragraph className="font-mono text-xs text-white break-all">{describe.did}</Paragraph> 96 + </div> 97 + )} 98 + {Array.isArray(describe.availableUserDomains) && describe.availableUserDomains.length > 0 && ( 99 + <div className="space-y-0.5"> 100 + <Paragraph className="text-xs font-medium text-white/60">User domains</Paragraph> 101 + <Paragraph className="font-mono text-xs text-white"> 102 + {describe.availableUserDomains.join(", ")} 103 + </Paragraph> 104 + </div> 105 + )} 106 + {describe.inviteCodeRequired !== undefined && ( 107 + <div className="space-y-0.5"> 108 + <Paragraph className="text-xs font-medium text-white/60">Invite required</Paragraph> 109 + <Paragraph className="text-xs text-white">{describe.inviteCodeRequired ? "Yes" : "No"}</Paragraph> 110 + </div> 111 + )} 112 + </div> 113 + )} 114 + </div> 115 + ); 116 + }