tokentracker-cli 0.5.71 → 0.5.72

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.
Files changed (21) hide show
  1. package/dashboard/dist/assets/{Card-5ebScQbM.js → Card-D_q1XGfK.js} +1 -1
  2. package/dashboard/dist/assets/{DashboardPage-B32RhUB1.js → DashboardPage-CuBSoNgI.js} +1 -1
  3. package/dashboard/dist/assets/{FadeIn-CHjFUAEe.js → FadeIn-ClpHby-T.js} +1 -1
  4. package/dashboard/dist/assets/{IpCheckPage-CrbKufId.js → IpCheckPage-B5TpHE6G.js} +1 -1
  5. package/dashboard/dist/assets/{LeaderboardPage-CVKBhGzY.js → LeaderboardPage-BOrtGeIf.js} +1 -1
  6. package/dashboard/dist/assets/LeaderboardProfilePage-C8xexZ08.js +1 -0
  7. package/dashboard/dist/assets/{LimitsPage-BwQ4isbM.js → LimitsPage-CERWQyIU.js} +1 -1
  8. package/dashboard/dist/assets/{SettingsPage-Y3Znzs1i.js → SettingsPage-DUwt4x-H.js} +1 -1
  9. package/dashboard/dist/assets/{WidgetsPage-tNojCBaj.js → WidgetsPage-Cmr2tbD7.js} +1 -1
  10. package/dashboard/dist/assets/{download-CQASvwEL.js → download-M8e1PJC-.js} +1 -1
  11. package/dashboard/dist/assets/{leaderboard-columns-DFG1TWe9.js → leaderboard-columns-Dcg9r7R2.js} +1 -1
  12. package/dashboard/dist/assets/{main-Cv6YpYdZ.js → main-D1VdJk4V.js} +2 -2
  13. package/dashboard/dist/assets/{use-limits-display-prefs-BUrHqTkA.js → use-limits-display-prefs-CSj55sfK.js} +1 -1
  14. package/dashboard/dist/assets/{use-usage-limits-D-gv2i1R.js → use-usage-limits-BuWINUAm.js} +1 -1
  15. package/dashboard/dist/index.html +1 -1
  16. package/dashboard/dist/share.html +1 -1
  17. package/package.json +1 -1
  18. package/src/commands/sync.js +26 -1
  19. package/src/lib/local-api.js +159 -56
  20. package/src/lib/rollout.js +45 -4
  21. package/dashboard/dist/assets/LeaderboardProfilePage-CbpzxvLA.js +0 -1
@@ -116,30 +116,90 @@ function resolveQueuePath() {
116
116
  return path.join(home, ".tokentracker", "tracker", "queue.jsonl");
117
117
  }
118
118
 
