Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| sudo: Invoices, Audit log, and follow-ups due | 1 | /** |
| 2 | * The Invoices and Audit log pages' arithmetic and wording: filters read | |
| 3 | * from the address, totals, links from billing's account ids to sudo's | |
| 4 | * pages, and paging. No Workers imports, so it can be tested under Node. | |
| 5 | */ | |
| 6 | import type { AdminAction, InvoiceSummary } from "@g1t/contracts"; | |
| 7 | ||
| 8 | // --- Invoices ----------------------------------------------------------------- | |
| 9 | ||
| 10 | export const INVOICE_STATUSES = ["open", "overdue", "failed", "paid", "void"] as const; | |
| 11 | export type InvoiceStatusFilter = (typeof INVOICE_STATUSES)[number]; | |
| 12 | ||
| 13 | export function parseInvoiceStatus(value: string | null): InvoiceStatusFilter | null { | |
| 14 | return INVOICE_STATUSES.find((status) => status === value) ?? null; | |
| 15 | } | |
| 16 | ||
| 17 | /** A month as `<input type="month">` sends it: `2026-10`. */ | |
| 18 | export function parseMonth(value: string | null): string | null { | |
| 19 | if (!value || !/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) return null; | |
| 20 | return value; | |
| 21 | } | |
| 22 | ||
| 23 | /** Not paid and not void: what is still owed. */ | |
| 24 | export function isOutstanding(status: string): boolean { | |
| 25 | return status === "open" || status === "overdue" || status === "failed"; | |
| 26 | } | |
| 27 | ||
| 28 | export type InvoiceTotals = { count: number; amountMicros: number; paidMicros: number; outstandingMicros: number; outstanding: number }; | |
| 29 | ||
| 30 | export function invoiceTotals(invoices: Pick<InvoiceSummary, "amountMicros" | "status">[]): InvoiceTotals { | |
| 31 | const totals: InvoiceTotals = { count: invoices.length, amountMicros: 0, paidMicros: 0, outstandingMicros: 0, outstanding: 0 }; | |
| 32 | for (const invoice of invoices) { | |
| 33 | if (invoice.status === "void") continue; | |
| 34 | totals.amountMicros += invoice.amountMicros; | |
| 35 | if (invoice.status === "paid") totals.paidMicros += invoice.amountMicros; | |
| 36 | if (isOutstanding(invoice.status)) { | |
| 37 | totals.outstandingMicros += invoice.amountMicros; | |
| 38 | totals.outstanding += 1; | |
| 39 | } | |
| 40 | } | |
| 41 | return totals; | |
| 42 | } | |
| 43 | ||
| 44 | /** A link to the invoices list with these filters; the defaults are left out. */ | |
| 45 | export function invoicesHref({ status, month }: { status?: string | null; month?: string | null }): string { | |
| 46 | const params = new URLSearchParams(); | |
| 47 | if (status) params.set("status", status); | |
| 48 | if (month) params.set("month", month); | |
| 49 | const query = params.toString(); | |
| 50 | return query ? `/invoices?${query}` : "/invoices"; | |
| 51 | } | |
| 52 | ||
| 53 | /** Only https links to Stripe are followed. */ | |
| 54 | export function safeUrl(url: string | null | undefined): string | null { | |
| 55 | return url && url.startsWith("https://") ? url : null; | |
| 56 | } | |
| 57 | ||
| 58 | // --- Accounts ------------------------------------------------------------------- | |
| 59 | ||
| 60 | const SLUG = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/; | |
| 61 | const ENTERPRISE = /^ent_[a-z0-9_-]{1,80}$/; | |
| 62 | ||
| 63 | /** | |
| 64 | * The sudo page for one of billing's account ids: `ws_<slug>` is the | |
| 65 | * workspace's, `ent_…` the enterprise's. Null for anything else, such as | |
| 66 | * Stripe's own lines. | |
| 67 | */ | |
| 68 | export function accountPath(account: string | null | undefined): string | null { | |
| 69 | const id = (account ?? "").trim().toLowerCase(); | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 70 | const status = /^(incident|maintenance):([a-z0-9-]{1,64})$/.exec(id); |
| 71 | if (status) return status[1] === "incident" ? `/incidents/${status[2]}` : `/incidents/maintenance/${status[2]}`; | |
| sudo: Invoices, Audit log, and follow-ups due | 72 | if (ENTERPRISE.test(id)) return `/enterprises/${encodeURIComponent(id)}`; |
| 73 | if (id.startsWith("ws_") && SLUG.test(id.slice(3))) return `/workspaces/${encodeURIComponent(id.slice(3))}`; | |
| 74 | return null; | |
| 75 | } | |
| 76 | ||
| 77 | /** What to call an account: a workspace by its slug, an enterprise by its name when known. */ | |
| 78 | export function accountName(account: string, names: Map<string, string> = new Map()): string { | |
| 79 | if (names.has(account)) return names.get(account) as string; | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 80 | if (account.startsWith("incident:")) return "Incident"; |
| 81 | if (account.startsWith("maintenance:")) return "Maintenance"; | |
| sudo: Invoices, Audit log, and follow-ups due | 82 | if (account.startsWith("ws_")) return account.slice(3); |
| 83 | if (ENTERPRISE.test(account)) return "an enterprise"; | |
| 84 | return account; | |
| 85 | } | |
| 86 | ||
| 87 | // --- Audit ---------------------------------------------------------------------- | |
| 88 | ||
| 89 | /** Each kind of change, as staff read it. */ | |
| 90 | export const AUDIT_ACTIONS: Record<string, string> = { | |
| 91 | terms: "Terms changed", | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 92 | allowances: "Plan, pools and caps changed", |
| 93 | payment: "Bank transfer recorded", | |
| 94 | goodwill: "Goodwill credit", | |
| 95 | request: "Request answered", | |
| sudo: Invoices, Audit log, and follow-ups due | 96 | create: "Enterprise created", |
| 97 | attach: "Workspace added", | |
| 98 | detach: "Workspace removed", | |
| 99 | credit: "Credit issued", | |
| sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's | 100 | reset: "Billing reset (testing)", |
| sudo: Invoices, Audit log, and follow-ups due | 101 | billing_link: "Billing link made", |
| 102 | billing_email: "Invoice email set", | |
| 103 | invoice: "Invoice sent", | |
| 104 | dispute: "Payment disputed", | |
| 105 | sales: "Sales record changed", | |
| 106 | note: "Sales note added", | |
| 107 | stripe: "From Stripe", | |
| 108 | webhook: "Stripe webhook registered", | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 109 | close: "Billing closed", |
| 110 | // From identity: deleted workspaces staff restored or purged. | |
| 111 | workspace_restored: "Workspace restored", | |
| 112 | workspace_purged: "Workspace purged", | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 113 | // From the status page (apps/status), merged in by the Audit log page. |
| 114 | incident_declared: "Incident declared", | |
| 115 | incident_detected: "Incident detected", | |
| 116 | incident_update: "Incident update", | |
| 117 | incident_note: "Incident note", | |
| 118 | incident_resolved: "Incident resolved", | |
| 119 | incident_roles: "Incident roles", | |
| 120 | incident_published: "Incident published", | |
| 121 | incident_dismissed: "Draft dismissed", | |
| 122 | incident_followup: "Incident follow-up", | |
| 123 | postmortem_saved: "Postmortem saved", | |
| 124 | postmortem_published: "Postmortem published", | |
| 125 | postmortem_unpublished: "Postmortem taken down", | |
| 126 | maintenance_scheduled: "Maintenance scheduled", | |
| 127 | maintenance_update: "Maintenance update", | |
| 128 | maintenance_started: "Maintenance started", | |
| 129 | maintenance_completed: "Maintenance completed", | |
| 130 | maintenance_cancelled: "Maintenance cancelled", | |
| sudo: Invoices, Audit log, and follow-ups due | 131 | }; |
| 132 | ||
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 133 | /** |
| 134 | * The status page's audit lines as the Audit log lists billing's: the | |
| 135 | * account is `incident:<id>` or `maintenance:<id>`, which link to their | |
| 136 | * pages in sudo. | |
| 137 | */ | |
| 138 | export function statusAuditActions(entries: { id: string; at: string; by: string; action: string; target: string; detail: string }[]): AdminAction[] { | |
| 139 | return entries.map((e) => ({ | |
| 140 | id: `status-${e.id}`, | |
| 141 | account: `${e.action.startsWith("maintenance_") ? "maintenance" : "incident"}:${e.target}`, | |
| 142 | action: e.action, | |
| 143 | detail: e.detail, | |
| 144 | by: e.by, | |
| 145 | createdAt: e.at, | |
| 146 | })); | |
| 147 | } | |
| 148 | ||
| 149 | /** Billing's lines and the status page's, newest first, one page's worth. */ | |
| 150 | export function mergeAudit(a: AdminAction[], b: AdminAction[], page = AUDIT_PAGE): AdminAction[] { | |
| 151 | return [...a, ...b].sort((x, y) => y.createdAt.localeCompare(x.createdAt)).slice(0, page); | |
| 152 | } | |
| 153 | ||
| sudo: Invoices, Audit log, and follow-ups due | 154 | export function actionLabel(action: string): string { |
| 155 | return AUDIT_ACTIONS[action] ?? action.replace(/_/g, " ").replace(/^./, (char) => char.toUpperCase()); | |
| 156 | } | |
| 157 | ||
| 158 | export const AUDIT_PAGE = 100; | |
| 159 | ||
| 160 | /** Where the next, older page starts: after the last line, if this page was full. */ | |
| 161 | export function olderBefore(actions: Pick<AdminAction, "createdAt">[], page = AUDIT_PAGE): string | null { | |
| 162 | return actions.length >= page ? (actions.at(-1)?.createdAt ?? null) : null; | |
| 163 | } | |
| 164 | ||
| 165 | /** A staff email filter: lowercased, at most one address's length. */ | |
| 166 | export function parseBy(value: string | null): string | null { | |
| 167 | const by = (value ?? "").trim().toLowerCase(); | |
| 168 | return by && by.length <= 254 && !/\s/.test(by) ? by : null; | |
| 169 | } | |
| 170 | ||
| 171 | export function parseAction(value: string | null): string | null { | |
| 172 | const action = (value ?? "").trim(); | |
| 173 | return /^[a-z_]{1,40}$/.test(action) ? action : null; | |
| 174 | } | |
| 175 | ||
| 176 | /** An RFC 3339 time to page from. */ | |
| 177 | export function parseBefore(value: string | null): string | null { | |
| 178 | if (!value || value.length > 40 || Number.isNaN(new Date(value).getTime())) return null; | |
| 179 | return value; | |
| 180 | } | |
| 181 | ||
| 182 | export function auditHref({ by, action, before }: { by?: string | null; action?: string | null; before?: string | null }): string { | |
| 183 | const params = new URLSearchParams(); | |
| 184 | if (by) params.set("by", by); | |
| 185 | if (action) params.set("action", action); | |
| 186 | if (before) params.set("before", before); | |
| 187 | const query = params.toString(); | |
| 188 | return query ? `/audit?${query}` : "/audit"; | |
| 189 | } |