page.tsx265 lines · main
| 1 | import type { Metadata } from 'next'; |
| 2 | import Link from 'next/link'; |
| 3 | |
| 4 | import { BackgroundGrid } from '../../components/marketing/background-grid'; |
| 5 | import { SiteFooter, WebDownLink } from '../../components/marketing/site-footer'; |
| 6 | import { SiteHeader } from '../../components/marketing/site-header'; |
| 7 | import { getSessionUser } from '../../lib/session'; |
| 8 | |
| 9 | export const metadata: Metadata = { |
| 10 | title: 'status — briven', |
| 11 | description: 'live health of every briven service. probes run when this page is rendered.', |
| 12 | }; |
| 13 | |
| 14 | export const dynamic = 'force-dynamic'; |
| 15 | export const revalidate = 0; |
| 16 | |
| 17 | interface ServiceProbe { |
| 18 | name: string; |
| 19 | url: string; |
| 20 | description: string; |
| 21 | expandChecks?: boolean; |
| 22 | } |
| 23 | |
| 24 | const PROBES: readonly ServiceProbe[] = [ |
| 25 | { |
| 26 | name: 'api', |
| 27 | url: process.env.BRIVEN_STATUS_API_URL ?? 'https://api.briven.tech/ready', |
| 28 | description: 'control plane — accounts, projects, billing, deploy intake', |
| 29 | expandChecks: true, |
| 30 | }, |
| 31 | { |
| 32 | name: 'realtime', |
| 33 | url: process.env.BRIVEN_STATUS_REALTIME_URL ?? 'https://realtime.briven.tech/ready', |
| 34 | description: 'reactive query websocket service', |
| 35 | }, |
| 36 | { |
| 37 | name: 'web', |
| 38 | url: process.env.BRIVEN_STATUS_WEB_URL ?? 'https://briven.tech/health', |
| 39 | description: 'marketing site + signed-in dashboard', |
| 40 | }, |
| 41 | { |
| 42 | name: 'docs', |
| 43 | url: process.env.BRIVEN_STATUS_DOCS_URL ?? 'https://docs.briven.tech', |
| 44 | description: 'developer docs + sdk reference', |
| 45 | }, |
| 46 | ]; |
| 47 | |
| 48 | interface ProbeResult { |
| 49 | name: string; |
| 50 | description: string; |
| 51 | ok: boolean; |
| 52 | status: number | null; |
| 53 | durationMs: number; |
| 54 | detail: string | null; |
| 55 | checks: Record<string, 'ok' | 'unreachable' | 'not_configured' | 'degraded'> | null; |
| 56 | } |
| 57 | |
| 58 | async function probe(svc: ServiceProbe): Promise<ProbeResult> { |
| 59 | const t0 = Date.now(); |
| 60 | try { |
| 61 | const res = await fetch(svc.url, { |
| 62 | signal: AbortSignal.timeout(3000), |
| 63 | headers: { accept: 'application/json' }, |
| 64 | cache: 'no-store', |
| 65 | }); |
| 66 | const durationMs = Date.now() - t0; |
| 67 | const body = await res.text().catch(() => ''); |
| 68 | let detail: string | null = null; |
| 69 | let checks: ProbeResult['checks'] = null; |
| 70 | if (svc.expandChecks) { |
| 71 | try { |
| 72 | const parsed = JSON.parse(body) as { checks?: Record<string, string> }; |
| 73 | if (parsed.checks) checks = parsed.checks as ProbeResult['checks']; |
| 74 | } catch { |
| 75 | // non-JSON body; checks stay null |
| 76 | } |
| 77 | } |
| 78 | if (!res.ok && !detail) { |
| 79 | detail = body.slice(0, 200) || null; |
| 80 | if (detail && body.length > 200) detail = `${detail}…`; |
| 81 | } |
| 82 | return { |
| 83 | name: svc.name, |
| 84 | description: svc.description, |
| 85 | ok: res.ok, |
| 86 | status: res.status, |
| 87 | durationMs, |
| 88 | detail, |
| 89 | checks, |
| 90 | }; |
| 91 | } catch (err) { |
| 92 | return { |
| 93 | name: svc.name, |
| 94 | description: svc.description, |
| 95 | ok: false, |
| 96 | status: null, |
| 97 | durationMs: Date.now() - t0, |
| 98 | detail: err instanceof Error ? err.message : 'unreachable', |
| 99 | checks: null, |
| 100 | }; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | export default async function StatusPage() { |
| 105 | const [user, probes] = await Promise.all([ |
| 106 | getSessionUser().catch(() => null), |
| 107 | Promise.all(PROBES.map(probe)), |
| 108 | ]); |
| 109 | const allOk = probes.every((p) => p.ok); |
| 110 | const anyDown = probes.some((p) => !p.ok); |
| 111 | |
| 112 | return ( |
| 113 | <main className="relative min-h-dvh overflow-hidden bg-[var(--color-bg)] text-[var(--color-text)]"> |
| 114 | <BackgroundGrid /> |
| 115 | <SiteHeader user={user} /> |
| 116 | |
| 117 | <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-12 pt-16 sm:pt-20"> |
| 118 | <p className="font-mono uppercase tracking-[0.12em] text-[var(--color-primary)] text-[var(--text-xs)]"> |
| 119 | status |
| 120 | </p> |
| 121 | <h1 className="mt-4 font-sans font-medium leading-[1.05] tracking-[-0.03em] text-[var(--color-text)] text-[var(--text-display-3)]"> |
| 122 | briven, live. |
| 123 | </h1> |
| 124 | <p className="mt-4 max-w-2xl leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-body)]"> |
| 125 | probes run when this page is rendered — no cached numbers, no marketing dashboards. for |
| 126 | incident history and post-mortems, see the{' '} |
| 127 | <Link |
| 128 | href="https://docs.briven.tech/status" |
| 129 | className="text-[var(--color-text-link)] underline-offset-2 hover:underline" |
| 130 | > |
| 131 | full status page |
| 132 | </Link>{' '} |
| 133 | on docs. |
| 134 | </p> |
| 135 | <p className="mt-4 font-mono text-xs text-[var(--color-text-subtle)]"> |
| 136 | every briven server & domain is guarded around the clock by{' '} |
| 137 | <WebDownLink>web down</WebDownLink> — our independent uptime watchdog. |
| 138 | </p> |
| 139 | </section> |
| 140 | |
| 141 | <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-4"> |
| 142 | <div className="rounded-[var(--radius-md)] border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-5 font-mono text-xs text-[var(--color-text-muted)]"> |
| 143 | <p className="text-[var(--color-text)]">what if sign-in feels broken?</p> |
| 144 | <ul className="mt-3 list-disc space-y-2 pl-5"> |
| 145 | <li> |
| 146 | check <code className="text-[var(--color-text)]">redis</code> under the api probe |
| 147 | above — rate limits need it healthy; logins can still work if only cache is down. |
| 148 | </li> |
| 149 | <li> |
| 150 | already-open sessions usually keep working; new sign-ups / magic links need the mail |
| 151 | path. |
| 152 | </li> |
| 153 | <li> |
| 154 | we post short incident notes here and on{' '} |
| 155 | <Link |
| 156 | href="https://docs.briven.tech/status" |
| 157 | className="text-[var(--color-text-link)] underline-offset-2 hover:underline" |
| 158 | > |
| 159 | docs status |
| 160 | </Link>{' '} |
| 161 | when something is customer-visible. |
| 162 | </li> |
| 163 | </ul> |
| 164 | </div> |
| 165 | </section> |
| 166 | |
| 167 | <section className="relative z-10 mx-auto w-full max-w-4xl px-6"> |
| 168 | <div |
| 169 | className={`rounded-[var(--radius-md)] border p-5 font-mono text-sm ${ |
| 170 | allOk |
| 171 | ? 'border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)]' |
| 172 | : 'border-[var(--color-error)] bg-[var(--color-surface)]' |
| 173 | }`} |
| 174 | > |
| 175 | <p className="text-[var(--color-text)]"> |
| 176 | <strong> |
| 177 | {allOk |
| 178 | ? 'all systems operational' |
| 179 | : anyDown |
| 180 | ? 'incident in progress' |
| 181 | : 'partial degradation'} |
| 182 | </strong> |
| 183 | </p> |
| 184 | <p className="mt-1 text-xs text-[var(--color-text-muted)]"> |
| 185 | checked at {new Date().toISOString()} · ttl 0s |
| 186 | </p> |
| 187 | </div> |
| 188 | </section> |
| 189 | |
| 190 | <section className="relative z-10 mx-auto mt-8 w-full max-w-4xl px-6 pb-20"> |
| 191 | <ul className="flex flex-col gap-3"> |
| 192 | {probes.map((p) => ( |
| 193 | <li |
| 194 | key={p.name} |
| 195 | className="rounded-[var(--radius-md)] border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-5" |
| 196 | > |
| 197 | <div className="flex items-start justify-between gap-4"> |
| 198 | <div> |
| 199 | <p className="font-mono text-sm text-[var(--color-text)]">{p.name}</p> |
| 200 | <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]"> |
| 201 | {p.description} |
| 202 | </p> |
| 203 | </div> |
| 204 | <div className="text-right"> |
| 205 | <span |
| 206 | className={`inline-flex items-center gap-2 rounded-[var(--radius-full)] border px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider ${ |
| 207 | p.ok |
| 208 | ? 'border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)] text-[var(--color-primary)]' |
| 209 | : 'border-[var(--color-error)] text-[var(--color-error)]' |
| 210 | }`} |
| 211 | > |
| 212 | <span |
| 213 | className={`h-1.5 w-1.5 rounded-full ${p.ok ? 'bg-[var(--color-primary)]' : 'bg-[var(--color-error)]'}`} |
| 214 | /> |
| 215 | {p.ok ? 'ok' : 'down'} |
| 216 | </span> |
| 217 | <p className="mt-1.5 font-mono text-[10px] text-[var(--color-text-subtle)]"> |
| 218 | {p.status ?? '—'} · {p.durationMs}ms |
| 219 | </p> |
| 220 | </div> |
| 221 | </div> |
| 222 | |
| 223 | {p.checks ? ( |
| 224 | <ul className="mt-4 grid grid-cols-2 gap-2 border-t border-[var(--color-border-subtle)] pt-4 sm:grid-cols-3 md:grid-cols-4"> |
| 225 | {Object.entries(p.checks).map(([k, v]) => ( |
| 226 | <li key={k} className="flex items-center gap-2"> |
| 227 | <span |
| 228 | className={`h-1.5 w-1.5 rounded-full ${ |
| 229 | v === 'ok' |
| 230 | ? 'bg-[var(--color-primary)]' |
| 231 | : v === 'degraded' |
| 232 | ? 'bg-[var(--color-warning)]' |
| 233 | : 'bg-[var(--color-error)]' |
| 234 | }`} |
| 235 | /> |
| 236 | <span className="font-mono text-xs text-[var(--color-text-muted)]"> |
| 237 | {k} |
| 238 | </span> |
| 239 | <span className="ml-auto font-mono text-[10px] text-[var(--color-text-subtle)]"> |
| 240 | {v} |
| 241 | </span> |
| 242 | </li> |
| 243 | ))} |
| 244 | </ul> |
| 245 | ) : null} |
| 246 | |
| 247 | {p.detail ? ( |
| 248 | <pre className="mt-3 overflow-x-auto rounded-[var(--radius-sm)] bg-[var(--color-code-bg)] p-3 font-mono text-[11px] text-[var(--color-code-text)]"> |
| 249 | {p.detail} |
| 250 | </pre> |
| 251 | ) : null} |
| 252 | </li> |
| 253 | ))} |
| 254 | </ul> |
| 255 | |
| 256 | <p className="mt-8 font-mono text-xs text-[var(--color-text-subtle)]"> |
| 257 | probes timeout at 3000ms. /ready returns the service's view of its dependencies; |
| 258 | /health only confirms the process is alive. |
| 259 | </p> |
| 260 | </section> |
| 261 | |
| 262 | <SiteFooter /> |
| 263 | </main> |
| 264 | ); |
| 265 | } |