8000 fix(web): keep detail labels horizontal and show sub-cent costs for 0.12.8 by InfinityPacer · Pull Request #57 · InfinityPacer/meterleaf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [未发布]

## [0.12.8] - 2026-10-26

- 费用金额按大小显示有效位数:低价模型的单次费用不再都显示成 $0.00,例如 $0.0014、$0.00673,常规金额仍保留两位小数。
- 请求明细的账户筛选不再列出已归档账户。从已归档账户查看请求时,当前账户仍保留并注明「已归档」,不会只剩一个账户 ID。
- 手机上使用侧边栏布局打开请求详情时,较长的来源记录 ID 会把字段名挤成竖排;现在字段名保持横排,长 ID 在数值列内折行。
- 数据库结构未变,升级不需要先备份。

## [0.12.7] - 2026-10-25

- 本机采集器可以登记额外任务:`meterleaf-collector tasks add` 登记一条本机命令,由采集器的后台任务按间隔运行,有限时,失败只记日志,不影响同步,`status` 和 `tasks` 显示最近一次结果。用法见[本机采集器](docs/collector.md#额外任务可选)。
Expand Down
3 changes: 3 additions & 0 deletions docs/reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- 同一账号从多个来源采集时,在来源账户菜单选择「合并额度与用量」,选定目标账户后确认。全部历史及后续用量都会归入目标,原来源记录仍可在请求明细查看,不做跨来源去重;若两个来源都记录了同一次请求,会累计两次。同一额度窗口使用最新快照,百分比不会相加,未知或到期状态仍如实显示。目标账户的名称和套餐保留。
- 在目标账户的「合并额度与用量」弹窗查看已合并账户,点击「解除合并」恢复各自展示。原始记录不删除,同步不会覆盖合并关系;连续合并多个账户时,解除某一层也会带回合并到它的账户。合并和解除后报表需要重新计算,数据较多时稍等片刻。
- 归档账户不在账户区显示。有归档账户时,标题右侧出现「已归档 N」,点击后查看归档列表,可在菜单中恢复或删除,点「使用中账户」返回。
- 归档账户不再出现在请求明细的账户筛选里。从归档列表打开某个账户的请求时,筛选仍保留这个账户并注明「已归档」,切回「全部账户」即可退出。
- 删除账户只移除账户展示,不会删除历史请求、累计统计,也不影响 Sub2API 或本机采集器。
- 重命名的名称只在 Meterleaf 中显示,最长 40 个字,不会改动上游。清空名称或选择恢复原名后,重新显示上游提供的名称。
- 套餐徽标按使用倍数分三档显示,同一档外观相同。Claude Pro、Claude Team Standard 与 ChatGPT Plus 为一档,Claude Max 5x、Claude Team Premium 与 ChatGPT Pro 5x 为二档,Claude Max 20x 与 ChatGPT Pro 20x 为三档。只知道订阅类型、不知道倍数时,例如只显示 Max 或 Team,不分档。无法识别的套餐只显示名称。
Expand All @@ -63,6 +64,8 @@

手机上预估跟在已用费用之后,桌面账户列表在右侧单独显示。缺少有效比例、可计价消费或窗口失效时保留未知,不补零。快照可能包含其他入口的消费,本地请求只覆盖已采集来源,模型组合和供应商计量规则也可能变化,因此估值不是供应商承诺额度。

金额按大小显示:常规金额保留两位小数,低于一美分时多显示几位有效数字并省略末尾的零,低价模型的单次费用不会都显示成 $0.00。导出文件保留未四舍五入的金额。

Tokens 总量由普通输入、缓存读取、缓存写入和输出组成。推理 Tokens 是输出子集,不能再相加。缺失金额保留未知;可计价部分的合计不代表完整费用。两种美元口径和订阅 Credits 的区别见[计价说明](pricing.md)。

## 更新与失败提示
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "meterleaf",
"version": "0.12.7",
"version": "0.12.8",
"private": true,
"type": "module",
"license": "Apache-2.0",
Expand Down
28 changes: 14 additions & 14 deletions src/web/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import {
usePreference,
useScopedPreference,
} from "./lib/preferences";
import { accountFilterOptions } from "./lib/account-filter";
import {
isEmptyAccount,
moveAccount,
Expand Down Expand Up @@ -873,6 +874,12 @@ export function App() {
const archivedAccounts = sortedAccounts.filter((account) =>
archivedIds.has(account.id),
);
const accountFilterChoices = accountFilterOptions(
activeAccounts,
quotaSnapshot?.accounts ?? [],
filter.account,
{ archived: archivedIds, hidden: hiddenIds },
);
const usdBasis = usdBasisOverride ?? snapshot?.usdBasis ?? "subscription";
const showArchivedAccounts = (show: boolean) => {
setEditingAccountOrder(false);
Expand Down Expand Up @@ -1390,9 +1397,11 @@ export function App() {
? [rangeTitle, usdNote]
: [
filter.model !== "all" ? modelLabel(filter.model) : null,
quotaSnapshot?.accounts.find(
(account) => account.id === filter.account,
)?.name ?? "全部账户",
filter.account === "all"
? "全部账户"
: (accountFilterChoices.find(
(option) => option.value === filter.account,
)?.label ?? filter.account),
]
)
.filter(Boolean)
Expand Down Expand Up @@ -1429,10 +1438,7 @@ export function App() {
icon={<Wallet size={15} />}
options={[
{ value: "all", label: "全部账户" },
...(quotaSnapshot?.accounts.map((account) => ({
value: account.id,
label: account.name,
})) ?? []),
...accountFilterChoices,
]}
/>
)}
Expand Down Expand Up @@ -1743,13 +1749,7 @@ export function App() {
? "单次平均费用"
: `单次平均 ${unitNames[unit]}`}
</dt>
<dd>
{unit === "usd" &&
insights.perRequest !== null &&
insights.perRequest < 0.01
? `$${insights.perRequest.toFixed(4)}`
: amount(insights.perRequest, unit)}
</dd>
<dd>{amount(insights.perRequest, unit)}</dd>
</div>
</dl>
) : null}
Expand Down
8 changes: 2 additions & 6 deletions src/web/components/AccountTrend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { LedgerView, UnitView } from "../../shared/ledger-view";
import { selectUsdView } from "../../shared/ledger-view";
import type { UsdBasis } from "../../domain/pricing";
import {
amount,
compact,
compactAxis,
continuousPoints,
Expand Down Expand Up @@ -110,11 +111,6 @@ export function selectAccountTrendPoints(
return view.view.units[metric].points;
}

const preciseUsdFormat = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 6,
});
const percentFormat = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 2,
});
Expand All @@ -129,7 +125,7 @@ export function formatMetricValue(
) {
if (value === null || !Number.isFinite(value)) return "N/A";
if (metric === "tokens") return `${compact(value)} ${metricLabels[metric]}`;
if (metric === "usd") return preciseUsdFormat.format(value);
if (metric === "usd") return amount(value, "usd");
const formatted = (metric === "percent" ? percentFormat : countFormat).format(
value,
);
Expand Down
31 changes: 31 additions & 0 deletions src/web/lib/account-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { LedgerAccount } from "../../shared/report";

export interface AccountFilterOption {
value: string;
label: string;
}

/**
* 账户筛选只列使用中的账户,已归档和已删除的账户不再出现在下拉里;当前筛选的账户
* 恰好是其中之一时补一项并注明状态,从账户卡片或归档列表钻取过来后仍能辨认。
*/
export function accountFilterOptions(
visible: readonly LedgerAccount[],
all: readonly LedgerAccount[],
selected: string,
state: { archived: ReadonlySet<string>; hidden: ReadonlySet<string> },
): AccountFilterOption[] {
const options = visible.map((account) => ({
value: account.id,
label: account.name,
}));
if (selected === "all" || visible.some((account) => account.id === selected))
return options;
const name = all.find((account) => account.id === selected)?.name ?? selected;
const suffix = state.archived.has(selected)
? "(已归档)"
: state.hidden.has(selected)
? "(已删除)"
: "";
return [...options, { value: selected, label: `${name}${suffix}` }];
}
29 changes: 23 additions & 6 deletions src/web/lib/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,28 @@ const compactAxisFormat = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
const usdFormat = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 2,
});
/**
* 金额按量级决定最多显示几位小数:常规金额保留两位,低于一美分时多留位数到能写出
* 有效数字,尾部的零由 Intl 省略。低价模型的单次费用因此不会都显示成 $0.00。
*/
const usdFormats = new Map<number, Intl.NumberFormat>();
function usdFormat(value: number) {
const digits =
value === 0
? 2
: Math.min(8, Math.max(2, 2 - Math.floor(Math.log10(Math.abs(value)))));
let format = usdFormats.get(digits);
if (!format) {
format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: digits,
});
usdFormats.set(digits, format);
}
return format;
}
const creditsFormat = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1,
});
Expand All @@ -110,7 +127,7 @@ export function compactAxis(value: number) {

export function amount(value: number | null, unit: ReportUnit) {
if (value === null || !Number.isFinite(value)) return "N/A";
if (unit === "usd") return usdFormat.format(value);
if (unit === "usd") return usdFormat(value).format(value);
return unit === "tokens" ? compact(value) : creditsFormat.format(value);
}

Expand Down
6 changes: 0 additions & 6 deletions src/web/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -510,12 +510,6 @@
margin-top: 0;
overflow-wrap: anywhere;
}
.details-list {
grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
}
.details-list dd {
min-width: 0;
}
.detail-amount {
font-size: 28px;
overflow-wrap: anywhere;
Expand Down
4 changes: 3 additions & 1 deletion src/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1229,13 +1229,15 @@ footer > span {
}
.details-list {
display: grid;
grid-template-columns: 1fr auto;
/* 标签列按内容取宽,长 ID 只在数值列内折行,不能把标签挤成竖排。 */
grid-template-columns: max-content minmax(0, 1fr);
gap: 17px 12px;
margin: 24px 0;
font-size: 14px;
}
.details-list dt {
color: var(--muted);
white-space: nowrap;
}
.details-list dd {
margin: 0;
Expand Down
54 changes: 54 additions & 0 deletions tests/account-filter.test.ts
10BC8
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect, test } from "bun:test";
import type { LedgerAccount } from "../src/shared/report";
import { accountFilterOptions } from "../src/web/lib/account-filter";

const account = (id: string, name: string): LedgerAccount => ({
id,
name,
plan: "",
kind: "subscription",
sampledAt: null,
fiveHour: null,
sevenDay: null,
});

const active = account("active", "Claude Ara");
const archived = account("archived", "Claude Team");
const hidden = account("hidden", "OpenCode Go");
const all = [active, archived, hidden];

test("account filter leaves archived and deleted accounts out of the menu", () => {
expect(
accountFilterOptions([active], all, "all", {
archived: new Set([archived.id]),
hidden: new Set([hidden.id]),
}),
).toEqual([{ value: "active", label: "Claude Ara" }]);
});

test("account filter keeps the drilled-in archived account visible and labelled", () => {
expect(
accountFilterOptions([active], all, archived.id, {
archived: new Set([archived.id]),
hidden: new Set(),
}),
).toEqual([
{ value: "active", label: "Claude Ara" },
{ value: "archived", label: "Claude Team(已归档)" },
]);
});

test("account filter marks a selected deleted account without inventing a name", () => {
expect(
accountFilterOptions([active], all, hidden.id, {
archived: new Set(),
hidden: new Set([hidden.id]),
}).at(-1),
).toEqual({ value: "hidden", label: "OpenCode Go(已删除)" });
expect(
accountFilterOptions([active], all, "unknown", {
archived: new Set(),
hidden: new Set(),
}).at(-1),
).toEqual({ value: "unknown", label: "unknown" });
});
22 changes: 20 additions & 2 deletions tests/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { LedgerRecord } from "../src/shared/report";
import { createDemoLedger } from "../src/web/demo/ledger";
import { datePresets } from "../src/web/components/DateRangePicker";
import {
amount,
bucketTime,
csv,
filterRecords,
Expand All @@ -20,14 +21,31 @@ const filter = { days: 7, model: "all", account: "all", search: "" };

describe("prototype report contract", () => {
test("scientific decimal amounts remain known and aggregate without binary rounding", () => {
const rows = ["1e-7", "2E-7", "0.0000003"].map(usd => ({ ...snapshot.records[0]!, usd }));
expect(summarize(rows, "usd")).toEqual({ value: 6e-7, hasKnown: true, knownRows: 3, incompleteRows: 0 });
const rows = ["1e-7", "2E-7", "0.0000003"].map((usd) => ({
...snapshot.records[0]!,
usd,
}));
expect(summarize(rows, "usd")).toEqual({
value: 6e-7,
hasKnown: true,
knownRows: 3,
incompleteRows: 0,
});
expect(numericAmount("1e-7")).toBe(1e-7);
expect(numericAmount("-1e-7")).toBeNull();
expect(numericAmount("1e9999")).toBeNull();
expect(numericAmount("Infinity")).toBeNull();
expect(aggregateReport(rows, "model")[0]!.usd).toBe("6e-7");
});
test("usd display keeps low-priced requests readable instead of rounding to zero", () => {
expect(amount(0.04, "usd")).toBe("$0.04");
expect(amount(0.0123, "usd")).toBe("$0.0123");
expect(amount(0.0014, "usd")).toBe("$0.0014");
expect(amount(0.000123, "usd")).toBe("$0.000123");
expect(amount(1234.5, "usd")).toBe("$1,234.50");
expect(amount(0, "usd")).toBe("$0.00");
expect(amount(null, "usd")).toBe("N/A");
});
test("custom calendar dates include both selected days without overlapping comparison", () => {
const rows = [
"2026-09-01T15:59:59.999Z",
Expand Down
Loading
Loading
0