Skip to content

g1t/apps/sudo/app/lib/nav.ts

367 lines14,771 bytesCodeBlame

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.

A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1/**
2 * sudo's navigation, and the roadmap for what g1t is building in house:
3 * its own CRM and back office, in place of bought ones. Each item is a
4 * real page. One marked `soon` renders a placeholder that says what it
5 * will do and why, so staff can see where sudo is going (routes.ts makes
6 * a route for each). No Workers or React imports, so it can be tested
7 * under Node.
8 */
9
10/** The icon a sidebar item shows; components/shell.tsx draws each one. */
11export type NavIcon =
12 | "overview"
13 | "workspaces"
14 | "enterprises"
15 | "reach-out"
16 | "people"
17 | "invoices"
18 | "prices"
19 | "credits"
20 | "usage"
21 | "stripe"
22 | "costs"
23 | "agents"
24 | "abuse"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas25 | "incidents"
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales26 | "inbox"
27 | "view-as"
28 | "announcements"
29 | "staff"
30 | "audit"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31 | "requests"
32 | "overages"
33 | "velocity"
34 | "invites"
Merge branch 'worktree-agent-a8385d293d42c913a'35 | "aliases"
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales36 // Sections
37 | "customers"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look38 | "spend"
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales39 | "revenue"
40 | "platform"
41 | "support"
42 | "team";
43
44/** What a page that is not built yet says about itself. */
45export type Roadmap = {
46 /** What it will do and why, two to four sentences. */
47 summary: string[];
48 /** What it will have, one line each. */
49 plans: string[];
50 /** Something staff should know now, such as where to go meanwhile. */
51 meanwhile?: { text: string; to?: string; link?: string };
52};
53
54export type NavItem = {
55 label: string;
56 /** Where it lives. A `soon` item's page is the placeholder. */
57 to: string;
58 icon: NavIcon;
59 /** One line, shown on hover. */
60 about: string;
61 /** Other paths under which this item is the current one. */
62 also?: string[];
63 /** Not built yet: the page says what it will be. */
64 soon?: Roadmap;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas65 /** A number the sidebar shows beside it while it is above zero, such as requests waiting. */
66 count?: NavCount;
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales67};
68
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas69/** The numbers the sidebar can show, read by root.tsx on every page. */
70export type NavCount = "waitlist" | "incidents";
71
72export type NavCounts = Partial<Record<NavCount, number>>;
73
74/** What a page or a folded section shows beside it: its items' counts, added up. */
75export function countFor(items: Pick<NavItem, "count">[], counts: NavCounts): number {
76 return items.reduce((sum, item) => sum + (item.count ? (counts[item.count] ?? 0) : 0), 0);
77}
78
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales79/**
80 * A section of the sidebar: a row with an icon that opens to its pages,
81 * as GitLab and Vercel fold theirs. One with no title is top-level links.
82 */
83export type NavGroup = { title: string | null; icon?: NavIcon; items: NavItem[] };
84
85export const NAV: NavGroup[] = [
86 {
87 title: null,
88 items: [
89 { label: "Overview", to: "/", icon: "overview", about: "The business at a glance: this month, the last six, and who needs a word." },
90 { label: "Reach out", to: "/reach-out", icon: "reach-out", about: "Who is worth a word now, and who on the team has it." },
91 ],
92 },
93 {
94 title: "Customers",
95 icon: "customers",
96 items: [
97 { label: "Workspaces", to: "/workspaces", icon: "workspaces", about: "Every workspace, who owns it, and how it pays." },
98 { label: "Enterprises", to: "/enterprises", icon: "enterprises", about: "Customers paying for several workspaces with one bill." },
99 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look100 label: "Invites",
101 to: "/invites",
102 icon: "invites",
103 about: "The waitlist, every invite, more invites for a person or workspace, and who invited whom.",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas104 count: "waitlist",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look105 },
106 {
Merge branch 'worktree-agent-a8385d293d42c913a'107 label: "Aliases",
108 to: "/aliases",
109 icon: "aliases",
110 about: "Names that lead to a workspace, such as g1t to flagon-io. Staff set them; customers cannot.",
111 },
112 {
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales113 label: "People",
114 to: "/people",
115 icon: "people",
116 about: "Everyone with a g1t account, across the workspaces they belong to.",
117 soon: {
118 summary: [
119 "Every person with a g1t account, not just the workspaces they belong to. Support starts from a person far more often than from a workspace: someone writes in from an email address, and staff need to find them, see which workspaces they own or belong to, and what they did lately.",
120 "It gives sales a view of contacts as well as accounts: the owner who signs, the engineer who brought g1t in, and who to ask when a workspace goes quiet.",
121 ],
122 plans: [
123 "Search by username, email or name, across every account",
124 "A person's page: their workspaces and role in each, sign-ins, verified email, two-factor status",
125 "Contacts on a workspace: who is the buyer, who is technical, who to bill",
126 "Merge duplicate accounts, and resend or change a verification email, each recorded",
127 ],
128 meanwhile: { text: "Search a workspace by an owner's username or email on Workspaces.", to: "/workspaces", link: "Workspaces" },
129 },
130 },
131 ],
132 },
133 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look134 title: "Spend",
135 icon: "spend",
136 items: [
137 {
138 label: "Requests",
139 to: "/requests",
140 icon: "requests",
141 about: "Owners asking for a higher limit or help with a month: answered within one business day.",
142 },
143 {
144 label: "Overages",
145 to: "/overages",
146 icon: "overages",
147 about: "Workspaces well past their typical month, what caused it, and goodwill credits.",
148 },
149 {
150 label: "Velocity",
151 to: "/velocity",
152 icon: "velocity",
153 about: "Who is spending fastest right now, against their usual hour, and any spikes.",
154 },
155 ],
156 },
157 {
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales158 title: "Revenue",
159 icon: "revenue",
160 items: [
161 {
162 label: "Invoices",
163 to: "/invoices",
164 icon: "invoices",
165 about: "Every invoice g1t has sent, to workspaces and enterprises.",
166 },
167 {
168 label: "Plans & prices",
169 to: "/prices",
170 icon: "prices",
171 about: "The price book: what each meter costs g1t and what it sells for.",
172 soon: {
173 summary: [
174 "The price book, edited in sudo instead of in code. Every metered unit (sandbox seconds, builds, app requests and CPU, app-months) has what it costs g1t and what g1t sells it for, and the price follows the cost with a markup.",
175 "Changing a markup or a plan's price is a decision finance makes and should be able to make safely: previewed against last month's usage, with a date it takes effect, and recorded with who made it and why.",
176 ],
177 plans: [
178 "Each meter's cost, markup and price, and where the cost came from (Cloudflare's list price or its bill)",
179 "Edit a markup or a plan's monthly price, with a preview of what last month would have charged",
180 "Schedule a change for the start of a month, so no one is charged mid-month on a new price",
181 "Every change in the history the public pricing page already shows",
182 ],
183 },
184 },
185 {
186 label: "Credits & refunds",
187 to: "/credits",
188 icon: "credits",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging189 about: "Every credit staff have given (promotional, goodwill, refunds), what was used, and each month by kind.",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales190 },
191 {
192 label: "Usage explorer",
193 to: "/usage",
194 icon: "usage",
195 about: "What customers used, sliced by meter, model, repository and day.",
196 soon: {
197 summary: [
198 "What customers used, across every workspace, sliced any way: by meter, model, repository, task and day. It answers the questions that come up in a sales call or a cost review: who uses the most sandbox time, which model costs most, what a customer's agents spend per pull request.",
199 "Billing records every run and sandbox second with its cost and charge already. This puts it in front of the people who need it, without a query.",
200 ],
201 plans: [
202 "Charged, cost and margin over any range, for everyone or one customer",
203 "Group by meter, model, task, repository or workspace; compare two periods",
204 "The heaviest workspaces this month, and how fast each is growing",
205 "Export to CSV",
206 ],
207 },
208 },
209 ],
210 },
211 {
212 title: "Platform",
213 icon: "platform",
214 items: [
215 { label: "Stripe", to: "/stripe", icon: "stripe", about: "Billing's Stripe keys, webhook, and the events Stripe sent." },
216 {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily217 label: "Costs & margin",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales218 to: "/costs",
219 icon: "costs",
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing220 about: "Where g1t's money goes, with what it gave away kept apart; on Bill & pricing, Cloudflare's bill, drift and the price book.",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales221 },
222 {
223 label: "Agents & models",
224 to: "/agents",
225 icon: "agents",
Models: g1t keeps up with new models, and staff choose each default in sudo226 about: "Every model g1t can use, new ones to approve, and which model each tier and job uses by default.",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales227 },
228 {
229 label: "Abuse & fraud",
230 to: "/abuse",
231 icon: "abuse",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look232 about: "Sandboxes stopped for looking like mining, and disputes and declines.",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales233 },
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas234 {
235 label: "Incidents",
236 to: "/incidents",
237 icon: "incidents",
238 about: "Declare and run incidents, schedule maintenance, and publish postmortems on status.g1t.sh.",
239 count: "incidents",
240 },
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales241 ],
242 },
243 {
244 title: "Support",
245 icon: "support",
246 items: [
247 {
248 label: "Inbox",
249 to: "/inbox",
250 icon: "inbox",
251 about: "Customers' messages, beside everything about who sent them.",
252 soon: {
253 summary: [
254 "Customers' email and in-app messages in one queue, each beside everything sudo knows about who sent it: their workspaces, what they pay, their limit, their last invoice, and the notes sales kept. Support answers faster when nothing needs looking up.",
255 "Built in house so a conversation can become an action without leaving it: a credit, a billing link, a note on the workspace.",
256 ],
257 plans: [
258 "Assign, snooze and close conversations; see who is answering what",
259 "The sender's workspaces, plan, limit and recent invoices alongside",
260 "Saved replies, and links that open the right page in g1t",
261 "Response times by person and by week",
262 ],
263 },
264 },
265 {
266 label: "View as customer",
267 to: "/view-as",
268 icon: "view-as",
269 about: "See g1t as a customer sees it, audited and time-limited.",
270 soon: {
271 summary: [
272 "See g1t exactly as a customer sees it, to reproduce what they describe. Read-only by default, it is the most sensitive thing staff can do, so it is built to be safe first.",
273 "Every session will be audited and time-limited: it needs a reason, it ends on its own, the customer's own audit log says that g1t staff looked, and nothing can be changed or any secret read while viewing.",
274 ],
275 plans: [
276 "Start a session with a reason (a support conversation or a note); it ends after 30 minutes",
277 "Read-only: no pushes, no settings, no secrets, no billing changes",
278 "A banner on every page while it lasts, and a record in the customer's audit log",
279 "Only roles allowed to, and a list of every session for review",
280 ],
281 },
282 },
283 {
284 label: "Announcements",
285 to: "/announcements",
286 icon: "announcements",
287 about: "Banners and notices for customers: incidents, maintenance, changes.",
288 soon: {
289 summary: [
290 "Tell customers what they need to know, from one place: a banner for an incident or maintenance, a notice about a price change, an email to the owners of the workspaces a change affects.",
291 "Notices can go to everyone or to a slice (owners on an enterprise, workspaces near their limit), with a start and an end, and a record of who sent what.",
292 ],
293 plans: [
294 "Banners in g1t with a start, an end, and a severity",
295 "Email to owners of chosen workspaces, previewed before it goes",
296 "Price-change notices with the notice period the terms require",
297 "A history of everything announced",
298 ],
299 },
300 },
301 ],
302 },
303 {
304 title: "Team",
305 icon: "team",
306 items: [
307 {
308 label: "Staff & roles",
309 to: "/staff",
310 icon: "staff",
311 about: "Who is staff, and what each role may do.",
312 soon: {
313 summary: [
314 "Who is g1t staff and what each of them may do. Today everyone in STAFF_EMAILS may do everything; as the team grows, roles should match the work: sales can see accounts and keep notes but cannot credit, support can make billing links and small credits, finance can change terms and issue refunds.",
315 "Access still decides who gets in. Roles decide what they can do once there, and every refusal is recorded like every change.",
316 ],
317 plans: [
318 "Roles (sales, support, finance, admin) with the permissions each grants",
319 "Per-role limits on money: the most a credit can be without a second person",
320 "Assign roles by email, recorded, with the date each was given",
321 "A second person's approval for comped terms and large credits",
322 ],
323 },
324 },
325 {
326 label: "Audit log",
327 to: "/audit",
328 icon: "audit",
329 about: "Every change made in sudo, by whom, across every customer.",
330 },
331 ],
332 },
333];
334
335/** Whether a section holds the page at `pathname`, so it is drawn open. */
336export function holdsCurrent(group: NavGroup, pathname: string): boolean {
337 return group.items.some((item) => isCurrent(item, pathname));
338}
339
340/** Every item, in sidebar order. */
341export function navItems(nav: NavGroup[] = NAV): NavItem[] {
342 return nav.flatMap((group) => group.items);
343}
344
345/** The items not built yet, each with the placeholder page routes.ts makes. */
346export function soonItems(nav: NavGroup[] = NAV): (NavItem & { soon: Roadmap })[] {
347 return navItems(nav).filter((item): item is NavItem & { soon: Roadmap } => item.soon != null);
348}
349
350/** The item for a path, if one is its page. */
351export function soonFor(pathname: string, nav: NavGroup[] = NAV): (NavItem & { soon: Roadmap }) | null {
352 const path = pathname.replace(/\/+$/, "") || "/";
353 return soonItems(nav).find((item) => item.to === path) ?? null;
354}
355
356/**
357 * Whether `item` is the current page at `pathname`: its own path or one
358 * beneath it. `/` is current only on itself.
359 */
360export function isCurrent(item: Pick<NavItem, "to" | "also">, pathname: string): boolean {
361 const path = pathname.replace(/\/+$/, "") || "/";
362 return [item.to, ...(item.also ?? [])].some((prefix) => {
363 const base = prefix.split("?")[0];
364 if (base === "/") return path === "/";
365 return path === base || path.startsWith(`${base}/`);
366 });
367}

This file's history is long; its oldest lines are credited to the oldest commit read.