Repository navigation
Show Free plan usage in zro status #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -741,6 +741,7 @@ type AccountStatus = { | |
| } | null; | ||
| usagePacks: { total: number; used: number; remaining: number }; | ||
| totalRemaining: number; | ||
| free?: { remainingPercent: number | null; resetsAt: string }; | ||
| }; | ||
| activity30d: { | ||
| requests: number; | ||
|
|
@@ -806,6 +807,7 @@ function parseAccountStatus(value: unknown): AccountStatus | null { | |
| const activity = asRecord(root.activity30d); | ||
| const planValue = billing.plan; | ||
| const plan = planValue === null ? null : asRecord(planValue); | ||
| const free = billing.free === undefined ? null : asRecord(billing.free); | ||
| if ( | ||
| typeof key.id !== "string" || typeof key.alias !== "string" || | ||
| typeof billing.status !== "string" || typeof billing.currency !== "string" || | ||
|
|
@@ -818,6 +820,10 @@ function parseAccountStatus(value: unknown): AccountStatus | null { | |
| (plan !== null && ( | ||
| typeof plan.id !== "string" || typeof plan.name !== "string" || | ||
| !hasNumbers(plan, ["allowance", "used", "remaining"]) | ||
| )) || | ||
| (free !== null && ( | ||
| (free.remainingPercent !== null && !hasNumbers(free, ["remainingPercent"])) || | ||
| typeof free.resetsAt !== "string" || Number.isNaN(Date.parse(free.resetsAt)) | ||
| )) | ||
| ) return null; | ||
|
|
||
|
|
@@ -838,19 +844,34 @@ function printAccountStatus( | |
| const packs = account.billing.usagePacks; | ||
| const activity = account.activity30d; | ||
|
|
||
| const free = account.billing.free; | ||
| const bar = (ratio: number) => { | ||
| const width = 24; | ||
| const filled = Math.round(ratio * width); | ||
| return colors.good("█".repeat(filled)) + colors.muted("░".repeat(width - filled)); | ||
| }; | ||
|
|
||
| io.stdout.write(`\n${colors.muted("ACCOUNT")}\n`); | ||
| io.stdout.write( | ||
| ` ${plan ? colors.good("◆") : colors.muted("◇")} Plan ` + | ||
| `${plan ? colors.strong(plan.name) : colors.muted("none")} ` + | ||
| `${colors.muted(account.billing.status)}\n`, | ||
| ); | ||
| if (free) { | ||
| io.stdout.write(` ${colors.good("◆")} Plan ${colors.strong("Free")}\n`); | ||
| const resets = colors.muted(`resets in ${formatTimeUntil(free.resetsAt)}`); | ||
| io.stdout.write( | ||
| free.remainingPercent === null | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] The The server sets Suggested fix: add a second status test with |
||
| ? ` ${resets}\n` | ||
| : ` ${bar(free.remainingPercent / 100)} ${colors.strong(`${free.remainingPercent}%`)} ` + | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Unclamped
Suggested fix: clamp inside |
||
| `${colors.muted("left today ·")} ${resets}\n`, | ||
| ); | ||
| } else { | ||
| io.stdout.write( | ||
| ` ${plan ? colors.good("◆") : colors.muted("◇")} Plan ` + | ||
| `${plan ? colors.strong(plan.name) : colors.muted("none")} ` + | ||
| `${colors.muted(account.billing.status)}\n`, | ||
| ); | ||
| } | ||
| if (plan) { | ||
| const ratio = plan.allowance > 0 ? Math.min(Math.max(plan.used / plan.allowance, 0), 1) : 0; | ||
| const width = 24; | ||
| const filled = Math.round(ratio * width); | ||
| const bar = colors.good("█".repeat(filled)) + colors.muted("░".repeat(width - filled)); | ||
| io.stdout.write( | ||
| ` ${bar} ${colors.strong(`${Math.round(ratio * 100)}%`)} ${colors.muted("used")}\n`, | ||
| ` ${bar(ratio)} ${colors.strong(`${Math.round(ratio * 100)}%`)} ${colors.muted("used")}\n`, | ||
| ); | ||
| } | ||
| io.stdout.write( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Free accounts still get a dollar top-up line The PR states "No dollar amounts are shown for Free", but the Top-up credits line below is unconditional, so the new test's own Free fixture renders Suggested fix: skip the Top-up line (or its money values) when |
||
|
|
@@ -863,6 +884,12 @@ function printAccountStatus( | |
| ); | ||
| } | ||
|
|
||
| function formatTimeUntil(iso: string): string { | ||
| const minutes = Math.max(0, Math.ceil((Date.parse(iso) - Date.now()) / 60_000)); | ||
| const hours = Math.floor(minutes / 60); | ||
| return hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`; | ||
| } | ||
|
|
||
| function money(value: number, currency: string): string { | ||
| return new Intl.NumberFormat("en-US", { | ||
| style: "currency", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Explicit
free: nulldegrades the entire ACCOUNT blockThe branch mirrors the
planfield one line above but drops the null case:billing.free === undefined ? null : asRecord(billing.free). SinceasRecord(null)returns{}, a server that serializes the absent optional plan as JSONnullproduces a non-nullfreethat then fails the guard at 824-827 (typeof free.resetsAt !== "string"),parseAccountStatusreturnsnull, andzro statusprints◇ Details unavailable— losing plan, top-up credits and 30-day activity, with--jsonreportingaccountStatus: "unavailable". Afreeobject that omitsremainingPercentfails identically, becauseundefined !== null. This is exactly the trust-boundary case the PR claims is safe (omission works, explicit null does not).Suggested fix:
const freeRaw = billing.free; const free = freeRaw === null || freeRaw === undefined ? null : asRecord(freeRaw);, treatremainingPercent === undefinedlikenullin the guard, and add tests forfree: null, a missingremainingPercent, and an unparseableresetsAtasserting the account still renders.