119
- function readQueueData(queuePath) {
119
+ function readProjectQueueData(projectQueuePath) {
120
+ let raw;
120
121
  try {
121
- const raw = fs.readFileSync(queuePath, "utf8");
122
- const lines = raw.split("\n").filter((l) => l.trim());
123
- const parsed = lines.map((l) => JSON.parse(l));
124
- // Deduplicate: each sync appends cumulative totals per bucket, so for
125
- // each (source, model, hour_start) keep only the latest (last) entry.
126
- const seen = new Map();
127
- for (const row of parsed) {
128
- const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
122
+ raw = fs.readFileSync(projectQueuePath, "utf8");
123
+ } catch (e) {
124
+ if (e?.code !== "ENOENT") {
125
+ console.error("[LocalAPI] readProjectQueueData: failed to read:", e?.message || e);
126
+ }
127
+ return [];
128
+ }
129
+ const lines = raw.split("\n").filter((l) => l.trim());
130
+ const seen = new Map();
131
+ for (const line of lines) {
132
+ try {
133
+ const row = JSON.parse(line);
134
+ const key = `${row.project_key || ""}|${row.source || ""}|${row.hour_start || ""}`;
129
135
  seen.set(key, row);
136
+ } catch {
137
+ // skip malformed
138
+ }
139
+ }
140
+ return Array.from(seen.values());
141
+ }
142
+
143
+ function readQueueData(queuePath) {
144
+ let raw;
145
+ try {
146
+ raw = fs.readFileSync(queuePath, "utf8");
147
+ } catch (e) {
148
+ // ENOENT is legitimate (never synced yet); anything else is a signal we
149
+ // don't want to hide behind an empty array forever — the dashboard would
150
+ // otherwise render "0 tokens" with no clue the queue was unreadable.
151
+ if (e?.code !== "ENOENT") {
152
+ console.error("[LocalAPI] readQueueData: failed to read queue:", e?.message || e);
130
153
  }
131
- return Array.from(seen.values());
132
- } catch (_e) {
133
154
  return [];
134
155
  }
156
+ const lines = raw.split("\n").filter((l) => l.trim());
157
+ // Parse row-by-row so a single corrupted line (partial write, disk-full
158
+ // truncation, …) does not wipe out every other row with it.
159
+ const parsed = [];
160
+ let malformed = 0;
161
+ for (const line of lines) {
162
+ try {
163
+ parsed.push(JSON.parse(line));
164
+ } catch {
165
+ malformed += 1;
166
+ }
167
+ }
168
+ if (malformed > 0) {
169
+ console.error(
170
+ `[LocalAPI] readQueueData: skipped ${malformed}/${lines.length} malformed line(s) in ${queuePath}`,
171
+ );
172
+ }
173
+ // Deduplicate: each sync appends cumulative totals per bucket, so for
174
+ // each (source, model, hour_start) keep only the latest (last) entry.
175
+ const seen = new Map();
176
+ for (const row of parsed) {
177
+ const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
178
+ seen.set(key, row);
179
+ }
180
+ return Array.from(seen.values());
135
181
  }
136
182
 
137
- function aggregateByDay(rows) {
183
+ function rowDayKey(row, timeZoneContext) {
184
+ const hs = row.hour_start;
185
+ if (!hs) return "";
186
+ if (
187
+ timeZoneContext &&
188
+ (timeZoneContext.timeZone || Number.isFinite(timeZoneContext.offsetMinutes))
189
+ ) {
190
+ const parts = getZonedParts(new Date(hs), timeZoneContext);
191
+ const key = formatPartsDayKey(parts);
192
+ if (key) return key;
193
+ }
194
+ return hs.slice(0, 10);
195
+ }
196
+
197
+ function aggregateByDay(rows, timeZoneContext = null) {
138
198
  const byDay = new Map();
139
199
  for (const row of rows) {
140
- const hs = row.hour_start;
141
- if (!hs) continue;
142
- const day = hs.slice(0, 10);
200
+ if (!row.hour_start) continue;
201
+ const day = rowDayKey(row, timeZoneContext);
202
+ if (!day) continue;
143
203
  if (!byDay.has(day)) {
144
204
  byDay.set(day, {
145
205
  day,
@@ -674,8 +734,9 @@ function createLocalApiHandler({ queuePath }) {
674
734
  if (p === "/functions/tokentracker-usage-summary") {
675
735
  const from = url.searchParams.get("from") || "";
676
736
  const to = url.searchParams.get("to") || "";
737
+ const timeZoneContext = getTimeZoneContext(url);
677
738
  const rows = readQueueData(qp);
678
- const daily = aggregateByDay(rows).filter((d) => d.day >= from && d.day <= to);
739
+ const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
679
740
  const totals = daily.reduce(
680
741
  (acc, r) => {
681
742
  acc.total_tokens += r.total_tokens;
@@ -693,16 +754,19 @@ function createLocalApiHandler({ queuePath }) {
693
754
  );
694
755
  const totalCost = totals.total_cost_usd;
695
756
 
696
- const today = new Date();
697
- const todayStr = today.toISOString().slice(0, 10);
698
- const allDaily = aggregateByDay(rows);
757
+ const todayParts = getZonedParts(new Date(), timeZoneContext);
758
+ const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
759
+ const allDaily = aggregateByDay(rows, timeZoneContext);
699
760
 
761
+ const shiftDay = (dayStr, delta) => {
762
+ const d = new Date(`${dayStr}T00:00:00Z`);
763
+ d.setUTCDate(d.getUTCDate() + delta);
764
+ return d.toISOString().slice(0, 10);
765
+ };
700
766
  const collectDays = (n) => {
701
767
  const out = [];
702
768
  for (let i = n - 1; i >= 0; i--) {
703
- const d = new Date(today);
704
- d.setUTCDate(d.getUTCDate() - i);
705
- const ds = d.toISOString().slice(0, 10);
769
+ const ds = shiftDay(todayStr, -i);
706
770
  const dd = allDaily.find((x) => x.day === ds);
707
771
  if (dd) out.push(dd);
708
772
  }
@@ -719,17 +783,15 @@ function createLocalApiHandler({ queuePath }) {
719
783
  const l30 = collectDays(30);
720
784
  const l7t = sumDays(l7);
721
785
  const l30t = sumDays(l30);
722
- const l7from = new Date(today);
723
- l7from.setUTCDate(l7from.getUTCDate() - 6);
724
- const l30from = new Date(today);
725
- l30from.setUTCDate(l30from.getUTCDate() - 29);
786
+ const l7fromStr = shiftDay(todayStr, -6);
787
+ const l30fromStr = shiftDay(todayStr, -29);
726
788
 
727
789
  json(res, {
728
790
  from, to, days: daily.length,
729
791
  totals: { ...totals, total_cost_usd: totalCost.toFixed(6) },
730
792
  rolling: {
731
- last_7d: { from: l7from.toISOString().slice(0, 10), to: todayStr, active_days: l7.length, totals: l7t },
732
- last_30d: { from: l30from.toISOString().slice(0, 10), to: todayStr, active_days: l30.length, totals: l30t, avg_per_active_day: l30.length > 0 ? Math.round(l30t.billable_total_tokens / l30.length) : 0 },
793
+ last_7d: { from: l7fromStr, to: todayStr, active_days: l7.length, totals: l7t },
794
+ last_30d: { from: l30fromStr, to: todayStr, active_days: l30.length, totals: l30t, avg_per_active_day: l30.length > 0 ? Math.round(l30t.billable_total_tokens / l30.length) : 0 },
733
795
  },
734
796
  });
735
797
  return true;
@@ -739,8 +801,9 @@ function createLocalApiHandler({ queuePath }) {
739
801
  if (p === "/functions/tokentracker-usage-daily") {
740
802
  const from = url.searchParams.get("from") || "";
741
803
  const to = url.searchParams.get("to") || "";
804
+ const timeZoneContext = getTimeZoneContext(url);
742
805
  const rows = readQueueData(qp);
743
- const daily = aggregateByDay(rows).filter((d) => d.day >= from && d.day <= to);
806
+ const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
744
807
  json(res, { from, to, data: daily });
745
808
  return true;
746
809
  }
@@ -748,10 +811,12 @@ function createLocalApiHandler({ queuePath }) {
748
811
  // --- usage-heatmap ---
749
812
  if (p === "/functions/tokentracker-usage-heatmap") {
750
813
  const weeks = parseInt(url.searchParams.get("weeks") || "52", 10);
814
+ const timeZoneContext = getTimeZoneContext(url);
751
815
  const rows = readQueueData(qp);
752
- const daily = aggregateByDay(rows);
753
- const today = new Date();
754
- const end = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
816
+ const daily = aggregateByDay(rows, timeZoneContext);
817
+ const todayParts = getZonedParts(new Date(), timeZoneContext);
818
+ const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
819
+ const end = new Date(`${todayStr}T00:00:00Z`);
755
820
  const start = new Date(end);
756
821
  start.setUTCDate(start.getUTCDate() - weeks * 7 + 1);
757
822
  const from = start.toISOString().slice(0, 10);
@@ -792,9 +857,10 @@ function createLocalApiHandler({ queuePath }) {
792
857
  if (p === "/functions/tokentracker-usage-model-breakdown") {
793
858
  const from = url.searchParams.get("from") || "";
794
859
  const to = url.searchParams.get("to") || "";
860
+ const timeZoneContext = getTimeZoneContext(url);
795
861
  const rows = readQueueData(qp).filter((r) => {
796
862
  if (!r.hour_start) return false;
797
- const d = r.hour_start.slice(0, 10);
863
+ const d = rowDayKey(r, timeZoneContext);
798
864
  return d >= from && d <= to;
799
865
  });
800
866
 
@@ -852,35 +918,71 @@ function createLocalApiHandler({ queuePath }) {
852
918
 
853
919
  // --- project-usage-summary ---
854
920
  if (p === "/functions/tokentracker-project-usage-summary") {
855
- const projectMap = new Map();
856
- scanCodexProjects(projectMap);
857
- scanClaudeProjects(projectMap);
858
-
859
- const rows = readQueueData(qp);
860
- const totalTokens = rows.reduce((s, r) => s + (r.total_tokens || 0), 0);
861
- const entries = [];
921
+ // Use the per-project bucket log that rollout.js emits — it already
922
+ // carries the actual tokens attributed to each (project_key, source,
923
+ // hour_start). Falling back to "session-file count × total tokens"
924
+ // (the old behavior) produced pure fiction: every short-and-hot
925
+ // project got the same weight as every long-and-cold one.
926
+ const projectQueuePath = path.join(
927
+ path.dirname(qp),
928
+ "project.queue.jsonl",
929
+ );
930
+ const projectRows = readProjectQueueData(projectQueuePath);
931
+
932
+ const byProject = new Map();
933
+ for (const row of projectRows) {
934
+ const key = row.project_key || "unknown";
935
+ if (!byProject.has(key)) {
936
+ byProject.set(key, {
937
+ project_key: key,
938
+ project_ref: row.project_ref || key,
939
+ total_tokens: 0,
940
+ billable_total_tokens: 0,
941
+ });
942
+ }
943
+ const agg = byProject.get(key);
944
+ agg.total_tokens += Number(row.total_tokens || 0);
945
+ agg.billable_total_tokens += Number(row.total_tokens || 0);
946
+ if (!agg.project_ref && row.project_ref) agg.project_ref = row.project_ref;
947
+ }
862
948
 
863
- if (projectMap.size === 0) {
949
+ // If no project-attributed rows exist yet (user hasn't synced project
950
+ // attribution, or never used a project-capable CLI), fall back to
951
+ // per-source aggregation over the main queue so the panel isn't
952
+ // totally empty. This path used to also exist for the non-empty case
953
+ // and produce wrong numbers; keep it only as the empty fallback.
954
+ let entries;
955
+ if (byProject.size === 0) {
956
+ const rows = readQueueData(qp);
864
957
  const bySrc = new Map();
865
958
  for (const row of rows) {
866
959
  const src = row.source || "unknown";
867
- if (!bySrc.has(src)) bySrc.set(src, { project_key: src, project_ref: `https://${src}.ai`, total_tokens: 0, billable_total_tokens: 0 });
960
+ if (!bySrc.has(src)) {
961
+ bySrc.set(src, {
962
+ project_key: src,
963
+ project_ref: `https://${src}.ai`,
964
+ total_tokens: 0,
965
+ billable_total_tokens: 0,
966
+ });
967
+ }
868
968
  bySrc.get(src).total_tokens += row.total_tokens || 0;
869
969
  bySrc.get(src).billable_total_tokens += row.total_tokens || 0;
870
970
  }
871
- entries.push(
872
- ...Array.from(bySrc.values())
873
- .sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
874
- .map((e) => ({ ...e, total_tokens: String(e.total_tokens), billable_total_tokens: String(e.billable_total_tokens) })),
875
- );
971
+ entries = Array.from(bySrc.values())
972
+ .sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
973
+ .map((e) => ({
974
+ ...e,
975
+ total_tokens: String(e.total_tokens),
976
+ billable_total_tokens: String(e.billable_total_tokens),
977
+ }));
876
978
  } else {
877
- const totalCount = Array.from(projectMap.values()).reduce((s, p) => s + p.count, 0);
878
- for (const [, proj] of projectMap) {
879
- const ratio = totalCount > 0 ? proj.count / totalCount : 1 / projectMap.size;
880
- const tokens = Math.floor(totalTokens * ratio);
881
- entries.push({ project_key: proj.project_key, project_ref: proj.project_ref, total_tokens: String(tokens), billable_total_tokens: String(tokens) });
882
- }
883
- entries.sort((a, b) => Number(b.billable_total_tokens) - Number(a.billable_total_tokens));
979
+ entries = Array.from(byProject.values())
980
+ .sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
981
+ .map((e) => ({
982
+ ...e,
983
+ total_tokens: String(e.total_tokens),
984
+ billable_total_tokens: String(e.billable_total_tokens),
985
+ }));
884
986
  }
885
987
 
886
988
  json(res, { generated_at: new Date().toISOString(), entries });
@@ -911,12 +1013,13 @@ function createLocalApiHandler({ queuePath }) {
911
1013
  if (p === "/functions/tokentracker-usage-monthly") {
912
1014
  const from = url.searchParams.get("from") || "";
913
1015
  const to = url.searchParams.get("to") || "";
1016
+ const timeZoneContext = getTimeZoneContext(url);
914
1017
  const rows = readQueueData(qp);
915
1018
  const byMonth = new Map();
916
1019
  for (const row of rows) {
917
1020
  if (!row.hour_start) continue;
918
- const day = row.hour_start.slice(0, 10);
919
- if (day < from || day > to) continue;
1021
+ const day = rowDayKey(row, timeZoneContext);
1022
+ if (!day || day < from || day > to) continue;
920
1023
  const month = day.slice(0, 7);
921
1024
  if (!byMonth.has(month))
922
1025
  byMonth.set(month, { month, total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0 });
@@ -676,12 +676,21 @@ async function parseOpenclawSessionFile({
676
676
 
677
677
  const model = normalizeModelInput(msg.model) || DEFAULT_MODEL;
678
678
 
679
+ // Per CLAUDE.md: cached_input_tokens = cache reads,
680
+ // cache_creation_input_tokens = cache writes. Also re-derive total_tokens
681
+ // as input + output + cache_creation + cache_read so cost math works
682
+ // even when the source's own totalTokens is stale or rounded.
683
+ const inputTok = Number(usage.input || 0);
684
+ const cacheReadTok = Number(usage.cacheRead || 0);
685
+ const cacheWriteTok = Number(usage.cacheWrite || 0);
686
+ const outputTok = Number(usage.output || 0);
679
687
  const delta = {
680
- input_tokens: Number(usage.input || 0),
681
- cached_input_tokens: Number((usage.cacheRead || 0) + (usage.cacheWrite || 0)),
682
- output_tokens: Number(usage.output || 0),
688
+ input_tokens: inputTok,
689
+ cached_input_tokens: cacheReadTok,
690
+ cache_creation_input_tokens: cacheWriteTok,
691
+ output_tokens: outputTok,
683
692
  reasoning_output_tokens: 0,
684
- total_tokens: Number(usage.totalTokens || 0),
693
+ total_tokens: inputTok + outputTok + cacheReadTok + cacheWriteTok,
685
694
  conversation_count: 1,
686
695
  };
687
696
 
@@ -2083,6 +2092,7 @@ function sameGeminiTotals(a, b) {
2083
2092
  return (
2084
2093
  a.input_tokens === b.input_tokens &&
2085
2094
  a.cached_input_tokens === b.cached_input_tokens &&
2095
+ a.cache_creation_input_tokens === b.cache_creation_input_tokens &&
2086
2096
  a.output_tokens === b.output_tokens &&
2087
2097
  a.reasoning_output_tokens === b.reasoning_output_tokens &&
2088
2098
  a.total_tokens === b.total_tokens
@@ -2097,12 +2107,20 @@ function diffGeminiTotals(current, previous) {
2097
2107
  const totalReset = (current.total_tokens || 0) < (previous.total_tokens || 0);
2098
2108
  if (totalReset) return current;
2099
2109
 
2110
+ // Must include cache_creation_input_tokens in both the equality check and
2111
+ // the delta — OpenCode routes through this diff and its cache.write number
2112
+ // would otherwise be permanently reported as zero. Gemini itself always
2113
+ // emits cache_creation=0 so the extra field is a no-op for Gemini.
2100
2114
  const delta = {
2101
2115
  input_tokens: Math.max(0, (current.input_tokens || 0) - (previous.input_tokens || 0)),
2102
2116
  cached_input_tokens: Math.max(
2103
2117
  0,
2104
2118
  (current.cached_input_tokens || 0) - (previous.cached_input_tokens || 0),
2105
2119
  ),
2120
+ cache_creation_input_tokens: Math.max(
2121
+ 0,
2122
+ (current.cache_creation_input_tokens || 0) - (previous.cache_creation_input_tokens || 0),
2123
+ ),
2106
2124
  output_tokens: Math.max(0, (current.output_tokens || 0) - (previous.output_tokens || 0)),
2107
2125
  reasoning_output_tokens: Math.max(
2108
2126
  0,
@@ -2651,6 +2669,16 @@ function readKiroDbTokens(dbPath, sinceId) {
2651
2669
  // The fallback file does not include per-row timestamps, so newly appended rows are
2652
2670
  // bucketed using the file mtime observed during this sync. We track a separate JSONL
2653
2671
  // cursor so it never shares state with the SQLite path.
2672
+ function countKiroJsonlLines(jsonlPath) {
2673
+ if (!jsonlPath || !fssync.existsSync(jsonlPath)) return 0;
2674
+ try {
2675
+ const raw = fssync.readFileSync(jsonlPath, "utf8");
2676
+ return raw.split("\n").filter((l) => l.trim()).length;
2677
+ } catch (_e) {
2678
+ return 0;
2679
+ }
2680
+ }
2681
+
2654
2682
  function readKiroJsonlTokens(jsonlPath, sinceLineIndex) {
2655
2683
  if (!jsonlPath || !fssync.existsSync(jsonlPath)) {
2656
2684
  return { rows: [], lineCount: 0, reset: false };
@@ -2795,6 +2823,14 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
2795
2823
  if (fssync.existsSync(resolvedDbPath)) {
2796
2824
  rows = readKiroDbTokens(resolvedDbPath, lastDbId);
2797
2825
  usingDb = true;
2826
+ // DB and JSONL are siblings for the same usage events. If the DB ever
2827
+ // disappears (corrupted / wiped) and we fall back to JSONL in a later
2828
+ // run, we must not re-read lines that the DB path already consumed.
2829
+ // Advance the JSONL line cursor to the current file tail.
2830
+ if (fssync.existsSync(resolvedJsonlPath)) {
2831
+ const tailLineCount = countKiroJsonlLines(resolvedJsonlPath);
2832
+ if (tailLineCount > nextJsonlLine) nextJsonlLine = tailLineCount;
2833
+ }
2798
2834
  } else if (fssync.existsSync(resolvedJsonlPath)) {
2799
2835
  const jsonlResult = readKiroJsonlTokens(resolvedJsonlPath, lastJsonlLine);
2800
2836
  rows = jsonlResult.rows;
@@ -3381,4 +3417,9 @@ module.exports = {
3381
3417
  resolveKimiWireFiles,
3382
3418
  resolveKimiDefaultModel,
3383
3419
  parseKimiIncremental,
3420
+ // Exposed for regression tests covering cache-token accounting.
3421
+ normalizeGeminiTokens,
3422
+ normalizeOpencodeTokens,
3423
+ sameGeminiTotals,
3424
+ diffGeminiTotals,
3384
3425
  };
@@ -1 +0,0 @@
1
- import{r as n,D as e,aA as D,C as H,aC as W,h as Y,j as Z,aE as G,aI as J,B as a,az as L,ax as R,a9 as E,aJ as q,aK as F}from"./main-Cv6YpYdZ.js";import{b as B,d as Q,l as V,f as X,e as ee,L as re,g as ae}from"./leaderboard-columns-DFG1TWe9.js";const y=18;function O(){return e.jsxs("svg",{"aria-hidden":!0,width:y,height:y,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("circle",{cx:"12",cy:"12",r:"4"}),e.jsx("path",{d:"M12 2v2"}),e.jsx("path",{d:"M12 20v2"}),e.jsx("path",{d:"m4.93 4.93 1.41 1.41"}),e.jsx("path",{d:"m17.66 17.66 1.41 1.41"}),e.jsx("path",{d:"M2 12h2"}),e.jsx("path",{d:"M20 12h2"}),e.jsx("path",{d:"m6.34 17.66-1.41 1.41"}),e.jsx("path",{d:"m19.07 4.93-1.41 1.41"})]})}function P(){return e.jsx("svg",{"aria-hidden":!0,width:y,height:y,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"})})}function te(){return e.jsxs("svg",{"aria-hidden":!0,width:y,height:y,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}),e.jsx("path",{d:"M8 21h8"}),e.jsx("path",{d:"M12 17v4"})]})}const oe=[{value:"light",label:"Light",Icon:O},{value:"dark",label:"Dark",Icon:P},{value:"system",label:"System",Icon:te}];function se(r){return r==="dark"?P:O}function ne({theme:r,resolvedTheme:s,onSetTheme:b,className:d="",direction:p="down",align:T="right"}){const[c,k]=n.useState(!1),h=n.useRef(null),i=n.useCallback(()=>k(!1),[]);n.useEffect(()=>{if(!c)return;const t=l=>{h.current&&!h.current.contains(l.target)&&i()};return document.addEventListener("mousedown",t),()=>document.removeEventListener("mousedown",t)},[c,i]),n.useEffect(()=>{if(!c)return;const t=l=>{l.key==="Escape"&&i()};return document.addEventListener("keydown",t),()=>document.removeEventListener("keydown",t)},[c,i]);const x=se(s);return e.jsxs("div",{ref:h,className:`relative ${d}`,children:[e.jsx("button",{type:"button","aria-label":"Theme","aria-expanded":c,onClick:()=>k(t=>!t),className:"flex items-center justify-center w-9 h-9 rounded-lg text-oai-gray-600 dark:text-oai-gray-400 hover:bg-oai-gray-100 dark:hover:bg-oai-gray-800 hover:text-oai-black dark:hover:text-white transition-colors",children:e.jsx(x,{})}),c&&e.jsx("div",{className:`absolute z-50 min-w-[140px] py-1 rounded-lg border border-oai-gray-200 dark:border-oai-gray-800 bg-white dark:bg-oai-gray-900 shadow-lg ${p==="up"?"bottom-full mb-1":"top-full mt-1"} ${T==="left"?"left-0":"right-0"}`,children:oe.map(({value:t,label:l,Icon:j})=>{const m=r===t;return e.jsxs("button",{type:"button",onClick:()=>{b(t),i()},className:`flex w-full items-center gap-2.5 px-3 py-2 text-sm transition-colors ${m?"text-oai-black dark:text-white bg-oai-gray-100 dark:bg-oai-gray-800":"text-oai-gray-600 dark:text-oai-gray-400 hover:bg-oai-gray-50 dark:hover:bg-oai-gray-800/60 hover:text-oai-black dark:hover:text-white"}`,children:[e.jsx(j,{}),e.jsx("span",{children:l})]},t)})})]})}function ie(r){if(!r)return a("shared.error.prefix",{error:a("leaderboard.error.unknown")});const s=r?.message||String(r),b=String(s||"").trim()||a("leaderboard.error.unknown");return a("shared.error.prefix",{error:b})}function le(r){return typeof r!="string"?"":r.trim()}function de(r){if(typeof r!="string")return null;const s=r.trim().toLowerCase();return s==="week"||s==="month"||s==="total"?s:null}function me({auth:r,signedIn:s,sessionSoftExpired:b,userId:d}){const p=D(),{theme:T,resolvedTheme:c,setTheme:k}=H(),h=n.useMemo(()=>W(),[]),i=Y(),x=s&&!b,t=n.useMemo(()=>x&&(typeof r=="function"||typeof r=="string"||r&&typeof r=="object")?r:null,[r,x]),l=x?t:null,j=x&&Z(l),m=n.useMemo(()=>{const o=new URLSearchParams(p?.search||"");return de(o.get("period"))||"week"},[p?.search]),_=p?.search||"",[v,w]=n.useState(()=>({loading:!1,error:null,data:null}));n.useEffect(()=>{if(!h&&!i||!d||!i&&(!x||!j))return;let o=!0;return w(f=>({...f,loading:!0,error:null})),(async()=>{const f=await G(l);if(!o)return;if(!f){w({loading:!1,error:null,data:null});return}const $=await J({accessToken:f,userId:d,period:m});o&&w({loading:!1,error:null,data:$})})().catch(f=>{o&&w({loading:!1,error:ie(f),data:null})}),()=>{o=!1}},[x,j,h,l,i,m,d]);const N=v.data,C=N?.from||null,A=N?.to||null,M=N?.generated_at||null,g=N?.entry||null,S=le(g?.display_name)||a("leaderboard.anon_label"),z=a("leaderboard.period.week"),K=a("leaderboard.period.month"),U=a("leaderboard.period.total"),I=m==="month"?K:m==="total"?U:z;let u=null;return d?v.loading?u=e.jsx("div",{className:"px-6 py-12 text-center",children:e.jsx("p",{className:"text-sm text-oai-gray-500 dark:text-oai-gray-400",children:a("leaderboard.loading")})}):v.error?u=e.jsx("div",{className:"px-6 py-12 text-center",children:e.jsx("p",{className:"text-sm text-red-500 dark:text-red-400",children:v.error})}):g?u=e.jsx("div",{className:"w-full overflow-x-auto",children:e.jsxs("table",{className:"min-w-max w-full text-left text-sm",children:[e.jsx("thead",{className:"border-b border-oai-gray-200 dark:border-oai-gray-800",children:e.jsxs("tr",{children:[e.jsx("th",{className:L(re,"font-medium text-oai-gray-500 dark:text-oai-gray-400"),children:a("leaderboard.column.rank")}),e.jsx("th",{className:L(ae,"font-medium text-oai-gray-500 dark:text-oai-gray-400 whitespace-nowrap"),children:a("leaderboard.column.total")}),B.map(o=>e.jsx("th",{className:"px-4 py-4 font-medium text-oai-gray-500 dark:text-oai-gray-400 whitespace-nowrap",children:e.jsx(Q,{iconSrc:o.icon,label:a(o.copyKey)})},o.key))]})}),e.jsx("tbody",{className:"divide-y divide-oai-gray-100 dark:divide-oai-gray-800/50",children:e.jsxs("tr",{className:"transition-colors hover:bg-oai-gray-50 dark:hover:bg-oai-gray-900/60",children:[e.jsx("td",{className:L(V(!1),"font-medium text-oai-gray-500 dark:text-oai-gray-400"),children:g?.rank??a("shared.placeholder.short")}),e.jsx("td",{className:L(X(),"text-oai-gray-700 dark:text-oai-gray-300"),children:R(g?.total_tokens)}),B.map(o=>e.jsx("td",{className:"px-4 py-4 text-oai-gray-500 dark:text-oai-gray-400 whitespace-nowrap",children:R(g?.[o.key])},o.key))]})})]})}):u=e.jsx("div",{className:"px-6 py-12 text-center",children:e.jsx("p",{className:"text-sm text-oai-gray-500 dark:text-oai-gray-400",children:a("leaderboard.empty")})}):u=e.jsx("div",{className:"px-6 py-12 text-center",children:e.jsx("p",{className:"text-sm text-oai-gray-500 dark:text-oai-gray-400",children:a("leaderboard.empty")})}),e.jsxs("div",{className:"flex flex-col min-h-screen bg-oai-white dark:bg-oai-gray-950 text-oai-black dark:text-oai-white font-oai antialiased transition-colors duration-200",children:[e.jsx("header",{className:"sticky top-0 z-50 bg-white/80 dark:bg-oai-gray-950/80 backdrop-blur-md border-b border-oai-gray-200 dark:border-oai-gray-900 transition-colors duration-200",children:e.jsxs("div",{className:"mx-auto flex h-14 max-w-6xl items-center justify-between px-4 sm:px-6",children:[e.jsxs("div",{className:"flex items-center gap-5",children:[e.jsxs(E,{to:"/",className:"flex items-center gap-3 no-underline outline-none rounded focus-visible:ring-2 focus-visible:ring-oai-brand-500 focus-visible:ring-offset-2 dark:ring-offset-oai-gray-950 transition-opacity hover:opacity-80",children:[e.jsx("img",{src:"/app-icon.png",alt:"",width:24,height:24,className:"rounded-md"}),e.jsx("span",{className:"text-sm font-semibold tracking-wide text-oai-black dark:text-white uppercase",children:"Token Tracker"})]}),e.jsx("div",{className:"hidden sm:block",children:e.jsx(q,{})})]}),e.jsxs("div",{className:"flex items-center gap-2 sm:gap-3",children:[e.jsx(E,{to:`/leaderboard${_}`,className:"no-underline inline-flex items-center justify-center h-9 px-5 text-sm font-medium rounded-full shadow-sm ring-1 ring-oai-gray-200 dark:ring-white/10 bg-oai-gray-900 dark:bg-white text-white dark:text-oai-gray-900 hover:bg-oai-gray-800 dark:hover:bg-oai-gray-100 transition-colors",children:a("leaderboard.profile.nav.back")}),e.jsx(ne,{theme:T,resolvedTheme:c,onSetTheme:k}),e.jsx(F,{})]})]})}),e.jsx("main",{className:"flex-1 py-12 sm:py-16",children:e.jsxs("div",{className:"mx-auto max-w-6xl px-4 sm:px-6",children:[e.jsxs("div",{className:"flex flex-col sm:flex-row sm:items-center gap-6 mb-10",children:[e.jsx(ee,{avatarUrl:g?.avatar_url,displayName:S,seed:typeof d=="string"?d:S,size:"lg",className:"shrink-0 ring-2 ring-oai-gray-200 dark:ring-oai-gray-800"}),e.jsxs("div",{className:"min-w-0",children:[e.jsx("h1",{className:"text-3xl sm:text-4xl font-semibold tracking-tight text-oai-black dark:text-white mb-3",children:S}),e.jsxs("p",{className:"text-oai-gray-500 dark:text-oai-gray-400 text-sm sm:text-base",children:[m==="total"?a("leaderboard.range.total"):C&&A?a("leaderboard.range",{period:I,from:C,to:A}):a("leaderboard.range_loading",{period:I}),M&&e.jsx("span",{className:"ml-2 pl-2 border-l border-oai-gray-200 dark:border-oai-gray-800 inline-block text-oai-gray-400 dark:text-oai-gray-500 text-xs",children:a("leaderboard.generated_at",{ts:M})})]})]})]}),e.jsx("div",{className:"rounded-xl border border-oai-gray-200 dark:border-oai-gray-800 overflow-hidden",children:u})]})}),e.jsx("footer",{className:"border-t border-oai-gray-200 dark:border-oai-gray-900 py-8 transition-colors duration-200",children:e.jsxs("div",{className:"mx-auto flex max-w-6xl items-center justify-between px-4 sm:px-6 text-sm text-oai-gray-400 dark:text-oai-gray-500",children:[e.jsx("p",{children:a("landing.v2.footer.line")}),e.jsx(E,{to:`/leaderboard${_}`,className:"text-oai-gray-400 dark:text-oai-gray-500 hover:text-oai-black dark:hover:text-white transition-colors",children:a("leaderboard.profile.nav.back")})]})})]})}export{me as LeaderboardProfilePage};