mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 19:56:31 +02:00
fix(ui): present missing costs as an informational notice (#15459)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Costs dashboard shows spending and gaps in cost data. > - Historical usage can have token counts without a recorded price. > - The red notice implies that the user must fix those records. > - This pull request shows the missing costs as a neutral note below the totals. > - Users can see the limit of the totals without a false call to action. ## Linked Issues or Issue Description Refs #14997. **What happened?** The Costs dashboard shows a red notice when a selected period contains usage without prices. It also shows “0 runs await accounting” when no runs are pending. Historical pricing gaps can therefore look like current errors that need cleanup. **Expected behavior** Show a neutral explanation below the totals. State that totals include only known costs. Show pending runs separately and only when the count is positive. **Steps to reproduce** 1. Open Costs for a period with unpriced usage and no pending runs. 2. Observe the red notice above the totals. 3. Apply this change. Confirm that the notice is muted and below the totals, with no zero-count pending message. **Paperclip version or commit** Reproduced on master after #14997. **Deployment mode** Board UI in both the embedded Activity page and the standalone Costs page. ## What Changed - Move the cost-data notice below the summary tiles and use the muted text token. - Explain unavailable prices with “Totals include known costs only.” - Separate pending runs from missing prices. Omit zero counts and use singular or plural copy as needed. - Update the existing UI tests for both page variants, including missing-only, pending-only, and fully accounted states after refresh. ## Verification - `pnpm --dir ui exec vitest run src/pages/Costs.test.tsx`: 21 tests passed on the final commit. Both page variants cover mixed, missing-only, pending-only, and fully accounted states. - `pnpm check:token-gates`: passed on the final commit. - `pnpm build`: passed locally. - `pnpm -r typecheck`: passed locally. - Full Linux CI passed on `0c23544c0f17e6641cc0c8de6ed498a731748bde`, including browser tests, server tests, shared-package tests, build, and typecheck. [CI run](https://github.com/paperclipai/paperclip/actions/runs/37661883464). - Greptile Apex: 5/5 on the final commit, with no unresolved review threads. - Local full-suite limit: `pnpm test:run` first failed because the fresh checkout lacked embedded PostgreSQL library links. A runtime-skill fixture also selected an unrelated parent directory, and a load test timed out. After setup repair, the database suite (70 tests), skill suite (3 tests), email suite (39 tests), and load suite (4 tests) passed individually. The subsequent full local rerun was stopped after the complete Linux CI run passed. This is not a claim that a full local test invocation passed. ## Risks Low risk. This changes presentation only. The note remains visible and retains its accessible status role. Cost calculations, stored records, and budget enforcement do not change. The copy does not assume that all missing prices are historical. No schema migration or operator documentation change is needed. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, repository inspection, code editing, and test execution. The exact served model identifier and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
465140596f
commit
572cec0344
2 files changed
+33
-7
No files matched your search
@@ -241,7 +241,7 @@ describe("Shared Costs surfaces", () => {
|
||||
} finally { queryClient.clear(); vi.useRealTimers(); }
|
||||
});
|
||||
|
||||
it.each(surfaces)("shows incomplete accounting and currency boundaries on the %s page", async (_name, props) => {
|
||||
it.each(surfaces)("explains missing costs and pending runs neutrally on the %s page", async (_name, props) => {
|
||||
for (const mock of Object.values(costsApiMocks)) mock.mockResolvedValue([]);
|
||||
costsApiMocks.byUser.mockResolvedValue({ activeUserCount: 1, rows: [] });
|
||||
costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: false, unpricedEventCount: 2, pendingRunCount: 1 });
|
||||
@@ -254,9 +254,26 @@ describe("Shared Costs surfaces", () => {
|
||||
root.render(<MemoryRouter><QueryClientProvider client={queryClient}><Costs {...props} /></QueryClientProvider></MemoryRouter>);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => expect(container.textContent).toContain("Spend is incomplete: 2 usage events have no reliable price; 1 runs await accounting."));
|
||||
await vi.waitFor(() => expect(container.textContent).toContain("Costs are unavailable for 2 usage entries in this period. Totals include known costs only."));
|
||||
});
|
||||
const notice = [...container.querySelectorAll('[role="status"]')].find(element => element.textContent?.includes("Costs are unavailable"))!;
|
||||
expect(notice.classList.contains("text-muted-foreground")).toBe(true);
|
||||
expect(notice.querySelector(".text-destructive")).toBeNull();
|
||||
expect(notice.classList.contains("text-destructive")).toBe(false);
|
||||
expect(notice.textContent).toContain("1 run is awaiting cost data.");
|
||||
expect(notice.previousElementSibling?.textContent).toContain("Inference spend");
|
||||
expect(container.textContent).toContain("Finance headline totals are USD only");
|
||||
costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: false, unpricedEventCount: 10, pendingRunCount: 0 });
|
||||
await act(async () => { await queryClient.invalidateQueries(); });
|
||||
await vi.waitFor(() => expect(notice.textContent).toContain("Costs are unavailable for 10 usage entries"));
|
||||
expect(notice.textContent).not.toContain("awaiting cost data");
|
||||
expect(notice.textContent).not.toContain("0 runs");
|
||||
costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: false, unpricedEventCount: 0, pendingRunCount: 1 });
|
||||
await act(async () => { await queryClient.invalidateQueries(); });
|
||||
await vi.waitFor(() => expect(notice.textContent).toBe("1 run is awaiting cost data."));
|
||||
costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: true, unpricedEventCount: 0, pendingRunCount: 0 });
|
||||
await act(async () => { await queryClient.invalidateQueries(); });
|
||||
await vi.waitFor(() => expect(container.contains(notice)).toBe(false));
|
||||
});
|
||||
|
||||
it.each(surfaces)("labels each agent and expanded model independently on the %s page", async (_name, props) => {
|
||||
|
||||
+14
-5
@@ -622,11 +622,6 @@ export function Costs({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{spendData?.summary.pricingComplete === false && (
|
||||
<p role="status" className="text-sm text-destructive">
|
||||
Spend is incomplete: {spendData.summary.unpricedEventCount} usage events have no reliable price; {spendData.summary.pendingRunCount} runs await accounting. Known spend is shown below.
|
||||
</p>
|
||||
)}
|
||||
{financeData?.summary.currencies?.some((row) => row.currency !== "USD") && (
|
||||
<p role="status" className="text-sm text-muted-foreground">
|
||||
Finance headline totals are USD only. Other currencies are listed separately; no exchange-rate conversion is applied.
|
||||
@@ -673,6 +668,20 @@ export function Costs({
|
||||
icon={ArrowUpRight}
|
||||
/>
|
||||
</div>
|
||||
{spendData?.summary.pricingComplete === false && (
|
||||
<div role="status" className="space-y-1 text-sm text-muted-foreground">
|
||||
{spendData.summary.unpricedEventCount > 0 && (
|
||||
<p>
|
||||
Costs are unavailable for {spendData.summary.unpricedEventCount} usage {spendData.summary.unpricedEventCount === 1 ? "entry" : "entries"} in this period. Totals include known costs only.
|
||||
</p>
|
||||
)}
|
||||
{spendData.summary.pendingRunCount > 0 && (
|
||||
<p>
|
||||
{spendData.summary.pendingRunCount} {spendData.summary.pendingRunCount === 1 ? "run is" : "runs are"} awaiting cost data.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in new issue
Block a user