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
26 changes: 26 additions & 0 deletions docs/frontend-ui-audit-2026-08-06/RuntimeUsageCharts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Runtime usage charts UI audit

## Scope

- `src/modules/shared/dataSource/TeamRuntimeToday.tsx`
- `src/modules/shared/dataSource/TeamMemberDetail.tsx`

The configured `frontend-ui-audit` skill file was unavailable, so this report
uses the repository's documented audit columns and manually checks design-system
reuse, arbitrary Tailwind values, accessibility, and duplicated visual patterns.

## Findings

| Line | Element | Verdict | Reason | Suggested change |
| -------------------------- | ------------------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `TeamRuntimeToday.tsx:258` | Rolling usage chart section | keep with reason | Reuses the existing lazy-loaded `UsageTrendChart`, shared section heading classes, semantic color tokens, and existing localized range/title strings. The standard `h-72` fallback adds no arbitrary Tailwind value. | None. |
| `TeamRuntimeToday.tsx:284` | Per-member usage breakdown | keep with reason | Reuses `SectionContainer`, `Avatar`, existing typography/border/fill tokens, and native buttons. `aria-pressed` exposes the selected member filter to assistive technology. | None. |
| `TeamMemberDetail.tsx:315` | Source filter visibility in 24h mode | keep with reason | Reuses the existing `TabPill`; hiding it for the all-source hourly snapshot prevents a control from implying unsupported per-source hourly data. | None. |
| `TeamMemberDetail.tsx:336` | Usage range selector | keep with reason | Reuses the shared `Select` and existing localized `24h` label, keeps the default daily controls intact, and adds a stable test id without introducing a parallel range component. | None. |

## Summary

- fix: 0
- keep with reason: 4
- abstract: 0
- systematic sweep candidates: 0
2 changes: 1 addition & 1 deletion src-tauri/crates/orgtrack-core/src/usage_dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ mod rounds;
mod tests;

use accumulator::UsageHeadlineAccumulator;
pub use daily_rollup::{usage_daily_rollup, DailyRollup, DailyRollupRow};
pub use daily_rollup::{usage_daily_rollup, DailyRollup, DailyRollupRow, RecentUsageSnapshot};
pub use overview::{usage_overview, usage_rounds, usage_trends, UsageOverview};
use rounds::visit_rounds;
pub use rounds::UsageRoundRow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ use std::collections::{BTreeMap, HashSet};
use rusqlite::Connection;
use serde::Serialize;

use super::accumulator::UsageHeadlineAccumulator;
use super::rounds::visit_rounds_windowed;
use super::{TrendBucket, UsageFilter};
use super::{TrendBucket, UsageFilter, UsageSummary, UsageTrendPoint};

const RECENT_USAGE_WINDOW_MS: i64 = 86_400_000;

/// One (UTC-day-floor, bucket) aggregate row.
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
Expand Down Expand Up @@ -60,6 +63,22 @@ pub struct DailyRollup {
/// retains the windowed daily rows, so the lifetime figure has to ride
/// along from the client.
pub total_sessions: i64,
/// Rolling 24-hour headline + hourly series derived during the same
/// bounded round scan. The member-runtime scheduler attaches this to the
/// opaque status stats blob, so team viewers can render an accurate 24h
/// chart without another local scan or a cloud schema migration.
pub recent_usage_24h: RecentUsageSnapshot,
}

/// A bounded local usage snapshot suitable for the member-runtime status
/// payload. Empty hourly buckets are omitted here and filled by the chart.
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RecentUsageSnapshot {
pub start_ms: i64,
pub end_ms: i64,
pub summary: UsageSummary,
pub trends: Vec<UsageTrendPoint>,
}

