@hanzo/build 0.2.20 → 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api/billing.d.ts +27 -0
- package/lib/api/billing.js +16 -0
- package/lib/settings/usage.js +17 -2
- package/package.json +1 -1
package/lib/api/billing.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* POST /v1/billing/topup charge a saved card into the balance
|
|
19
19
|
* GET /v1/billing/usage/rollup the plan's month: included spend and request windows
|
|
20
20
|
* GET /v1/usage/summary?range=month this month's spend, by category
|
|
21
|
+
* GET /v1/allowance the Free plan's allowance, and the pool it is served from
|
|
21
22
|
* GET /v1/billing/alerts spend caps (bare array)
|
|
22
23
|
* POST /v1/billing/alerts open a cap (org admin)
|
|
23
24
|
* PATCH /v1/billing/alerts/{id} change one (org admin)
|
|
@@ -183,6 +184,32 @@ export interface Spend {
|
|
|
183
184
|
}[];
|
|
184
185
|
}
|
|
185
186
|
export declare function spend(t: Target): Promise<Spend>;
|
|
187
|
+
/** The pool every free user shares, as its vendor accounts last said. */
|
|
188
|
+
export interface Pool {
|
|
189
|
+
/** available, busy or exhausted. */
|
|
190
|
+
state: string;
|
|
191
|
+
/** RFC 3339: when the pool refills, where the platform says; ''. */
|
|
192
|
+
resets: string;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* What the caller has left of the Free plan: limited usage, from a pool shared
|
|
196
|
+
* by all free users. `pooled` is false for a paid plan, whose usage is its spend.
|
|
197
|
+
*/
|
|
198
|
+
export interface Allowance {
|
|
199
|
+
plan: string;
|
|
200
|
+
/** Requests allowed in `window`; 0 is no bound. */
|
|
201
|
+
limit: number;
|
|
202
|
+
used: number;
|
|
203
|
+
spent: boolean;
|
|
204
|
+
/** hour or day. */
|
|
205
|
+
window: string;
|
|
206
|
+
/** RFC 3339. */
|
|
207
|
+
resets: string;
|
|
208
|
+
pooled: boolean;
|
|
209
|
+
/** Absent when the platform could not read it. */
|
|
210
|
+
pool: Pool | null;
|
|
211
|
+
}
|
|
212
|
+
export declare function allowance(t: Target): Promise<Allowance>;
|
|
186
213
|
export interface Cap {
|
|
187
214
|
id: string;
|
|
188
215
|
title: string;
|
package/lib/api/billing.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* POST /v1/billing/topup charge a saved card into the balance
|
|
19
19
|
* GET /v1/billing/usage/rollup the plan's month: included spend and request windows
|
|
20
20
|
* GET /v1/usage/summary?range=month this month's spend, by category
|
|
21
|
+
* GET /v1/allowance the Free plan's allowance, and the pool it is served from
|
|
21
22
|
* GET /v1/billing/alerts spend caps (bare array)
|
|
22
23
|
* POST /v1/billing/alerts open a cap (org admin)
|
|
23
24
|
* PATCH /v1/billing/alerts/{id} change one (org admin)
|
|
@@ -256,6 +257,21 @@ export async function spend(t) {
|
|
|
256
257
|
.filter((c) => c.cents > 0),
|
|
257
258
|
};
|
|
258
259
|
}
|
|
260
|
+
const stamp = (unix) => (num(unix) > 0 ? new Date(num(unix) * 1000).toISOString() : '');
|
|
261
|
+
export async function allowance(t) {
|
|
262
|
+
const r = obj(await call(t, 'GET', '/v1/allowance'));
|
|
263
|
+
const p = r.pool && typeof r.pool === 'object' ? obj(r.pool) : null;
|
|
264
|
+
return {
|
|
265
|
+
plan: str(r.plan),
|
|
266
|
+
limit: num(r.limit),
|
|
267
|
+
used: num(r.used),
|
|
268
|
+
spent: r.spent === true,
|
|
269
|
+
window: str(r.window),
|
|
270
|
+
resets: stamp(r.resets),
|
|
271
|
+
pooled: r.pooled === true,
|
|
272
|
+
pool: p && str(p.state) ? { state: str(p.state), resets: stamp(p.resets) } : null,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
259
275
|
export function cap(raw) {
|
|
260
276
|
const a = obj(raw);
|
|
261
277
|
return {
|
package/lib/settings/usage.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
9
|
import { Button, Input } from '@hanzo/ui';
|
|
10
10
|
import { useState } from 'react';
|
|
11
|
-
import { balance, caps, cents, chosen, credit, label, lift, limit, methods, money, month, monthly, spend, topup, } from '../api/billing.js';
|
|
11
|
+
import { allowance, balance, caps, cents, chosen, credit, label, lift, limit, methods, money, month, monthly, spend, topup, } from '../api/billing.js';
|
|
12
12
|
import { useRead } from '../data.js';
|
|
13
13
|
import { useHost, useTarget } from '../host.js';
|
|
14
14
|
import { path } from '../route.js';
|
|
@@ -40,11 +40,26 @@ function resets(iso) {
|
|
|
40
40
|
return `Resets in ${Math.floor(mins / 60)} hr ${mins % 60} min`;
|
|
41
41
|
return `Resets ${day(iso)}`;
|
|
42
42
|
}
|
|
43
|
+
const POOL = { available: 'Available', busy: 'Busy', exhausted: 'Used up' };
|
|
44
|
+
const POOL_SAYS = {
|
|
45
|
+
available: () => 'Every free request is being served',
|
|
46
|
+
busy: () => 'Try again in a minute',
|
|
47
|
+
exhausted: (refill) => refill || 'Refills when the accounts reset',
|
|
48
|
+
};
|
|
49
|
+
/** `Refills at 16:00 UTC`, for a window or the pool: the UTC time it opens again, with the date when not today. */
|
|
50
|
+
function refills(iso) {
|
|
51
|
+
const at = iso ? new Date(iso) : null;
|
|
52
|
+
if (!at || Number.isNaN(at.getTime()))
|
|
53
|
+
return '';
|
|
54
|
+
const hhmm = `${String(at.getUTCHours()).padStart(2, '0')}:${String(at.getUTCMinutes()).padStart(2, '0')} UTC`;
|
|
55
|
+
return at.toISOString().slice(0, 10) === new Date().toISOString().slice(0, 10) ? `Refills at ${hhmm}` : `Refills ${day(iso)}, ${hhmm}`;
|
|
56
|
+
}
|
|
43
57
|
export function Usage() {
|
|
44
58
|
const host = useHost();
|
|
45
59
|
const t = useTarget();
|
|
46
60
|
const signed = Boolean(host.person);
|
|
47
61
|
const plan = useRead(signed ? () => month(t) : null, null, [t, signed]);
|
|
62
|
+
const free = useRead(signed ? () => allowance(t) : null, null, [t, signed]);
|
|
48
63
|
const left = useRead(signed ? () => balance(t) : null, 0, [t, signed]);
|
|
49
64
|
const granted = useRead(signed ? () => credit(t) : null, 0, [t, signed]);
|
|
50
65
|
const spent = useRead(signed ? () => spend(t) : null, null, [t, signed]);
|
|
@@ -103,7 +118,7 @@ export function Usage() {
|
|
|
103
118
|
ceilings.reload();
|
|
104
119
|
});
|
|
105
120
|
};
|
|
106
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
|
|
121
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), free.value?.pooled && free.value.limit > 0 ? (_jsx(Group, { title: "Free plan", detail: "Free \u2014 limited usage, from a pool shared by all free users.", action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Upgrade" }), children: _jsxs(Card, { children: [_jsx(Meter, { first: true, title: `${Math.max(0, free.value.limit - free.value.used).toLocaleString()} of ${free.value.limit.toLocaleString()} left ${free.value.window === 'hour' ? 'this hour' : free.value.window === 'day' ? 'today' : 'this period'}`, used: free.value.used, of: free.value.limit, says: [`${free.value.used.toLocaleString()} used`, refills(free.value.resets)].filter(Boolean).join(' · ') }), free.value.pool ? (_jsx(Row, { title: "Shared pool", detail: POOL_SAYS[free.value.pool.state]?.(refills(free.value.pool.resets)) ?? '', trailing: _jsx(SizableText, { size: "$2", color: free.value.pool.state === 'exhausted' ? '$red10' : '$ink', children: POOL[free.value.pool.state] ?? free.value.pool.state }) })) : null] }) })) : null, _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
|
|
107
122
|
.filter(Boolean)
|
|
108
123
|
.join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": "Monthly limit in dollars", placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(t, cap.id), 'The monthly limit is lifted', ceilings.reload), children: "Remove" })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? 'Change limit' : 'Set limit' })] })] })) : null] })), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin sets the monthly limit." })) : null] }), _jsx(Note, { children: note })] }));
|
|
109
124
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
|