Files
threadcount-community/app/ops/(console)/page.tsx
T
ThreadCount 344b1701dd ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
2026-09-13 08:54:35 +10:00

79 lines
6.2 KiB
TypeScript

import { requireOperator } from "@/lib/ops/session";
import { overview } from "@/lib/ops/projections";
import { version } from "@/lib/ops/version";
/* Overview: is everything up, how many facilities, who needs attention. Counts and dates only —
* nothing on this screen names a person or reads a record. */
export const dynamic = "force-dynamic";
const tile: React.CSSProperties = { background: "var(--color-bg)", padding: 16 };
const figure: React.CSSProperties = { fontSize: 30, fontWeight: 800, lineHeight: 1, fontVariantNumeric: "tabular-nums" };
const label: React.CSSProperties = { fontSize: 11, fontWeight: 700, letterSpacing: "0.09em", textTransform: "uppercase", color: "var(--color-neutral-600)", marginTop: 6 };
const note: React.CSSProperties = { fontSize: 12, color: "var(--color-neutral-700)", marginTop: 2 };
export default async function OpsOverview() {
await requireOperator();
const o = await overview();
const v = version();
const stale = !!(v.deployed && v.running && v.deployed.sha !== v.running);
const fmtAt = (d: Date) => d.toLocaleString("en-AU", { timeZone: "Australia/Brisbane", dateStyle: "medium", timeStyle: "short" });
const attention = [
...o.overdueBackups.map((f) => ({ tone: "warn", who: f.name, what: f.backupAgeDays === null ? "has never exported a backup" : `has not exported a backup in ${f.backupAgeDays} days`, meta: `${f.issues.toLocaleString()} issues on record`, href: `/ops/facilities/${f.id}` })),
...o.neverIssued.map((f) => ({ tone: "dormant", who: f.name, what: "has never issued", meta: `signed up ${f.createdAt.toLocaleDateString("en-AU", { day: "numeric", month: "short" })} · ${f.staff} staff · ${f.items} items`, href: `/ops/facilities/${f.id}` })),
...o.unconfiguredList.map((f) => ({ tone: "dormant", who: f.name, what: "has no staff groups named", meta: "everyone is on manager approval until a group goes on a route", href: `/ops/facilities/${f.id}` })),
];
const drift = o.migrations.applied !== o.migrations.inRepo;
return (
<>
<div className="page-head">
<div>
<div className="sec" style={{ border: 0, padding: 0 }}>Overview</div>
<h1 className="h1">Everything, at a glance</h1>
<div style={{ fontSize: 13, color: "var(--color-neutral-700)", marginTop: 4 }}>
{o.facilities} facilities · {o.live} live · migrations {o.migrations.applied}/{o.migrations.inRepo}{drift ? " — DRIFT" : ""}
</div>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 2, background: "var(--color-divider)", border: "2px solid var(--color-text)", marginTop: 20 }}>
<div style={tile}><div style={figure}>{o.facilities}</div><div style={label}>Facilities</div><div style={note}>{o.demo} demo, excluded below</div></div>
<div style={tile}><div style={figure}>{o.live}</div><div style={label}>Live</div><div style={note}>have issued at least once</div></div>
<div style={tile}><div style={figure}>{o.dormant + o.unconfigured}</div><div style={label}>Not yet live</div><div style={note}>{o.unconfigured} never set up</div></div>
<div style={tile}><div style={figure}>{o.signupsThisMonth}</div><div style={label}>Signups this month</div><div style={note}>{o.reachedFirstIssue} reached first issue</div></div>
</div>
<div className="tc-panel" style={{ marginTop: 24 }}>
<div className="tc-panel-head"><span>Needs attention</span><span className="tc-panel-aside">{attention.length} {attention.length === 1 ? "item" : "items"}</span></div>
{attention.length === 0 ? (
<div className="tc-panel-body" style={{ fontSize: 13, color: "var(--color-neutral-700)" }}>Nothing. Every live facility has a recent backup and every signup has issued.</div>
) : (
<ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
{attention.map((a, i) => (
<li key={i} style={{ display: "flex", gap: 12, alignItems: "baseline", padding: "11px 16px", borderBottom: "1px solid var(--color-divider)" }}>
<span style={{ width: 4, alignSelf: "stretch", flex: "none", background: a.tone === "warn" ? "#8a6218" : "var(--color-neutral-600)" }} />
<span style={{ flex: 1, minWidth: 0 }}><b>{a.who}</b> {a.what}<div style={{ fontSize: 12, color: "var(--color-neutral-700)" }}>{a.meta}</div></span>
<a className="btn" href={a.href} style={{ padding: "3px 9px", fontSize: 12 }}>Open</a>
</li>
))}
</ul>
)}
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 24, marginTop: 24 }}>
<div className="tc-panel">
<div className="tc-panel-head"><span>Platform</span></div>
<dl className="tc-panel-body" style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "8px 16px", fontSize: 13, margin: 0 }}>
<dt style={{ color: "var(--color-neutral-600)" }}>Migrations</dt><dd style={{ margin: 0, fontWeight: 600 }}>{o.migrations.applied} applied / {o.migrations.inRepo} in repo{drift ? " — drift" : ""}</dd>
<dt style={{ color: "var(--color-neutral-600)" }}>Enquiries</dt><dd style={{ margin: 0, fontWeight: 600 }}>{o.enquiriesUnhandled} unhandled</dd>
<dt style={{ color: "var(--color-neutral-600)" }}>Demo facility</dt><dd style={{ margin: 0, fontWeight: 600 }}>{o.demoRebuiltAgoMin === null ? "none" : `rebuilt ${o.demoRebuiltAgoMin} min ago`}</dd>
{/* Whole phrases (see the facility page): React puts comment nodes between adjacent expressions. */}
<dt style={{ color: "var(--color-neutral-600)" }}>Deployed</dt><dd style={{ margin: 0, fontWeight: 600 }}>{v.deployed ? `${v.deployed.sha} · ${fmtAt(v.deployed.at)}${v.deployed.from ? ` · was ${v.deployed.from}` : ""}` : "not recorded — the deploy script predates the release file"}</dd>
<dt style={{ color: "var(--color-neutral-600)" }}>Running</dt><dd style={{ margin: 0, fontWeight: 600, color: stale ? "var(--color-accent-700)" : undefined }}>{`${v.running || "release unknown"}${stale ? " — STALE: built but never restarted" : ""} · up ${v.uptimeMin} min · node ${v.node}`}</dd>
</dl>
</div>
</div>
</>
);
}