tokentracker-cli 0.77.2 → 0.78.0
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/README.de.md +21 -0
- package/README.md +21 -0
- package/dashboard/dist/assets/ActivityHeatmap-5352XxLR.js +42 -0
- package/dashboard/dist/assets/{Card-D_aKGDr7.js → Card-Ca_hYHhx.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-vRiBiNhb.js → ClawdAnimated-DSgOS5ND.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CjetJZ-5.js → CommunityStatsModal-o8uQVQxu.js} +1 -1
- package/dashboard/dist/assets/ConfirmModal-D1hi9XUf.js +1 -0
- package/dashboard/dist/assets/Controls-CwnKP56V.js +1 -0
- package/dashboard/dist/assets/DashboardPage-CDv0TIOL.js +19 -0
- package/dashboard/dist/assets/{DevicePage-YAo4ert4.js → DevicePage-BlfIHcZX.js} +1 -1
- package/dashboard/dist/assets/DialogClose-CismfYwe.js +1 -0
- package/dashboard/dist/assets/DialogDescription-DWKp1yez.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CA5znwms.js → DialogTitle-DuuTVbUb.js} +1 -1
- package/dashboard/dist/assets/{FadeIn--ff4ZbC-.js → FadeIn-DpnitRKo.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CTZvanhz.js → HeaderGithubStar-D6zy-ADl.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Bg47gNVu.js → IpCheckPage-yUNp0VNe.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DZ8Mz38n.js → LandingPage-BvmpAaVn.js} +1 -1
- package/dashboard/dist/assets/LeaderboardAvatar-2WTIeXye.js +1 -0
- package/dashboard/dist/assets/LeaderboardPage-Bq3-VO7w.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-CfVMENby.js → LeaderboardProfileModal-DH0EEK6q.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-C4bGXJCE.js → LeaderboardProfilePage-uJAng2JE.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-BAQ10X55.js → LimitsPage-D8UohgJm.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-gtHCn7Mx.js → LocalOnlyNotice-Dv_BnDVV.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-TXvIBzoW.js → LoginPage-D6b6DZU3.js} +1 -1
- package/dashboard/dist/assets/PetPage-DfpBkuCa.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-CQxxgUXk.js → PopoverPopup-CjzdZn-9.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-tN0tmSfP.js → ResetPasswordPage-BnOCFkwe.js} +1 -1
- package/dashboard/dist/assets/Select-CHtQAiLD.js +1 -0
- package/dashboard/dist/assets/{SelectItemText-2LjzGvo5.js → SelectItemText-Vjqvf8wO.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-B-iHGL4q.js +1 -0
- package/dashboard/dist/assets/SkillsPage-DtG2aV-q.js +1 -0
- package/dashboard/dist/assets/WidgetsPage-4kqTtWfK.js +1 -0
- package/dashboard/dist/assets/{WrappedPage-YxshTScX.js → WrappedPage-DMMRpani.js} +1 -1
- package/dashboard/dist/assets/agent-logos-D0vYkEpX.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-Bm3t-iA0.js → arrow-up-right-BAQYGZkR.js} +1 -1
- package/dashboard/dist/assets/check-C3RPBZiS.js +1 -0
- package/dashboard/dist/assets/{copy-DUNz-xWm.js → copy-gG_BLBs8.js} +1 -1
- package/dashboard/dist/assets/{download-COSLX73O.js → download-BQOcg3gT.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CR28AiQO.js → format-tokens-CGK6lQAf.js} +1 -1
- package/dashboard/dist/assets/{info-Cm7V_KDO.js → info-C_BrDrcq.js} +1 -1
- package/dashboard/dist/assets/{link-2-CYqDGrkv.js → link-2-DFwCBbIZ.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-6XAdy2ec.js → loader-circle-BMFI_BJ8.js} +1 -1
- package/dashboard/dist/assets/main-Dy8Y_qst.css +1 -0
- package/dashboard/dist/assets/main-w0hAaCsy.js +1190 -0
- package/dashboard/dist/assets/{trash-2-DKLewOQb.js → trash-2-GeZ6IzgV.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-AdPaun74.js → use-community-stats-pC8cmVof.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Bau4ykMw.js → use-limits-display-prefs-DnFpPcA3.js} +1 -1
- package/dashboard/dist/assets/use-native-settings-CvcOSUhn.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-BVaEJaWa.js → use-ordered-list-CbMroVZ8.js} +2 -2
- package/dashboard/dist/assets/{use-quality-per-dollar-pref-Htlc-uFe.js → use-quality-per-dollar-pref-Bn3Al6yX.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BnzWTJQS.js → use-usage-limits-D08q_Dr8.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-bd6TLRsc.js → useCurrency-h3kqF52v.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-ajoWrfku.js → useScrollLock-9au2731u.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/status.js +119 -12
- package/src/commands/sync.js +158 -83
- package/src/lib/install-resolver.js +1 -0
- package/src/lib/local-api.js +202 -56
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +18 -2
- package/dashboard/dist/assets/ActivityHeatmap-CH6m_ZJM.js +0 -42
- package/dashboard/dist/assets/ConfirmModal-C6bZw6FZ.js +0 -1
- package/dashboard/dist/assets/Controls-Cj22oGL3.js +0 -1
- package/dashboard/dist/assets/DashboardPage-DPxiQkQw.js +0 -19
- package/dashboard/dist/assets/DialogClose-hhTDeRp1.js +0 -1
- package/dashboard/dist/assets/DialogDescription-gxEpcUbJ.js +0 -1
- package/dashboard/dist/assets/LeaderboardAvatar-3T57GKKK.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-wP_yCU9I.js +0 -2
- package/dashboard/dist/assets/PetPage-Dj1jrxYe.js +0 -1
- package/dashboard/dist/assets/Select-DnbHYPS4.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DosKS_L6.js +0 -1
- package/dashboard/dist/assets/SkillsPage-DaCJ25vT.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-CCGemQVD.js +0 -1
- package/dashboard/dist/assets/agent-logos-SeuxwQsp.js +0 -1
- package/dashboard/dist/assets/check-D8Owfjyw.js +0 -1
- package/dashboard/dist/assets/main-BCstrDYE.js +0 -1172
- package/dashboard/dist/assets/main-BYdJ5vWR.css +0 -1
- package/dashboard/dist/assets/use-native-settings-CidwlprT.js +0 -1
package/src/lib/local-api.js
CHANGED
|
@@ -44,6 +44,11 @@ const {
|
|
|
44
44
|
|
|
45
45
|
const { computeCodexContextBreakdown } = require("./codex-context-breakdown");
|
|
46
46
|
|
|
47
|
+
const {
|
|
48
|
+
deriveProjectKeyFromRef,
|
|
49
|
+
CLAUDE_MEM_OBSERVER_PROJECT_REF,
|
|
50
|
+
} = require("./rollout");
|
|
51
|
+
|
|
47
52
|
// ---------------------------------------------------------------------------
|
|
48
53
|
// Queue data helpers
|
|
49
54
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +58,38 @@ function resolveQueuePath() {
|
|
|
53
58
|
return path.join(home, ".tokentracker", "tracker", "queue.jsonl");
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
// Pseudo-project written by the pre-fix claude-mem observer attribution;
|
|
62
|
+
// mirrored Claude Code sessions, not a real repository. Excluded at read
|
|
63
|
+
// time so historical rows never surface on the Project Usage panel. Derived
|
|
64
|
+
// from rollout.js's ref so a rename there cannot silently desync the filter.
|
|
65
|
+
const CLAUDE_MEM_OBSERVER_PROJECT_KEY = deriveProjectKeyFromRef(
|
|
66
|
+
CLAUDE_MEM_OBSERVER_PROJECT_REF,
|
|
67
|
+
);
|
|
68
|
+
const PROJECT_USAGE_MAX_ENTRIES = 10;
|
|
69
|
+
|
|
70
|
+
// Shared by project-usage-summary and project-usage-detail: reads the
|
|
71
|
+
// deduped project bucket log, drops the claude-mem pseudo project, and
|
|
72
|
+
// builds the from/to day-range predicate. Callers compute each row's day
|
|
73
|
+
// key ONCE (rowDayKey constructs an Intl.DateTimeFormat per call — the
|
|
74
|
+
// dominant per-row cost) and reuse it for both the range check and any
|
|
75
|
+
// day bucketing.
|
|
76
|
+
function readProjectUsageContext(qp, url) {
|
|
77
|
+
const from = url.searchParams.get("from") || "";
|
|
78
|
+
const to = url.searchParams.get("to") || "";
|
|
79
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
80
|
+
const hasRange = Boolean(from || to);
|
|
81
|
+
const dayInRange = (day) => {
|
|
82
|
+
if (!hasRange) return true;
|
|
83
|
+
if (!day) return false;
|
|
84
|
+
return (!from || day >= from) && (!to || day <= to);
|
|
85
|
+
};
|
|
86
|
+
const projectQueuePath = path.join(path.dirname(qp), "project.queue.jsonl");
|
|
87
|
+
const projectRows = readProjectQueueData(projectQueuePath).filter(
|
|
88
|
+
(row) => row.project_key !== CLAUDE_MEM_OBSERVER_PROJECT_KEY,
|
|
89
|
+
);
|
|
90
|
+
return { from, to, timeZoneContext, hasRange, dayInRange, projectRows };
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
function readProjectQueueData(projectQueuePath) {
|
|
57
94
|
let raw;
|
|
58
95
|
try {
|
|
@@ -1912,76 +1949,185 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1912
1949
|
// hour_start). Falling back to "session-file count × total tokens"
|
|
1913
1950
|
// (the old behavior) produced pure fiction: every short-and-hot
|
|
1914
1951
|
// project got the same weight as every long-and-cold one.
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
const projectRows =
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const
|
|
1924
|
-
if (!byProject.has(key)) {
|
|
1925
|
-
byProject.set(key, {
|
|
1926
|
-
project_key: key,
|
|
1927
|
-
project_ref: row.project_ref || key,
|
|
1928
|
-
total_tokens: 0,
|
|
1929
|
-
billable_total_tokens: 0,
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
const agg = byProject.get(key);
|
|
1933
|
-
agg.total_tokens += Number(row.total_tokens || 0);
|
|
1934
|
-
agg.billable_total_tokens += Number(row.total_tokens || 0);
|
|
1935
|
-
if (!agg.project_ref && row.project_ref) agg.project_ref = row.project_ref;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
// If no project-attributed rows exist yet (user hasn't synced project
|
|
1939
|
-
// attribution, or never used a project-capable CLI), fall back to
|
|
1940
|
-
// per-source aggregation over the main queue so the panel isn't
|
|
1941
|
-
// totally empty. This path used to also exist for the non-empty case
|
|
1942
|
-
// and produce wrong numbers; keep it only as the empty fallback.
|
|
1943
|
-
let entries;
|
|
1944
|
-
if (byProject.size === 0) {
|
|
1945
|
-
const rows = readQueueData(qp);
|
|
1946
|
-
const bySrc = new Map();
|
|
1952
|
+
const limitParam = Number(url.searchParams.get("limit"));
|
|
1953
|
+
const limit = Number.isFinite(limitParam) && limitParam > 0
|
|
1954
|
+
? Math.min(Math.floor(limitParam), PROJECT_USAGE_MAX_ENTRIES)
|
|
1955
|
+
: PROJECT_USAGE_MAX_ENTRIES;
|
|
1956
|
+
const { timeZoneContext, hasRange, dayInRange, projectRows } =
|
|
1957
|
+
readProjectUsageContext(qp, url);
|
|
1958
|
+
|
|
1959
|
+
const aggregateEntries = (rows, keyOf, refOf) => {
|
|
1960
|
+
const byKey = new Map();
|
|
1947
1961
|
for (const row of rows) {
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
// domains (e.g. codex.ai, cursor.ai) and was sent to the
|
|
1955
|
-
// dashboard as a clickable href before v0.11.1 / this commit.
|
|
1956
|
-
project_ref: "",
|
|
1962
|
+
if (hasRange && !dayInRange(rowDayKey(row, timeZoneContext))) continue;
|
|
1963
|
+
const key = keyOf(row);
|
|
1964
|
+
if (!byKey.has(key)) {
|
|
1965
|
+
byKey.set(key, {
|
|
1966
|
+
project_key: key,
|
|
1967
|
+
project_ref: refOf(row),
|
|
1957
1968
|
total_tokens: 0,
|
|
1958
1969
|
billable_total_tokens: 0,
|
|
1970
|
+
input_tokens: 0,
|
|
1971
|
+
output_tokens: 0,
|
|
1972
|
+
cached_input_tokens: 0,
|
|
1973
|
+
cache_creation_input_tokens: 0,
|
|
1974
|
+
reasoning_output_tokens: 0,
|
|
1975
|
+
conversation_count: 0,
|
|
1976
|
+
sourceTotals: new Map(),
|
|
1959
1977
|
});
|
|
1960
1978
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1979
|
+
const agg = byKey.get(key);
|
|
1980
|
+
agg.total_tokens += Number(row.total_tokens || 0);
|
|
1981
|
+
agg.billable_total_tokens += Number(
|
|
1982
|
+
row.billable_total_tokens ?? row.total_tokens ?? 0,
|
|
1983
|
+
);
|
|
1984
|
+
agg.input_tokens += Number(row.input_tokens || 0);
|
|
1985
|
+
agg.output_tokens += Number(row.output_tokens || 0);
|
|
1986
|
+
agg.cached_input_tokens += Number(row.cached_input_tokens || 0);
|
|
1987
|
+
agg.cache_creation_input_tokens += Number(row.cache_creation_input_tokens || 0);
|
|
1988
|
+
agg.reasoning_output_tokens += Number(row.reasoning_output_tokens || 0);
|
|
1989
|
+
agg.conversation_count += Number(row.conversation_count || 0);
|
|
1990
|
+
if (!agg.project_ref) agg.project_ref = refOf(row);
|
|
1991
|
+
const src = row.source || "unknown";
|
|
1992
|
+
agg.sourceTotals.set(
|
|
1993
|
+
src,
|
|
1994
|
+
(agg.sourceTotals.get(src) || 0) + Number(row.total_tokens || 0),
|
|
1995
|
+
);
|
|
1963
1996
|
}
|
|
1964
|
-
|
|
1997
|
+
return Array.from(byKey.values())
|
|
1965
1998
|
.sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
|
|
1966
|
-
.
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
.map((e) => ({
|
|
1975
|
-
...e,
|
|
1976
|
-
total_tokens: String(e.total_tokens),
|
|
1977
|
-
billable_total_tokens: String(e.billable_total_tokens),
|
|
1999
|
+
.slice(0, limit)
|
|
2000
|
+
.map(({ sourceTotals, ...entry }) => ({
|
|
2001
|
+
...entry,
|
|
2002
|
+
total_tokens: String(entry.total_tokens),
|
|
2003
|
+
billable_total_tokens: String(entry.billable_total_tokens),
|
|
2004
|
+
sources: Array.from(sourceTotals.entries())
|
|
2005
|
+
.sort((a, b) => b[1] - a[1])
|
|
2006
|
+
.map(([source, totalTokens]) => ({ source, total_tokens: totalTokens })),
|
|
1978
2007
|
}));
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
let entries = aggregateEntries(
|
|
2011
|
+
projectRows,
|
|
2012
|
+
(row) => row.project_key || "unknown",
|
|
2013
|
+
(row) => row.project_ref || "",
|
|
2014
|
+
);
|
|
2015
|
+
|
|
2016
|
+
// If no project-attributed rows exist yet (user hasn't synced project
|
|
2017
|
+
// attribution, or never used a project-capable CLI), fall back to
|
|
2018
|
+
// per-source aggregation over the main queue so the panel isn't
|
|
2019
|
+
// totally empty. This path used to also exist for the non-empty case
|
|
2020
|
+
// and produce wrong numbers; keep it only as the empty fallback.
|
|
2021
|
+
if (entries.length === 0 && projectRows.length === 0) {
|
|
2022
|
+
entries = aggregateEntries(
|
|
2023
|
+
readQueueData(qp),
|
|
2024
|
+
(row) => row.source || "unknown",
|
|
2025
|
+
// Synthetic source-only row: leave project_ref empty rather than
|
|
2026
|
+
// fabricating `https://${src}.ai`, which resolves to unrelated
|
|
2027
|
+
// domains (e.g. codex.ai, cursor.ai) and was sent to the
|
|
2028
|
+
// dashboard as a clickable href before v0.11.1 / this commit.
|
|
2029
|
+
() => "",
|
|
2030
|
+
);
|
|
1979
2031
|
}
|
|
1980
2032
|
|
|
1981
2033
|
json(res, { generated_at: new Date().toISOString(), entries });
|
|
1982
2034
|
return true;
|
|
1983
2035
|
}
|
|
1984
2036
|
|
|
2037
|
+
// --- project-usage-detail ---
|
|
2038
|
+
// Per-project drill-down for the Project Usage modal: daily series,
|
|
2039
|
+
// per-source breakdown, and aggregate stats. Everything comes from the
|
|
2040
|
+
// local project bucket log — no external requests, no git access.
|
|
2041
|
+
if (p === "/functions/tokentracker-project-usage-detail") {
|
|
2042
|
+
const projectKey = url.searchParams.get("project_key") || "";
|
|
2043
|
+
if (!projectKey) {
|
|
2044
|
+
json(res, { error: "missing_project_key" }, 400);
|
|
2045
|
+
return true;
|
|
2046
|
+
}
|
|
2047
|
+
const { from, to, timeZoneContext, dayInRange, projectRows } =
|
|
2048
|
+
readProjectUsageContext(qp, url);
|
|
2049
|
+
|
|
2050
|
+
const emptyMeasures = () => ({
|
|
2051
|
+
total_tokens: 0,
|
|
2052
|
+
billable_total_tokens: 0,
|
|
2053
|
+
input_tokens: 0,
|
|
2054
|
+
output_tokens: 0,
|
|
2055
|
+
cached_input_tokens: 0,
|
|
2056
|
+
cache_creation_input_tokens: 0,
|
|
2057
|
+
reasoning_output_tokens: 0,
|
|
2058
|
+
conversation_count: 0,
|
|
2059
|
+
});
|
|
2060
|
+
const addMeasures = (acc, row) => {
|
|
2061
|
+
acc.total_tokens += Number(row.total_tokens || 0);
|
|
2062
|
+
acc.billable_total_tokens += Number(
|
|
2063
|
+
row.billable_total_tokens ?? row.total_tokens ?? 0,
|
|
2064
|
+
);
|
|
2065
|
+
acc.input_tokens += Number(row.input_tokens || 0);
|
|
2066
|
+
acc.output_tokens += Number(row.output_tokens || 0);
|
|
2067
|
+
acc.cached_input_tokens += Number(row.cached_input_tokens || 0);
|
|
2068
|
+
acc.cache_creation_input_tokens += Number(row.cache_creation_input_tokens || 0);
|
|
2069
|
+
acc.reasoning_output_tokens += Number(row.reasoning_output_tokens || 0);
|
|
2070
|
+
acc.conversation_count += Number(row.conversation_count || 0);
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
let projectRef = "";
|
|
2074
|
+
const totals = emptyMeasures();
|
|
2075
|
+
const byDay = new Map();
|
|
2076
|
+
const bySource = new Map();
|
|
2077
|
+
let rangeTotalTokens = 0;
|
|
2078
|
+
|
|
2079
|
+
for (const row of projectRows) {
|
|
2080
|
+
const day = rowDayKey(row, timeZoneContext);
|
|
2081
|
+
if (!dayInRange(day)) continue;
|
|
2082
|
+
rangeTotalTokens += Number(row.total_tokens || 0);
|
|
2083
|
+
if (row.project_key !== projectKey) continue;
|
|
2084
|
+
if (!projectRef && row.project_ref) projectRef = row.project_ref;
|
|
2085
|
+
addMeasures(totals, row);
|
|
2086
|
+
|
|
2087
|
+
const src = row.source || "unknown";
|
|
2088
|
+
if (day) {
|
|
2089
|
+
// `models` keyed by source (project rows carry no model column) —
|
|
2090
|
+
// it feeds TrendMonitor's stacked-segment slot so the modal chart
|
|
2091
|
+
// colors by provider exactly like the dashboard's Usage Trend.
|
|
2092
|
+
if (!byDay.has(day)) byDay.set(day, { day, ...emptyMeasures(), models: {} });
|
|
2093
|
+
const dayAgg = byDay.get(day);
|
|
2094
|
+
addMeasures(dayAgg, row);
|
|
2095
|
+
dayAgg.models[src] = (dayAgg.models[src] || 0) + Number(row.total_tokens || 0);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
if (!bySource.has(src)) {
|
|
2099
|
+
bySource.set(src, {
|
|
2100
|
+
source: src,
|
|
2101
|
+
total_tokens: 0,
|
|
2102
|
+
conversation_count: 0,
|
|
2103
|
+
dayKeys: new Set(),
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
const srcAgg = bySource.get(src);
|
|
2107
|
+
srcAgg.total_tokens += Number(row.total_tokens || 0);
|
|
2108
|
+
srcAgg.conversation_count += Number(row.conversation_count || 0);
|
|
2109
|
+
if (day) srcAgg.dayKeys.add(day);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
json(res, {
|
|
2113
|
+
generated_at: new Date().toISOString(),
|
|
2114
|
+
from,
|
|
2115
|
+
to,
|
|
2116
|
+
project_key: projectKey,
|
|
2117
|
+
project_ref: projectRef,
|
|
2118
|
+
totals,
|
|
2119
|
+
days_active: byDay.size,
|
|
2120
|
+
// All-project total over the same range so the client can render
|
|
2121
|
+
// "share of everything you tracked" without a second request.
|
|
2122
|
+
range_total_tokens: rangeTotalTokens,
|
|
2123
|
+
daily: Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day)),
|
|
2124
|
+
sources: Array.from(bySource.values())
|
|
2125
|
+
.sort((a, b) => b.total_tokens - a.total_tokens)
|
|
2126
|
+
.map(({ dayKeys, ...src }) => ({ ...src, days_active: dayKeys.size })),
|
|
2127
|
+
});
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
1985
2131
|
// --- user-status (stub) ---
|
|
1986
2132
|
if (p === "/functions/tokentracker-user-status") {
|
|
1987
2133
|
json(res, {
|