#[derive(Default)]
Expand Down Expand Up @@ -95,6 +114,7 @@ pub fn usage_daily_rollup(
start_ms: i64,
end_ms: i64,
) -> Result<DailyRollup, String> {
let recent_start_ms = start_ms.max(end_ms.saturating_sub(RECENT_USAGE_WINDOW_MS));
let filter = UsageFilter {
bucket: None,
start_ms: Some(start_ms),
Expand All @@ -107,12 +127,16 @@ pub fn usage_daily_rollup(

// BTreeMap keys give the required (day, bucket) output ordering for free.
let mut cells: BTreeMap<(i64, String), RollupCell> = BTreeMap::new();
let mut recent = UsageHeadlineAccumulator::new(TrendBucket::Hour, true, true);
visit_rounds_windowed(conn, &filter, |round| {
// Rounds without a usable timestamp cannot be attributed to a UTC
// day (mirrors the trend accumulator's `created_at_ms > 0` gate).
if round.created_at_ms <= 0 {
return Ok(());
}
if round.created_at_ms >= recent_start_ms {
recent.observe(&round);
}
let day_start_ms = TrendBucket::Day.floor(round.created_at_ms);
let cell = cells
.entry((day_start_ms, round.bucket.clone()))
Expand Down Expand Up @@ -147,8 +171,16 @@ pub fn usage_daily_rollup(
})
.collect();

let (recent_summary, recent_trends) = recent.finish();

Ok(DailyRollup {
days,
total_sessions: total_session_count(conn)?,
recent_usage_24h: RecentUsageSnapshot {
start_ms: recent_start_ms,
end_ms,
summary: recent_summary,
trends: recent_trends,
},
})
}
67 changes: 67 additions & 0 deletions src-tauri/crates/orgtrack-core/src/usage_dashboard/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,60 @@ fn daily_rollup_window_clips_rounds() {
assert_eq!(rollup.days[0].input_tokens, 400_000);
}

#[test]
fn daily_rollup_derives_a_true_rolling_24h_snapshot_in_the_same_scan() {
let conn = fixture_conn();
insert_code_session(
&conn,
"rolling-claude",
"claude",
"Rolling window",
"2026-07-18T12:00:00+00:00",
);
insert_turn(
&conn,
"rolling-claude",
"claude-sonnet-4-5",
(100, 10, 0, 0),
"2026-07-17T11:59:59.999Z",
);
insert_turn(
&conn,
"rolling-claude",
"claude-sonnet-4-5",
(200, 20, 30, 40),
"2026-07-17T12:00:00Z",
);
recompute_session_usage(&conn, "rolling-claude")
.unwrap()
.expect("rolling session projected");

let end_ms = ms("2026-07-18T12:00:00Z");
let rollup = usage_daily_rollup(&conn, ms("2026-07-01T00:00:00Z"), end_ms)
.expect("daily + rolling rollup");

assert_eq!(rollup.recent_usage_24h.start_ms, ms("2026-07-17T12:00:00Z"));
assert_eq!(rollup.recent_usage_24h.end_ms, end_ms);
assert_eq!(rollup.recent_usage_24h.summary.input_tokens, 200);
assert_eq!(rollup.recent_usage_24h.summary.output_tokens, 20);
assert_eq!(rollup.recent_usage_24h.summary.cache_read_tokens, 30);
assert_eq!(rollup.recent_usage_24h.summary.cache_write_tokens, 40);
assert_eq!(rollup.recent_usage_24h.summary.session_count, 1);
assert_eq!(rollup.recent_usage_24h.summary.request_count, 1);
assert_eq!(rollup.recent_usage_24h.trends.len(), 1);
assert_eq!(
rollup.recent_usage_24h.trends[0].bucket_ms,
ms("2026-07-17T12:00:00Z")
);

// The daily sync window still contains both rounds; the rolling snapshot
// alone excludes the row one millisecond before the 24h boundary.
assert_eq!(
rollup.days.iter().map(|row| row.input_tokens).sum::<i64>(),
300
);
}

#[test]
fn daily_rollup_skips_zero_usage_cells() {
let conn = seeded_conn();
Expand Down Expand Up @@ -1022,6 +1076,16 @@ fn daily_rollup_serializes_camel_case() {
let json = serde_json::to_value(DailyRollup {
days: vec![row],
total_sessions: 42,
recent_usage_24h: RecentUsageSnapshot {
start_ms: 1_784_332_800_000,
end_ms: 1_784_419_200_000,
summary: UsageSummary::default(),
trends: vec![UsageTrendPoint {
bucket_ms: 1_784_332_800_000,
input_tokens: 5,
..UsageTrendPoint::default()
}],
},
})
.expect("serialize rollup");
assert_eq!(json["totalSessions"], 42);
Expand All @@ -1036,4 +1100,7 @@ fn daily_rollup_serializes_camel_case() {
assert_eq!(row["costUsd"], 0.5);
assert_eq!(row["sessions"], 1);
assert_eq!(row["requests"], 2);
assert_eq!(json["recentUsage24h"]["startMs"], 1_784_332_800_000_i64);
assert_eq!(json["recentUsage24h"]["endMs"], 1_784_419_200_000_i64);
assert_eq!(json["recentUsage24h"]["trends"][0]["inputTokens"], 5);
}
7 changes: 4 additions & 3 deletions src-tauri/src/orgtrack/usage_dashboard_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,10 @@ pub async fn usage_dashboard_rounds(
.map_err(|err| format!("Task join error: {err}"))?
}

/// Per-(UTC day, bucket) rollup for the member-runtime cloud push. Unlike the
/// scoped desktop views above, this always spans ALL sources (the `other`
/// bucket included) so the totals a member shares with their org are complete.
/// Per-(UTC day, bucket) rollup plus rolling-24h snapshot for the
/// member-runtime cloud push. Unlike the scoped desktop views above, this
/// always spans ALL sources (the `other` bucket included) so the totals a
/// member shares with their org are complete.
#[tauri::command]
pub async fn usage_dashboard_daily_rollup(
start_ms: i64,
Expand Down
14 changes: 12 additions & 2 deletions src/api/tauri/usageDashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export interface UsageTrendPoint {
costUsd: number;
}

/** A bounded headline + trend snapshot captured at one instant. */
export interface RecentUsageSnapshot {
startMs: number;
endMs: number;
summary: UsageSummary;
trends: UsageTrendPoint[];
}

export interface UsageSessionRow {
sessionId: string;
name: string;
Expand Down Expand Up @@ -302,14 +310,16 @@ export interface DailyRollupResult {
days: DailyRollupRow[];
/** Lifetime mirror-deduped session count — independent of the window. */
totalSessions: number;
/** Rolling 24h usage derived during the same local round scan. */
recentUsage24h: RecentUsageSnapshot;
}

/**
* Per-UTC-day, per-bucket rollup over `[startMs, endMs]` for the
* member-runtime push (registered behind the same 1-permit semaphore as the
* other dashboard scans, so a plain wrapper is enough — concurrent callers
* queue in the backend). Also carries the lifetime session census the push
* shares as `stats.totalSessions`.
* queue in the backend). Also carries the lifetime session census and a
* rolling-24h snapshot the push shares through its bounded status blob.
*/
export async function usageDashboardDailyRollup(
startMs: number,
Expand Down
53 changes: 50 additions & 3 deletions src/features/Org2Cloud/memberRuntime/memberRuntimeClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,45 @@ describe("listMemberRuntime", () => {
reportedAt: "2026-07-29T09:00:00Z",
machine: STATUS_INPUT.status?.machine,
sample: STATUS_INPUT.status?.sample,
stats: { totalSessions: 321 },
stats: {
totalSessions: 321,
recentUsage24h: {
startMs: 1_753_000_000_000,
endMs: 1_753_086_400_000,
summary: {
sessionCount: 2,
requestCount: 3,
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 30,
cacheWriteTokens: 40,
realTotalTokens: 100,
totalTokens: 100,
costUsd: 1.25,
estimatedCostUsd: 1.25,
recordedCostUsd: 0,
cacheHitRate: 0.375,
byBucket: [
{
bucket: "claude",
sessionCount: 2,
realTotalTokens: 100,
costUsd: 1.25,
},
],
},
trends: [
{
bucketMs: 1_753_000_000_000,
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 30,
cacheWriteTokens: 40,
costUsd: 1.25,
},
],
},
},
builderTypeCode: "MDFS",
profile: { code: "MDFS", axes: [], extraFutureField: 1 },
installedAgents: [{ id: "claude", status: "installed" }],
Expand Down Expand Up @@ -206,6 +244,10 @@ describe("listMemberRuntime", () => {
reportedAt: null,
machine: { totally: "malformed" },
sample: "not-an-object",
stats: {
totalSessions: 7,
recentUsage24h: { startMs: "malformed" },
},
builderTypeCode: null,
profile: null,
installedAgents: "garbage",
Expand All @@ -223,7 +265,9 @@ describe("listMemberRuntime", () => {
expect(members[0].userId).toBe("user-1");
expect(members[0].machine?.deviceId).toBe("dev-1");
expect(members[0].sample?.cpuPercent).toBe(42.5);
expect(members[0].stats).toEqual({ totalSessions: 321 });
expect(members[0].stats?.totalSessions).toBe(321);
expect(members[0].stats?.recentUsage24h?.summary.realTotalTokens).toBe(100);
expect(members[0].stats?.recentUsage24h?.trends).toHaveLength(1);
expect(members[0].profile?.code).toBe("MDFS");
expect(members[0].installedAgents).toEqual([
{ id: "claude", status: "installed" },
Expand All @@ -234,7 +278,10 @@ describe("listMemberRuntime", () => {
expect(members[1].displayName).toBeNull();
expect(members[1].machine).toBeNull();
expect(members[1].sample).toBeNull();
expect(members[1].stats).toBeNull();
expect(members[1].stats).toEqual({
totalSessions: 7,
recentUsage24h: undefined,
});
expect(members[1].profile).toBeNull();
expect(members[1].installedAgents).toEqual([]);
expect(members[1].recentDays).toEqual([]);
Expand Down
53 changes: 52 additions & 1 deletion src/features/Org2Cloud/memberRuntime/memberRuntimeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,51 @@ const MemberUsageDayWireSchema = z.object({
requests: z.number().catch(0),
});

const NonNegativeNumberWireSchema = z.number().nonnegative().catch(0);

const UsageBucketSummaryWireSchema = z.object({
bucket: z.string(),
sessionCount: NonNegativeNumberWireSchema,
realTotalTokens: NonNegativeNumberWireSchema,
costUsd: NonNegativeNumberWireSchema,
});

const UsageSummaryWireSchema = z.object({
sessionCount: NonNegativeNumberWireSchema,
requestCount: NonNegativeNumberWireSchema,
inputTokens: NonNegativeNumberWireSchema,
outputTokens: NonNegativeNumberWireSchema,
cacheReadTokens: NonNegativeNumberWireSchema,
cacheWriteTokens: NonNegativeNumberWireSchema,
realTotalTokens: NonNegativeNumberWireSchema,
totalTokens: NonNegativeNumberWireSchema,
costUsd: NonNegativeNumberWireSchema,
estimatedCostUsd: NonNegativeNumberWireSchema,
recordedCostUsd: NonNegativeNumberWireSchema,
cacheHitRate: z.number().min(0).max(1).catch(0),
byBucket: z
.array(UsageBucketSummaryWireSchema)
.max(TEAM_USAGE_BUCKETS.length)
.catch([])
.default([]),
});

const UsageTrendPointWireSchema = z.object({
bucketMs: z.number().nonnegative(),
inputTokens: NonNegativeNumberWireSchema,
outputTokens: NonNegativeNumberWireSchema,
cacheReadTokens: NonNegativeNumberWireSchema,
cacheWriteTokens: NonNegativeNumberWireSchema,
costUsd: NonNegativeNumberWireSchema,
});

const RecentUsageSnapshotWireSchema = z.object({
startMs: z.number().nonnegative(),
endMs: z.number().nonnegative(),
summary: UsageSummaryWireSchema,
trends: z.array(UsageTrendPointWireSchema).max(25).catch([]).default([]),
});

const optionalString = z
.string()
.nullish()
Expand Down Expand Up @@ -214,7 +259,13 @@ const MemberRuntimeListEntryWireSchema = z.object({
reportedAt: z.string().nullish().catch(undefined),
machine: MemberRuntimeMachineWireSchema.nullish().catch(undefined),
sample: MemberRuntimeSampleWireSchema.nullish().catch(undefined),
stats: z.object({ totalSessions: z.number() }).nullish().catch(undefined),
stats: z
.object({
totalSessions: z.number(),
recentUsage24h: RecentUsageSnapshotWireSchema.optional().catch(undefined),
})
.nullish()
.catch(undefined),
builderTypeCode: z.string().nullish().catch(undefined),
profile: MemberBuilderProfileWireSchema.nullish().catch(undefined),
installedAgents: z
Expand Down
Loading
Loading