SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
10.3 KB · 234 lines tsx
Raw Blame History
1import Link from "next/link";2import { requireAdmin } from "@/lib/session";3import { internalApi, InternalApiError } from "@/lib/api";4import { getEnvPresence, getFeatureFlags, getIncidents, getTableCounts } from "@/lib/queries/admin";5import { formatNumber, titleCase } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Alert } from "@/components/ui/alert";8import { Badge, StatusBadge } from "@/components/ui/badge";9import { Button } from "@/components/ui/button";10import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table";11import { DateCell, KV, Mono, Panel, numCell } from "@/components/admin/primitives";12import { FlagSwitch } from "@/components/admin/flag-controls";13import { NewIncidentDialog, ResolveIncidentButton } from "@/components/admin/incident-controls";1415export const dynamic = "force-dynamic";1617async function safe<T>(fn: () => Promise<T>): Promise<{ ok: true; value: T } | { ok: false; error: string }> {18  try {19    return { ok: true, value: await fn() };20  } catch (e) {21    return { ok: false, error: e instanceof InternalApiError ? `${e.code}: ${e.message}` : (e as Error).message };22  }23}2425export default async function AdminSystemPage() {26  await requireAdmin();27  const [health, ready, dbStats, flags, incidents] = await Promise.all([safe(() => internalApi.health()), safe(() => internalApi.ready()), safe(getTableCounts), getFeatureFlags(), getIncidents(50)]);28  const env = getEnvPresence();29  const groups = Array.from(new Set(env.map((e) => e.group)));30  const openIncidents = incidents.filter((i) => !i.resolvedAt).length;3132  return (33    <>34      <PageHeader eyebrow="Operations" title="System" description="Service health, database footprint, environment presence, feature flags and public status incidents." actions={<NewIncidentDialog />} />3536      <div className="grid gap-6 lg:grid-cols-2">37        <Panel title="API service" description="GET /health and /ready on the Fastify service, called server-side with the internal token.">38          {health.ok ? (39            <KV40              cols={2}41              items={[42                { label: "Health", value: <StatusBadge status={health.value.status === "ok" ? "healthy" : health.value.status} /> },43                { label: "Version", value: health.value.version, mono: true },44                { label: "Readiness", value: ready.ok ? <StatusBadge status={ready.value.status === "ok" || ready.value.status === "ready" ? "healthy" : ready.value.status} /> : <StatusBadge status="down" /> },45                { label: "Available networks", value: ready.ok && ready.value.available_networks.length ? ready.value.available_networks.join(", ") : "none" },46                {47                  label: "Readiness checks",48                  span: true,49                  value: ready.ok ? (50                    Object.keys(ready.value.checks).length ? (51                      <ul className="flex flex-wrap gap-1.5">52                        {Object.entries(ready.value.checks).map(([k, v]) => (53                          <li key={k}>54                            <Badge variant={v ? "success" : "danger"} dot>55                              {k}56                            </Badge>57                          </li>58                        ))}59                      </ul>60                    ) : (61                      <span className="text-fg-muted">no check details (degraded)</span>62                    )63                  ) : (64                    <span className="text-danger">{ready.error}</span>65                  ),66                },67              ]}68            />69          ) : (70            <Alert variant="danger" title="API unreachable">71              {health.error}72            </Alert>73          )}74        </Panel>7576        <Panel title="Database" description={dbStats.ok ? `${dbStats.value.pgVersion} · ${dbStats.value.dbSize}` : undefined} flush>77          {dbStats.ok ? (78            <Table>79              <TableHeader>80                <TableRow>81                  <TableHead>Table</TableHead>82                  <TableHead className="text-right">Rows</TableHead>83                </TableRow>84              </TableHeader>85              <TableBody>86                {Object.entries(dbStats.value.counts).map(([t, n]) => (87                  <TableRow key={t}>88                    <TableCell>89                      <Mono>{t}</Mono>90                    </TableCell>91                    <TableCell className={numCell}>{formatNumber(n)}</TableCell>92                  </TableRow>93                ))}94              </TableBody>95            </Table>96          ) : (97            <div className="p-4">98              <Alert variant="danger" title="Database query failed">99                {dbStats.error}100              </Alert>101            </div>102          )}103        </Panel>104      </div>105106      <Panel title="Environment presence" description="Whether each variable is set in the web process. Values are never read or displayed. The API service has its own environment — provider credentials must be set there too." className="mt-6">107        <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">108          {groups.map((g) => (109            <div key={g}>110              <h3 className="mb-1.5 text-[12px] font-semibold uppercase tracking-wide text-fg-subtle">{g}</h3>111              <ul className="space-y-1">112                {env113                  .filter((e) => e.group === g)114                  .map((e) => (115                    <li key={e.key} className="flex items-center justify-between gap-2 text-[12.5px]">116                      <Mono>{e.key}</Mono>117                      {e.present ? <Badge variant="success" dot>set</Badge> : <Badge variant="outline">missing</Badge>}118                    </li>119                  ))}120              </ul>121            </div>122          ))}123        </div>124      </Panel>125126      <div className="mt-6 grid gap-6 xl:grid-cols-2">127        <Panel title="Feature flags" right={<Button asChild variant="ghost" size="xs"><Link href="/admin/flags">Manage →</Link></Button>} flush>128          <Table>129            <TableHeader>130              <TableRow>131                <TableHead>Key</TableHead>132                <TableHead>Description</TableHead>133                <TableHead>Targeting</TableHead>134                <TableHead>Enabled</TableHead>135              </TableRow>136            </TableHeader>137            <TableBody>138              {flags.length === 0 ? (139                <TableEmpty colSpan={4}>140                  No flags. <Link href="/admin/flags" className="text-accent hover:underline">Create one</Link>.141                </TableEmpty>142              ) : (143                flags.map((f) => (144                  <TableRow key={f.key}>145                    <TableCell>146                      <Mono>{f.key}</Mono>147                    </TableCell>148                    <TableCell className="max-w-[260px] truncate text-fg-muted">{f.description ?? "—"}</TableCell>149                    <TableCell className="text-[12px] text-fg-muted">150                      {f.plans?.length ? `plans: ${f.plans.join(", ")}` : null}151                      {f.plans?.length && f.organizationIds?.length ? " · " : null}152                      {f.organizationIds?.length ? `${f.organizationIds.length} org(s)` : null}153                      {!f.plans?.length && !f.organizationIds?.length ? "everyone" : null}154                    </TableCell>155                    <TableCell>156                      <FlagSwitch flagKey={f.key} enabled={f.enabled} />157                    </TableCell>158                  </TableRow>159                ))160              )}161            </TableBody>162          </Table>163        </Panel>164165        <Panel166          title={167            <span className="flex items-center gap-2">168              Status incidents {openIncidents ? <Badge variant="danger" dot>{openIncidents} open</Badge> : <Badge variant="success" dot>all resolved</Badge>}169            </span>170          }171          description="Shown on the public /status page. Write for customers; never name upstream providers."172          flush173        >174          <Table>175            <TableHeader>176              <TableRow>177                <TableHead>Started</TableHead>178                <TableHead>Component</TableHead>179                <TableHead>Severity</TableHead>180                <TableHead>Title</TableHead>181                <TableHead>Status</TableHead>182                <TableHead></TableHead>183              </TableRow>184            </TableHeader>185            <TableBody>186              {incidents.length === 0 ? (187                <TableEmpty colSpan={6}>No incidents recorded. The status page shows all systems operational.</TableEmpty>188              ) : (189                incidents.map((i) => (190                  <TableRow key={i.id} className={i.resolvedAt ? "opacity-70" : undefined}>191                    <TableCell>192                      <DateCell value={i.startedAt} relative={false} />193                    </TableCell>194                    <TableCell>195                      <Badge variant="outline">{titleCase(i.component)}</Badge>196                    </TableCell>197                    <TableCell>198                      <Badge variant={i.severity === "critical" || i.severity === "major" ? "danger" : i.severity === "maintenance" ? "info" : "warning"}>{i.severity}</Badge>199                    </TableCell>200                    <TableCell className="max-w-[280px]">201                      <div className="truncate font-medium" title={i.title}>202                        {i.title}203                      </div>204                      {i.body ? (205                        <div className="truncate text-[12px] text-fg-muted" title={i.body}>206                          {i.body}207                        </div>208                      ) : null}209                    </TableCell>210                    <TableCell>211                      {i.resolvedAt ? (212                        <span className="text-[12px] text-fg-muted">213                          resolved <DateCell value={i.resolvedAt} />214                        </span>215                      ) : (216                        <Badge variant="danger" dot>217                          open218                        </Badge>219                      )}220                    </TableCell>221                    <TableCell className="text-right">222                      <ResolveIncidentButton id={i.id} resolved={Boolean(i.resolvedAt)} />223                    </TableCell>224                  </TableRow>225                ))226              )}227            </TableBody>228          </Table>229        </Panel>230      </div>231    </>232  );233}234