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 sales | 1 | /** |
| 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. */ | |
| 11 | export 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" | |
| 25 | | "inbox" | |
| 26 | | "view-as" | |
| 27 | | "announcements" | |
| 28 | | "staff" | |
| 29 | | "audit" | |
| 30 | // Sections | |
| 31 | | "customers" | |
| 32 | | "revenue" | |
| 33 | | "platform" | |
| 34 | | "support" | |
| 35 | | "team"; | |
| 36 | ||
| 37 | /** What a page that is not built yet says about itself. */ | |
| 38 | export type Roadmap = { | |
| 39 | /** What it will do and why, two to four sentences. */ | |
| 40 | summary: string[]; | |
| 41 | /** What it will have, one line each. */ | |
| 42 | plans: string[]; | |
| 43 | /** Something staff should know now, such as where to go meanwhile. */ | |
| 44 | meanwhile?: { text: string; to?: string; link?: string }; | |
| 45 | }; | |
| 46 | ||
| 47 | export type NavItem = { | |
| 48 | label: string; | |
| 49 | /** Where it lives. A `soon` item's page is the placeholder. */ | |
| 50 | to: string; | |
| 51 | icon: NavIcon; | |
| 52 | /** One line, shown on hover. */ | |
| 53 | about: string; | |
| 54 | /** Other paths under which this item is the current one. */ | |
| 55 | also?: string[]; | |
| 56 | /** Not built yet: the page says what it will be. */ | |
| 57 | soon?: Roadmap; | |
| 58 | }; | |
| 59 | ||
| 60 | /** | |
| 61 | * A section of the sidebar: a row with an icon that opens to its pages, | |
| 62 | * as GitLab and Vercel fold theirs. One with no title is top-level links. | |
| 63 | */ | |
| 64 | export type NavGroup = { title: string | null; icon?: NavIcon; items: NavItem[] }; | |
| 65 | ||
| 66 | export const NAV: NavGroup[] = [ | |
| 67 | { | |
| 68 | title: null, | |
| 69 | items: [ | |
| 70 | { label: "Overview", to: "/", icon: "overview", about: "The business at a glance: this month, the last six, and who needs a word." }, | |
| 71 | { label: "Reach out", to: "/reach-out", icon: "reach-out", about: "Who is worth a word now, and who on the team has it." }, | |
| 72 | ], | |
| 73 | }, | |
| 74 | { | |
| 75 | title: "Customers", | |
| 76 | icon: "customers", | |
| 77 | items: [ | |
| 78 | { label: "Workspaces", to: "/workspaces", icon: "workspaces", about: "Every workspace, who owns it, and how it pays." }, | |
| 79 | { label: "Enterprises", to: "/enterprises", icon: "enterprises", about: "Customers paying for several workspaces with one bill." }, | |
| 80 | { | |
| 81 | label: "People", | |
| 82 | to: "/people", | |
| 83 | icon: "people", | |
| 84 | about: "Everyone with a g1t account, across the workspaces they belong to.", | |
| 85 | soon: { | |
| 86 | summary: [ | |
| 87 | "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.", | |
| 88 | "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.", | |
| 89 | ], | |
| 90 | plans: [ | |
| 91 | "Search by username, email or name, across every account", | |
| 92 | "A person's page: their workspaces and role in each, sign-ins, verified email, two-factor status", | |
| 93 | "Contacts on a workspace: who is the buyer, who is technical, who to bill", | |
| 94 | "Merge duplicate accounts, and resend or change a verification email, each recorded", | |
| 95 | ], | |
| 96 | meanwhile: { text: "Search a workspace by an owner's username or email on Workspaces.", to: "/workspaces", link: "Workspaces" }, | |
| 97 | }, | |
| 98 | }, | |
| 99 | ], | |
| 100 | }, | |
| 101 | { | |
| 102 | title: "Revenue", | |
| 103 | icon: "revenue", | |
| 104 | items: [ | |
| 105 | { | |
| 106 | label: "Invoices", | |
| 107 | to: "/invoices", | |
| 108 | icon: "invoices", | |
| 109 | about: "Every invoice g1t has sent, to workspaces and enterprises.", | |
| 110 | }, | |
| 111 | { | |
| 112 | label: "Plans & prices", | |
| 113 | to: "/prices", | |
| 114 | icon: "prices", | |
| 115 | about: "The price book: what each meter costs g1t and what it sells for.", | |
| 116 | soon: { | |
| 117 | summary: [ | |
| 118 | "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.", | |
| 119 | "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.", | |
| 120 | ], | |
| 121 | plans: [ | |
| 122 | "Each meter's cost, markup and price, and where the cost came from (Cloudflare's list price or its bill)", | |
| 123 | "Edit a markup or a plan's monthly price, with a preview of what last month would have charged", | |
| 124 | "Schedule a change for the start of a month, so no one is charged mid-month on a new price", | |
| 125 | "Every change in the history the public pricing page already shows", | |
| 126 | ], | |
| 127 | }, | |
| 128 | }, | |
| 129 | { | |
| 130 | label: "Credits & refunds", | |
| 131 | to: "/credits", | |
| 132 | icon: "credits", | |
| 133 | about: "Every credit and refund staff have issued, and why.", | |
| 134 | soon: { | |
| 135 | summary: [ | |
| 136 | "Every credit and refund staff have given, across all customers: how much, to whom, by whom and why. Goodwill is a cost, and finance needs to see what it adds up to each month.", | |
| 137 | "It is also where refunds to a card will live. Today a credit goes to a workspace's balance; giving money back to the card it came from goes through Stripe, and should be done from here, recorded, with the same typed confirmation as a credit.", | |
| 138 | ], | |
| 139 | plans: [ | |
| 140 | "Every credit, filterable by staff member, workspace and month, with totals", | |
| 141 | "Refund a payment to the card it came from, in full or in part", | |
| 142 | "Per-role limits: support can credit up to a set amount; more needs finance", | |
| 143 | "Reasons as a short list (outage, billing error, goodwill, trial) so they can be counted", | |
| 144 | ], | |
| 145 | meanwhile: { text: "Issue a credit from the workspace's page, under Billing.", to: "/workspaces", link: "Workspaces" }, | |
| 146 | }, | |
| 147 | }, | |
| 148 | { | |
| 149 | label: "Usage explorer", | |
| 150 | to: "/usage", | |
| 151 | icon: "usage", | |
| 152 | about: "What customers used, sliced by meter, model, repository and day.", | |
| 153 | soon: { | |
| 154 | summary: [ | |
| 155 | "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.", | |
| 156 | "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.", | |
| 157 | ], | |
| 158 | plans: [ | |
| 159 | "Charged, cost and margin over any range, for everyone or one customer", | |
| 160 | "Group by meter, model, task, repository or workspace; compare two periods", | |
| 161 | "The heaviest workspaces this month, and how fast each is growing", | |
| 162 | "Export to CSV", | |
| 163 | ], | |
| 164 | }, | |
| 165 | }, | |
| 166 | ], | |
| 167 | }, | |
| 168 | { | |
| 169 | title: "Platform", | |
| 170 | icon: "platform", | |
| 171 | items: [ | |
| 172 | { label: "Stripe", to: "/stripe", icon: "stripe", about: "Billing's Stripe keys, webhook, and the events Stripe sent." }, | |
| 173 | { | |
| 174 | label: "Cloudflare costs", | |
| 175 | to: "/costs", | |
| 176 | icon: "costs", | |
| 177 | about: "What running g1t costs on Cloudflare, measured, against what it charges.", | |
| 178 | soon: { | |
| 179 | summary: [ | |
| 180 | "What running g1t actually costs on Cloudflare, as the keeper measures it from Cloudflare's bill, against what g1t charges for the same usage. Prices follow costs, so this is where staff see whether they still do.", | |
| 181 | "When a cost moves, the keeper updates the price book; this page shows what it measured, when, and the margin each meter earns as a result.", | |
| 182 | ], | |
| 183 | plans: [ | |
| 184 | "Each meter's measured cost per unit, the list price, and when the keeper last checked", | |
| 185 | "Margin by meter and in total, this month and the last six", | |
| 186 | "Cloudflare's invoice against what g1t charged for the same period", | |
| 187 | "An alert when a meter's margin drops below its markup", | |
| 188 | ], | |
| 189 | }, | |
| 190 | }, | |
| 191 | { | |
| 192 | label: "Agents & models", | |
| 193 | to: "/agents", | |
| 194 | icon: "agents", | |
| 195 | about: "The models agents run on, what each costs, and how runs are going.", | |
| 196 | soon: { | |
| 197 | summary: [ | |
| 198 | "The models g1t's agents run on, and how they are doing: runs, failures, tokens and cost per model, and which workspaces bring their own provider. It is where staff decide which models to offer and see what a change in a provider's price means.", | |
| 199 | "Hosted models are open to some workspaces and not others; that list belongs here, edited and recorded, not in configuration.", | |
| 200 | ], | |
| 201 | plans: [ | |
| 202 | "Runs, failures and cost per model, per day", | |
| 203 | "Who may use g1t's hosted models, and the free allowance's pool", | |
| Prices are what g1t pays plus 20%, from the first second | 204 | "Workspaces on their own provider, and the sandbox time their runs use", |
| A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales | 205 | "Stuck or long-running agents, with a way to stop one", |
| 206 | ], | |
| 207 | }, | |
| 208 | }, | |
| 209 | { | |
| 210 | label: "Abuse & fraud", | |
| 211 | to: "/abuse", | |
| 212 | icon: "abuse", | |
| 213 | about: "Stripe Radar signals, disputes and suspicious sign-ups.", | |
| 214 | soon: { | |
| 215 | summary: [ | |
| 216 | "Where trust and safety work happens: Stripe Radar's warnings, disputed payments, and sign-ups that look like abuse (many accounts from one card, throwaway emails, sandboxes used for mining). Limits keep g1t's exposure small, but someone still needs to look.", | |
| 217 | "Each case ends in an action that is recorded: clear it, lower a workspace's limit, or suspend it, with a note for whoever looks next.", | |
| 218 | ], | |
| 219 | plans: [ | |
| 220 | "Disputes with their deadlines, and the evidence to send Stripe", | |
| 221 | "Radar's early fraud warnings and blocked payments", | |
| 222 | "Suspicious sign-ups: shared cards, disposable domains, bursts from one network", | |
| 223 | "Suspend or restore a workspace, with a confirmation and a note", | |
| 224 | ], | |
| 225 | meanwhile: { text: "Disputes and declines show up in Reach out.", to: "/reach-out?kind=declined", link: "Declined" }, | |
| 226 | }, | |
| 227 | }, | |
| 228 | ], | |
| 229 | }, | |
| 230 | { | |
| 231 | title: "Support", | |
| 232 | icon: "support", | |
| 233 | items: [ | |
| 234 | { | |
| 235 | label: "Inbox", | |
| 236 | to: "/inbox", | |
| 237 | icon: "inbox", | |
| 238 | about: "Customers' messages, beside everything about who sent them.", | |
| 239 | soon: { | |
| 240 | summary: [ | |
| 241 | "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.", | |
| 242 | "Built in house so a conversation can become an action without leaving it: a credit, a billing link, a note on the workspace.", | |
| 243 | ], | |
| 244 | plans: [ | |
| 245 | "Assign, snooze and close conversations; see who is answering what", | |
| 246 | "The sender's workspaces, plan, limit and recent invoices alongside", | |
| 247 | "Saved replies, and links that open the right page in g1t", | |
| 248 | "Response times by person and by week", | |
| 249 | ], | |
| 250 | }, | |
| 251 | }, | |
| 252 | { | |
| 253 | label: "View as customer", | |
| 254 | to: "/view-as", | |
| 255 | icon: "view-as", | |
| 256 | about: "See g1t as a customer sees it, audited and time-limited.", | |
| 257 | soon: { | |
| 258 | summary: [ | |
| 259 | "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.", | |
| 260 | "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.", | |
| 261 | ], | |
| 262 | plans: [ | |
| 263 | "Start a session with a reason (a support conversation or a note); it ends after 30 minutes", | |
| 264 | "Read-only: no pushes, no settings, no secrets, no billing changes", | |
| 265 | "A banner on every page while it lasts, and a record in the customer's audit log", | |
| 266 | "Only roles allowed to, and a list of every session for review", | |
| 267 | ], | |
| 268 | }, | |
| 269 | }, | |
| 270 | { | |
| 271 | label: "Announcements", | |
| 272 | to: "/announcements", | |
| 273 | icon: "announcements", | |
| 274 | about: "Banners and notices for customers: incidents, maintenance, changes.", | |
| 275 | soon: { | |
| 276 | summary: [ | |
| 277 | "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.", | |
| 278 | "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.", | |
| 279 | ], | |
| 280 | plans: [ | |
| 281 | "Banners in g1t with a start, an end, and a severity", | |
| 282 | "Email to owners of chosen workspaces, previewed before it goes", | |
| 283 | "Price-change notices with the notice period the terms require", | |
| 284 | "A history of everything announced", | |
| 285 | ], | |
| 286 | }, | |
| 287 | }, | |
| 288 | ], | |
| 289 | }, | |
| 290 | { | |
| 291 | title: "Team", | |
| 292 | icon: "team", | |
| 293 | items: [ | |
| 294 | { | |
| 295 | label: "Staff & roles", | |
| 296 | to: "/staff", | |
| 297 | icon: "staff", | |
| 298 | about: "Who is staff, and what each role may do.", | |
| 299 | soon: { | |
| 300 | summary: [ | |
| 301 | "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.", | |
| 302 | "Access still decides who gets in. Roles decide what they can do once there, and every refusal is recorded like every change.", | |
| 303 | ], | |
| 304 | plans: [ | |
| 305 | "Roles (sales, support, finance, admin) with the permissions each grants", | |
| 306 | "Per-role limits on money: the most a credit can be without a second person", | |
| 307 | "Assign roles by email, recorded, with the date each was given", | |
| 308 | "A second person's approval for comped terms and large credits", | |
| 309 | ], | |
| 310 | }, | |
| 311 | }, | |
| 312 | { | |
| 313 | label: "Audit log", | |
| 314 | to: "/audit", | |
| 315 | icon: "audit", | |
| 316 | about: "Every change made in sudo, by whom, across every customer.", | |
| 317 | }, | |
| 318 | ], | |
| 319 | }, | |
| 320 | ]; | |
| 321 | ||
| 322 | /** Whether a section holds the page at `pathname`, so it is drawn open. */ | |
| 323 | export function holdsCurrent(group: NavGroup, pathname: string): boolean { | |
| 324 | return group.items.some((item) => isCurrent(item, pathname)); | |
| 325 | } | |
| 326 | ||
| 327 | /** Every item, in sidebar order. */ | |
| 328 | export function navItems(nav: NavGroup[] = NAV): NavItem[] { | |
| 329 | return nav.flatMap((group) => group.items); | |
| 330 | } | |
| 331 | ||
| 332 | /** The items not built yet, each with the placeholder page routes.ts makes. */ | |
| 333 | export function soonItems(nav: NavGroup[] = NAV): (NavItem & { soon: Roadmap })[] { | |
| 334 | return navItems(nav).filter((item): item is NavItem & { soon: Roadmap } => item.soon != null); | |
| 335 | } | |
| 336 | ||
| 337 | /** The item for a path, if one is its page. */ | |
| 338 | export function soonFor(pathname: string, nav: NavGroup[] = NAV): (NavItem & { soon: Roadmap }) | null { | |
| 339 | const path = pathname.replace(/\/+$/, "") || "/"; | |
| 340 | return soonItems(nav).find((item) => item.to === path) ?? null; | |
| 341 | } | |
| 342 | ||
| 343 | /** | |
| 344 | * Whether `item` is the current page at `pathname`: its own path or one | |
| 345 | * beneath it. `/` is current only on itself. | |
| 346 | */ | |
| 347 | export function isCurrent(item: Pick<NavItem, "to" | "also">, pathname: string): boolean { | |
| 348 | const path = pathname.replace(/\/+$/, "") || "/"; | |
| 349 | return [item.to, ...(item.also ?? [])].some((prefix) => { | |
| 350 | const base = prefix.split("?")[0]; | |
| 351 | if (base === "/") return path === "/"; | |
| 352 | return path === base || path.startsWith(`${base}/`); | |
| 353 | }); | |
| 354 | } |