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.
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 1 | /** |
| 2 | * Home's reads: each section's data from its own service, every call on | |
| 3 | * its own so one that fails leaves only its section saying so. The shaping | |
| 4 | * is in ./home.ts. | |
| 5 | */ | |
| 6 | import { env } from "cloudflare:workers"; | |
| 7 | ||
| 8 | import { type AgentSession, type AgentsOverview, type Deployment, type InstallRequests, type Memory, type ProjectDeploys, type Repo, type User, notifyClient } from "@g1t/contracts"; | |
| 9 | ||
| 10 | import { type CodePull, type CodeWork, type SessionWork, type Span, type Spend, pullKey, spanMonths, spendIn } from "./home"; | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 11 | import { monthSpan, spentMicros } from "./spend"; |
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 12 | import { agents, billing, deployments, repos, work, workspaceAgents } from "./services.server"; |
| 13 | ||
| 14 | /** Pull requests read per project and list: `LIST_PAGE` in services/work. */ | |
| 15 | const PULL_PAGE = 100; | |
| 16 | /** The most projects whose pull requests are read. */ | |
| 17 | const MAX_PROJECTS = 50; | |
| 18 | /** The most projects whose builds are read for the span. */ | |
| 19 | const MAX_DEPLOY_PROJECTS = 20; | |
| 20 | /** Revise runs and sessions read: the services' most in one list. */ | |
| 21 | const RUN_PAGE = 200; | |
| 22 | const SESSION_PAGE = 200; | |
| 23 | const DAY = 24 * 60 * 60 * 1000; | |
| 24 | ||
| 25 | const warn = (what: string) => (error: unknown) => { | |
| 26 | console.warn(`home: ${what} failed`, error); | |
| 27 | return null; | |
| 28 | }; | |
| 29 | ||
| 30 | /** | |
| 31 | * When the viewer was last on this workspace's Home, from notify, where it | |
| 32 | * is kept with them (the same on every device). Null when there is no | |
| 33 | * visit on record; undefined when notify did not answer. | |
| 34 | */ | |
| 35 | export async function loadLastVisit(viewer: User, slug: string): Promise<number | null | undefined> { | |
| 36 | if (!env.NOTIFY) return undefined; | |
| 37 | const visit = await notifyClient(env.NOTIFY).lastVisit(viewer, slug).catch(warn("last visit")); | |
| 38 | if (!visit) return undefined; | |
| 39 | return visit.seen_at ? Date.parse(visit.seen_at) : null; | |
| 40 | } | |
| 41 | ||
| 42 | /** Marks the viewer's visit at `at`, the time the page they looked at loaded. */ | |
| 43 | export async function markVisit(viewer: User, slug: string, at: number): Promise<boolean> { | |
| 44 | if (!env.NOTIFY) return false; | |
| 45 | const marked = await notifyClient(env.NOTIFY).markVisit(viewer, slug, new Date(at).toISOString()).catch(warn("mark visit")); | |
| 46 | return marked != null; | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * The workspace's projects' pull requests, open and recently merged or | |
| 51 | * closed, any author's, with how many times each agent's was sent back to | |
| 52 | * revise. `complete` when every list reaches back to `reach`. Null when | |
| 53 | * Code did not answer. | |
| 54 | */ | |
| 55 | export async function loadCodeWork(viewer: User, slug: string, reach: number): Promise<CodeWork | null> { | |
| 56 | const [list, runs] = await Promise.all([ | |
| 57 | repos.list(viewer, { namespace: slug }).catch(warn("repos")), | |
| 58 | agents.listRuns(viewer, { workspace: slug, kind: "revise", limit: RUN_PAGE }).catch(warn("revise runs")), | |
| 59 | ]); | |
| 60 | if (!list || !runs?.ok) return null; | |
| 61 | const owned = list.filter((repo) => !repo.forkOf && repo.namespace.toLowerCase() === slug); | |
| 62 | const chosen = owned.slice(0, MAX_PROJECTS); | |
| 63 | const byId = new Map<string, Repo>(chosen.map((repo) => [repo.id, repo])); | |
| 64 | const batch = chosen.length > 0 ? await work.pullsForRepos([...byId.keys()], viewer, PULL_PAGE).catch(warn("pull requests")) : []; | |
| 65 | if (!batch) return null; | |
| 66 | const pulls: CodePull[] = []; | |
| 67 | let complete = owned.length <= MAX_PROJECTS; | |
| 68 | for (const entry of batch) { | |
| 69 | const repo = byId.get(entry.repoId); | |
| 70 | if (!repo) continue; | |
| 71 | const path = { namespace: repo.namespace, name: repo.name }; | |
| 72 | for (const pull of [...entry.open, ...entry.closed]) { | |
| 73 | pulls.push({ | |
| 74 | repo: path, | |
| 75 | number: pull.number, | |
| 76 | title: pull.title, | |
| 77 | status: pull.status, | |
| 78 | mergedAt: pull.mergedAt, | |
| 79 | mergedBy: pull.mergedBy, | |
| 80 | createdAt: pull.createdAt, | |
| 81 | updatedAt: pull.updatedAt, | |
| 82 | author: { username: pull.author.username, kind: pull.author.kind }, | |
| 83 | }); | |
| 84 | } | |
| 85 | // A full page that does not reach back far enough may leave some out. | |
| 86 | for (const list of [entry.open, entry.closed]) { | |
| 87 | if (list.length >= PULL_PAGE) { | |
| 88 | const oldest = Math.min(...list.map((pull) => Date.parse(pull.mergedAt ?? pull.updatedAt))); | |
| 89 | if (oldest > reach) complete = false; | |
| 90 | } | |
| 91 | } | |
| 92 | } | |
| 93 | const revisions: Record<string, number> = {}; | |
| 94 | for (const run of runs.value) { | |
| 95 | if (run.number == null) continue; | |
| 96 | const key = pullKey(run.repo, run.number); | |
| 97 | revisions[key] = (revisions[key] ?? 0) + 1; | |
| 98 | } | |
| 99 | if (runs.value.length >= RUN_PAGE && Date.parse(runs.value[runs.value.length - 1].createdAt) > reach) complete = false; | |
| 100 | return { pulls, revisions, complete }; | |
| 101 | } | |
| 102 | ||
| 103 | /** The workspace's newest sessions; `complete` when the list reaches back before `from`. */ | |
| 104 | export async function loadSessions(viewer: User, slug: string, from: number): Promise<SessionWork | null> { | |
| 105 | const listed = await workspaceAgents.sessions(slug, viewer, { limit: SESSION_PAGE }).catch(warn("sessions")); | |
| 106 | if (!listed?.ok) return null; | |
| 107 | const sessions: AgentSession[] = listed.value; | |
| 108 | const oldest = sessions.at(-1); | |
| 109 | const complete = sessions.length < SESSION_PAGE || (oldest != null && Date.parse(oldest.created_at) < from); | |
| 110 | return { sessions, complete }; | |
| 111 | } | |
| 112 | ||
| 113 | /** The agents overview: sessions waiting on the viewer, live ones, and the workspace's budget alert. */ | |
| 114 | export async function loadAgentsOverview(viewer: User, slug: string): Promise<AgentsOverview | null> { | |
| 115 | const overview = await workspaceAgents.overview(slug, viewer).catch(warn("agents overview")); | |
| 116 | return overview?.ok ? overview.value : null; | |
| 117 | } | |
| 118 | ||
| 119 | /** Install requests as the viewer sees them; only an owner's (`can_resolve`) are anyone else's. */ | |
| 120 | export async function loadInstallRequests(viewer: User, slug: string): Promise<InstallRequests | null> { | |
| 121 | const requests = await workspaceAgents.installRequests(slug, viewer).catch(warn("install requests")); | |
| 122 | return requests?.ok ? requests.value : null; | |
| 123 | } | |
| 124 | ||
| 125 | /** The workspace's memory, for the decisions recorded in it. */ | |
| 126 | export async function loadMemories(viewer: User, slug: string): Promise<Memory[] | null> { | |
| 127 | const memories = await agents.listMemories(viewer, slug, null).catch(warn("memories")); | |
| 128 | return memories?.ok ? memories.value.workspace : null; | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * Every project's builds at a glance, and the builds of the projects that | |
| 133 | * deploy, for the span: the projects whose newest build is older than the | |
| 134 | * span are not read again. | |
| 135 | */ | |
| 136 | export async function loadDeploys( | |
| 137 | viewer: User, | |
| 138 | slug: string, | |
| 139 | from: number, | |
| 140 | ): Promise<{ overview: ProjectDeploys[]; projects: { slug: string; deployments: Deployment[] }[]; complete: boolean } | null> { | |
| 141 | const overview = await deployments.overview(slug, viewer).catch(warn("deploys overview")); | |
| 142 | if (!overview?.ok) return null; | |
| 143 | const recent = overview.value.filter((project) => project.latest && Date.parse(project.latest.finishedAt ?? project.latest.createdAt) >= from); | |
| 144 | const read = recent.slice(0, MAX_DEPLOY_PROJECTS); | |
| 145 | const lists = await Promise.all( | |
| 146 | read.map((project) => | |
| 147 | deployments | |
| 148 | .list({ workspace: slug, slug: project.slug }, viewer) | |
| 149 | .then((result) => (result.ok ? { slug: project.slug, deployments: result.value.deployments } : null)) | |
| 150 | .catch(warn(`deploys of ${project.slug}`)), | |
| 151 | ), | |
| 152 | ); | |
| 153 | const projects = lists.filter((entry) => entry != null); | |
| 154 | return { overview: overview.value, projects, complete: projects.length === recent.length }; | |
| 155 | } | |
| 156 | ||
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 157 | /** |
| 158 | * The span's spend from the statements of the months it touches, and the | |
| 159 | * month so far from the usage report the top bar, Spend and Usage read | |
| 160 | * (`spend.ts` `monthSpan`, `spentMicros`), so Home's figure is theirs. Null | |
| 161 | * when billing did not answer; the month alone null when only it did not. | |
| 162 | */ | |
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 163 | export async function loadSpend(viewer: User, slug: string, span: Pick<Span, "from" | "now">): Promise<Spend | null> { |
| 164 | const months = spanMonths(span); | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 165 | const [statements, month] = await Promise.all([ |
| 166 | Promise.all(months.map((month) => billing.statement(slug, viewer, month, "day").catch(warn(`statement ${month}`)))), | |
| 167 | billing | |
| 168 | .usageReport(slug, viewer, monthSpan(new Date(span.now))) | |
| 169 | .then((result) => (result.ok ? spentMicros(result.value) : null)) | |
| 170 | .catch(warn("month usage")), | |
| 171 | ]); | |
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 172 | if (statements.some((statement) => !statement?.ok)) return null; |
| 173 | return spendIn( | |
| 174 | statements.map((statement) => (statement as Extract<typeof statement, { ok: true }>).value), | |
| 175 | span, | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 176 | month, |
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 177 | ); |
| 178 | } | |
| 179 | ||
| 180 | /** How far back Code is read: 7 days before the span, for the comparison. */ | |
| 181 | export function reachFor(span: Pick<Span, "from">): number { | |
| 182 | return span.from - 7 * DAY; | |
| 183 | } |