feat: More updates to hq
Chart Release / chart (push) Successful in 12s
Server Deploy / deploy (push) Failing after 1m11s

This commit is contained in:
2026-08-10 13:56:59 +01:00
parent 1fe4ba5999
commit 727bb09eff
11 changed files with 455 additions and 382 deletions
+36 -27
View File
@@ -7,6 +7,8 @@ import { PageFrame, RailCard, RailFacts } from "@/components/PageFrame";
import { PageHeader } from "@/components/PageHeader";
import { ManageBillingButton } from "@/components/ManageBillingButton";
import { TermSpark } from "@/components/TermBar";
import { EmptyState, Panel } from "@/components/Panel";
import { Sub, TBody, TD, TH, THead, TR, Table } from "@/components/Table";
import { formatDate, licenceState } from "@/lib/format";
export default function BillingPage() {
@@ -72,42 +74,49 @@ export default function BillingPage() {
</>
}
>
{rows.length === 0 ? (
<p className="rounded border border-rule bg-panel p-5 text-ink-2">You have no subscriptions. Cloud instances and self-hosted licences are both bought from the pricing page.</p>
) : (
<div className="overflow-x-auto rounded border border-rule bg-panel">
<table className="w-full border-collapse text-left">
<thead>
<tr className="border-b border-rule bg-panel-2 font-mono text-[0.68rem] uppercase tracking-[0.1em] text-ink-3">
<th className="px-4 py-2.5 font-normal">Instance</th>
<th className="px-4 py-2.5 font-normal">Plan</th>
<th className="px-4 py-2.5 font-normal">Term</th>
<th className="px-4 py-2.5 font-normal">Status</th>
<th className="px-4 py-2.5 font-normal">Renews</th>
</tr>
</thead>
<tbody>
<Panel title="Subscriptions" meta={rows.length ? `${rows.length}` : undefined} bodyless>
{rows.length === 0 ? (
<EmptyState
title="No subscriptions yet."
body="Cloud instances and self-hosted licences are both bought from the plan page, and each one bills separately."
/>
) : (
<Table>
<THead>
<TR className="hover:bg-transparent">
<TH>Instance</TH>
<TH>Plan</TH>
<TH>Billing</TH>
<TH>Status</TH>
<TH>Renews</TH>
</TR>
</THead>
<TBody>
{rows.map((s) => {
const start = periodStart(s.current_period_end, s.term);
const name = nameFor(s.instance_id);
return (
<tr key={s.subscription_id} className="border-b border-rule-soft last:border-0">
<td className="px-4 py-3">{nameFor(s.instance_id) ?? <span className="text-ink-3">Not linked yet</span>}</td>
<td className="px-4 py-3">{s.tier.replace("_", " ")}</td>
<td className="px-4 py-3">{s.term}</td>
<td className="px-4 py-3">{s.status}</td>
<td className="px-4 py-3">
<TR key={s.subscription_id}>
<TD>
{name ?? <span className="text-ink-3">Not linked yet</span>}
{name && <Sub>{s.instance_id?.slice(0, 8)}</Sub>}
</TD>
<TD>{s.tier.replace("_", " ")}</TD>
<TD className="text-ink-2">{s.term}</TD>
<TD className="text-ink-2">{s.status}</TD>
<TD>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
{start && <TermSpark issuedAt={start} expiresAt={s.current_period_end} state={licenceState(s.current_period_end, true)} />}
<span className="font-mono text-[0.78rem] tabular-nums text-ink-2">{formatDate(s.current_period_end)}</span>
</div>
</td>
</tr>
</TD>
</TR>
);
})}
</tbody>
</table>
</div>
)}
</TBody>
</Table>
)}
</Panel>
</PageFrame>
</div>
);
@@ -10,6 +10,7 @@ import { MembersPanel } from "@/components/MembersPanel";
import { RelinkPanel } from "@/components/RelinkPanel";
import { StatePill } from "@/components/StatePill";
import { TermBar } from "@/components/TermBar";
import { EmptyState, Note, Panel } from "@/components/Panel";
import { PageFrame, RailCard, RailFacts } from "@/components/PageFrame";
import { PageHeader } from "@/components/PageHeader";
import { formatDate, licenceState, limitLabel } from "@/lib/format";
@@ -143,36 +144,24 @@ export default function InstancePage() {
* text — which is the arithmetic this bar does for the reader.
*/}
{lic && (
<section className="grid gap-3 rounded border border-rule bg-panel p-5">
<div className="flex flex-wrap items-baseline justify-between gap-3">
<h2 className="text-[0.95rem] font-bold">Licence</h2>
<span className="font-mono text-[0.64rem] uppercase tracking-[0.14em] text-ink-3">
{lic.tier.replace("_", " ")} · {cloud ? "Cloud" : "Self-hosted"}
</span>
</div>
<Panel title="Licence" meta={`${lic.tier.replace("_", " ")} · ${cloud ? "Cloud" : "Self-hosted"}`}>
<TermBar issuedAt={lic.issued_at} expiresAt={lic.expires_at} state={state} />
{state === "warn" && (
<p className="rounded border border-rule border-l-[3px] border-l-warn bg-panel-2 px-3.5 py-2.5 text-[0.84rem] text-ink-2">
Renewing extends the term from the current expiry, not from today, so nothing is lost by renewing early.
</p>
)}
{state === "warn" && <Note tone="warn">Renewing extends the term from the current expiry, not from today, so nothing is lost by renewing early.</Note>}
{state === "expired" && (
<p className="rounded border border-rule border-l-[3px] border-l-expired bg-panel-2 px-3.5 py-2.5 text-[0.84rem] text-ink-2">
Servers and monitors keep running and your agents keep their keys. Changes are disabled until this is renewed.
</p>
<Note tone="expired">Servers and monitors keep running and your agents keep their keys. Changes are disabled until this is renewed.</Note>
)}
</section>
</Panel>
)}
{cloud ? (
<MembersPanel instanceId={instance.instance_id} />
) : (
<p className="rounded border border-rule bg-panel p-5 text-ink-2">
Users for this install are managed inside it, in Settings Instance. We do
not have access to your own deployment.
</p>
<Panel title="People">
<p className="text-[0.86rem] text-ink-2">
Users for this install are managed inside it, in Settings &rarr; Instance. We have no access to your own deployment.
</p>
</Panel>
)}
{lic && !cloud && (
@@ -193,9 +182,12 @@ export default function InstancePage() {
)}
{!lic && (
<p className="rounded border border-rule bg-panel p-5 text-ink-2">
No licence has been issued for this instance yet.
</p>
<Panel bodyless>
<EmptyState
title="No licence issued yet."
body="A licence binds to one install, so it is issued once this instance is linked to the ID its install reports."
/>
</Panel>
)}
</PageFrame>
</div>
+3 -8
View File
@@ -6,6 +6,7 @@ import { API_BASE, NotConnected, api, type License } from "@/lib/api";
import { NotConnectedPanel } from "@/components/NotConnected";
import { InstanceRecord } from "@/components/InstanceRecord";
import { PageFrame, RailCard, RailFacts } from "@/components/PageFrame";
import { Panel } from "@/components/Panel";
import { PageHeader } from "@/components/PageHeader";
import { LinkButton } from "@/components/Button";
import { StatePill } from "@/components/StatePill";
@@ -200,13 +201,7 @@ export default function OverviewPage() {
* nobody needs to read twice a week.
*/}
{attention.length > 0 && (
<section className="grid overflow-hidden rounded border border-rule bg-panel">
<div className="flex items-center justify-between gap-3 border-b border-rule-soft px-4 py-3">
<h2 className="text-[0.95rem] font-bold">Needs you</h2>
<span className="font-mono text-[0.64rem] uppercase tracking-[0.14em] text-ink-3">
{attention.length} {attention.length === 1 ? "item" : "items"}
</span>
</div>
<Panel title="Needs you" meta={`${attention.length} ${attention.length === 1 ? "item" : "items"}`} bodyless>
<ul className="grid">
{attention.map((a) => (
<li key={a.id} className="flex flex-wrap items-center justify-between gap-3 border-b border-rule-soft px-4 py-3 last:border-b-0">
@@ -221,7 +216,7 @@ export default function OverviewPage() {
</li>
))}
</ul>
</section>
</Panel>
)}
{live.map((i, n) => {