@oh-my-pi/omp-stats 18.2.0 → 18.2.2
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/CHANGELOG.md +11 -0
- package/dist/client/index.css +1 -1
- package/dist/client/styles.css +20 -0
- package/dist/types/db.d.ts +9 -1
- package/dist/types/parser.d.ts +18 -17
- package/dist/types/sync-worker.d.ts +4 -2
- package/package.json +4 -4
- package/src/aggregator.ts +51 -30
- package/src/client/styles.css +30 -0
- package/src/db.ts +98 -12
- package/src/parser.ts +137 -31
- package/src/port-conflict.ts +18 -7
- package/src/sync-worker.ts +11 -4
- package/src/trace.ts +49 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.1] - 2026-09-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed the sessions API returning an encoded storage key instead of the session working directory ([#12078](https://github.com/can1357/oh-my-pi/pull/12078) by [@Dante-dan](https://github.com/Dante-dan)).
|
|
10
|
+
- Fixed Traces search inputs and checkbox using unthemed browser defaults, and iOS viewport zoom on search focus.
|
|
11
|
+
- Fixed garbage tool names from provider-side parse failures (e.g. a gateway returning the model's whole invocation text as the tool name) polluting the tools dashboard's per-tool rows and filter dropdown; such names now collapse to their leading identifier, and existing databases re-ingest cleaned on next sync.
|
|
12
|
+
- Reduced repeat stats-sync reads to appended transcript data while preserving service-tier accounting across restarts.
|
|
13
|
+
- Rebuilt stats for replaced or truncated session files instead of retaining stale totals.
|
|
14
|
+
- Reclaimed lingering stats dashboards from older releases using their versioned HTTP identity when process command lines are unavailable, so upgrades no longer leave `omp stats` blocked by an opaque Bun listener.
|
|
15
|
+
|
|
5
16
|
## [18.1.17] - 2026-09-10
|
|
6
17
|
|
|
7
18
|
### Fixed
|
package/dist/client/index.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer theme,base,components,utilities;@layer theme{@theme default{--font-sans: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; --font-serif: ui-serif,Georgia,Cambria,"Times New Roman",Times,serif; --font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; --color-red-50: oklch(97.1% .013 17.38); --color-red-100: oklch(93.6% .032 17.717); --color-red-200: oklch(88.5% .062 18.334); --color-red-300: oklch(80.8% .114 19.571); --color-red-400: oklch(70.4% .191 22.216); --color-red-500: oklch(63.7% .237 25.331); --color-red-600: oklch(57.7% .245 27.325); --color-red-700: oklch(50.5% .213 27.518); --color-red-800: oklch(44.4% .177 26.899); --color-red-900: oklch(39.6% .141 25.723); --color-red-950: oklch(25.8% .092 26.042); --color-orange-50: oklch(98% .016 73.684); --color-orange-100: oklch(95.4% .038 75.164); --color-orange-200: oklch(90.1% .076 70.697); --color-orange-300: oklch(83.7% .128 66.29); --color-orange-400: oklch(75% .183 55.934); --color-orange-500: oklch(70.5% .213 47.604); --color-orange-600: oklch(64.6% .222 41.116); --color-orange-700: oklch(55.3% .195 38.402); --color-orange-800: oklch(47% .157 37.304); --color-orange-900: oklch(40.8% .123 38.172); --color-orange-950: oklch(26.6% .079 36.259); --color-amber-50: oklch(98.7% .022 95.277); --color-amber-100: oklch(96.2% .059 95.617); --color-amber-200: oklch(92.4% .12 95.746); --color-amber-300: oklch(87.9% .169 91.605); --color-amber-400: oklch(82.8% .189 84.429); --color-amber-500: oklch(76.9% .188 70.08); --color-amber-600: oklch(66.6% .179 58.318); --color-amber-700: oklch(55.5% .163 48.998); --color-amber-800: oklch(47.3% .137 46.201); --color-amber-900: oklch(41.4% .112 45.904); --color-amber-950: oklch(27.9% .077 45.635); --color-yellow-50: oklch(98.7% .026 102.212); --color-yellow-100: oklch(97.3% .071 103.193); --color-yellow-200: oklch(94.5% .129 101.54); --color-yellow-300: oklch(90.5% .182 98.111); --color-yellow-400: oklch(85.2% .199 91.936); --color-yellow-500: oklch(79.5% .184 86.047); --color-yellow-600: oklch(68.1% .162 75.834); --color-yellow-700: oklch(55.4% .135 66.442); --color-yellow-800: oklch(47.6% .114 61.907); --color-yellow-900: oklch(42.1% .095 57.708); --color-yellow-950: oklch(28.6% .066 53.813); --color-lime-50: oklch(98.6% .031 120.757); --color-lime-100: oklch(96.7% .067 122.328); --color-lime-200: oklch(93.8% .127 124.321); --color-lime-300: oklch(89.7% .196 126.665); --color-lime-400: oklch(84.1% .238 128.85); --color-lime-500: oklch(76.8% .233 130.85); --color-lime-600: oklch(64.8% .2 131.684); --color-lime-700: oklch(53.2% .157 131.589); --color-lime-800: oklch(45.3% .124 130.933); --color-lime-900: oklch(40.5% .101 131.063); --color-lime-950: oklch(27.4% .072 132.109); --color-green-50: oklch(98.2% .018 155.826); --color-green-100: oklch(96.2% .044 156.743); --color-green-200: oklch(92.5% .084 155.995); --color-green-300: oklch(87.1% .15 154.449); --color-green-400: oklch(79.2% .209 151.711); --color-green-500: oklch(72.3% .219 149.579); --color-green-600: oklch(62.7% .194 149.214); --color-green-700: oklch(52.7% .154 150.069); --color-green-800: oklch(44.8% .119 151.328); --color-green-900: oklch(39.3% .095 152.535); --color-green-950: oklch(26.6% .065 152.934); --color-emerald-50: oklch(97.9% .021 166.113); --color-emerald-100: oklch(95% .052 163.051); --color-emerald-200: oklch(90.5% .093 164.15); --color-emerald-300: oklch(84.5% .143 164.978); --color-emerald-400: oklch(76.5% .177 163.223); --color-emerald-500: oklch(69.6% .17 162.48); --color-emerald-600: oklch(59.6% .145 163.225); --color-emerald-700: oklch(50.8% .118 165.612); --color-emerald-800: oklch(43.2% .095 166.913); --color-emerald-900: oklch(37.8% .077 168.94); --color-emerald-950: oklch(26.2% .051 172.552); --color-teal-50: oklch(98.4% .014 180.72); --color-teal-100: oklch(95.3% .051 180.801); --color-teal-200: oklch(91% .096 180.426); --color-teal-300: oklch(85.5% .138 181.071); --color-teal-400: oklch(77.7% .152 181.912); --color-teal-500: oklch(70.4% .14 182.503); --color-teal-600: oklch(60% .118 184.704); --color-teal-700: oklch(51.1% .096 186.391); --color-teal-800: oklch(43.7% .078 188.216); --color-teal-900: oklch(38.6% .063 188.416); --color-teal-950: oklch(27.7% .046 192.524); --color-cyan-50: oklch(98.4% .019 200.873); --color-cyan-100: oklch(95.6% .045 203.388); --color-cyan-200: oklch(91.7% .08 205.041); --color-cyan-300: oklch(86.5% .127 207.078); --color-cyan-400: oklch(78.9% .154 211.53); --color-cyan-500: oklch(71.5% .143 215.221); --color-cyan-600: oklch(60.9% .126 221.723); --color-cyan-700: oklch(52% .105 223.128); --color-cyan-800: oklch(45% .085 224.283); --color-cyan-900: oklch(39.8% .07 227.392); --color-cyan-950: oklch(30.2% .056 229.695); --color-sky-50: oklch(97.7% .013 236.62); --color-sky-100: oklch(95.1% .026 236.824); --color-sky-200: oklch(90.1% .058 230.902); --color-sky-300: oklch(82.8% .111 230.318); --color-sky-400: oklch(74.6% .16 232.661); --color-sky-500: oklch(68.5% .169 237.323); --color-sky-600: oklch(58.8% .158 241.966); --color-sky-700: oklch(50% .134 242.749); --color-sky-800: oklch(44.3% .11 240.79); --color-sky-900: oklch(39.1% .09 240.876); --color-sky-950: oklch(29.3% .066 243.157); --color-blue-50: oklch(97% .014 254.604); --color-blue-100: oklch(93.2% .032 255.585); --color-blue-200: oklch(88.2% .059 254.128); --color-blue-300: oklch(80.9% .105 251.813); --color-blue-400: oklch(70.7% .165 254.624); --color-blue-500: oklch(62.3% .214 259.815); --color-blue-600: oklch(54.6% .245 262.881); --color-blue-700: oklch(48.8% .243 264.376); --color-blue-800: oklch(42.4% .199 265.638); --color-blue-900: oklch(37.9% .146 265.522); --color-blue-950: oklch(28.2% .091 267.935); --color-indigo-50: oklch(96.2% .018 272.314); --color-indigo-100: oklch(93% .034 272.788); --color-indigo-200: oklch(87% .065 274.039); --color-indigo-300: oklch(78.5% .115 274.713); --color-indigo-400: oklch(67.3% .182 276.935); --color-indigo-500: oklch(58.5% .233 277.117); --color-indigo-600: oklch(51.1% .262 276.966); --color-indigo-700: oklch(45.7% .24 277.023); --color-indigo-800: oklch(39.8% .195 277.366); --color-indigo-900: oklch(35.9% .144 278.697); --color-indigo-950: oklch(25.7% .09 281.288); --color-violet-50: oklch(96.9% .016 293.756); --color-violet-100: oklch(94.3% .029 294.588); --color-violet-200: oklch(89.4% .057 293.283); --color-violet-300: oklch(81.1% .111 293.571); --color-violet-400: oklch(70.2% .183 293.541); --color-violet-500: oklch(60.6% .25 292.717); --color-violet-600: oklch(54.1% .281 293.009); --color-violet-700: oklch(49.1% .27 292.581); --color-violet-800: oklch(43.2% .232 292.759); --color-violet-900: oklch(38% .189 293.745); --color-violet-950: oklch(28.3% .141 291.089); --color-purple-50: oklch(97.7% .014 308.299); --color-purple-100: oklch(94.6% .033 307.174); --color-purple-200: oklch(90.2% .063 306.703); --color-purple-300: oklch(82.7% .119 306.383); --color-purple-400: oklch(71.4% .203 305.504); --color-purple-500: oklch(62.7% .265 303.9); --color-purple-600: oklch(55.8% .288 302.321); --color-purple-700: oklch(49.6% .265 301.924); --color-purple-800: oklch(43.8% .218 303.724); --color-purple-900: oklch(38.1% .176 304.987); --color-purple-950: oklch(29.1% .149 302.717); --color-fuchsia-50: oklch(97.7% .017 320.058); --color-fuchsia-100: oklch(95.2% .037 318.852); --color-fuchsia-200: oklch(90.3% .076 319.62); --color-fuchsia-300: oklch(83.3% .145 321.434); --color-fuchsia-400: oklch(74% .238 322.16); --color-fuchsia-500: oklch(66.7% .295 322.15); --color-fuchsia-600: oklch(59.1% .293 322.896); --color-fuchsia-700: oklch(51.8% .253 323.949); --color-fuchsia-800: oklch(45.2% .211 324.591); --color-fuchsia-900: oklch(40.1% .17 325.612); --color-fuchsia-950: oklch(29.3% .136 325.661); --color-pink-50: oklch(97.1% .014 343.198); --color-pink-100: oklch(94.8% .028 342.258); --color-pink-200: oklch(89.9% .061 343.231); --color-pink-300: oklch(82.3% .12 346.018); --color-pink-400: oklch(71.8% .202 349.761); --color-pink-500: oklch(65.6% .241 354.308); --color-pink-600: oklch(59.2% .249 .584); --color-pink-700: oklch(52.5% .223 3.958); --color-pink-800: oklch(45.9% .187 3.815); --color-pink-900: oklch(40.8% .153 2.432); --color-pink-950: oklch(28.4% .109 3.907); --color-rose-50: oklch(96.9% .015 12.422); --color-rose-100: oklch(94.1% .03 12.58); --color-rose-200: oklch(89.2% .058 10.001); --color-rose-300: oklch(81% .117 11.638); --color-rose-400: oklch(71.2% .194 13.428); --color-rose-500: oklch(64.5% .246 16.439); --color-rose-600: oklch(58.6% .253 17.585); --color-rose-700: oklch(51.4% .222 16.935); --color-rose-800: oklch(45.5% .188 13.697); --color-rose-900: oklch(41% .159 10.272); --color-rose-950: oklch(27.1% .105 12.094); --color-slate-50: oklch(98.4% .003 247.858); --color-slate-100: oklch(96.8% .007 247.896); --color-slate-200: oklch(92.9% .013 255.508); --color-slate-300: oklch(86.9% .022 252.894); --color-slate-400: oklch(70.4% .04 256.788); --color-slate-500: oklch(55.4% .046 257.417); --color-slate-600: oklch(44.6% .043 257.281); --color-slate-700: oklch(37.2% .044 257.287); --color-slate-800: oklch(27.9% .041 260.031); --color-slate-900: oklch(20.8% .042 265.755); --color-slate-950: oklch(12.9% .042 264.695); --color-gray-50: oklch(98.5% .002 247.839); --color-gray-100: oklch(96.7% .003 264.542); --color-gray-200: oklch(92.8% .006 264.531); --color-gray-300: oklch(87.2% .01 258.338); --color-gray-400: oklch(70.7% .022 261.325); --color-gray-500: oklch(55.1% .027 264.364); --color-gray-600: oklch(44.6% .03 256.802); --color-gray-700: oklch(37.3% .034 259.733); --color-gray-800: oklch(27.8% .033 256.848); --color-gray-900: oklch(21% .034 264.665); --color-gray-950: oklch(13% .028 261.692); --color-zinc-50: oklch(98.5% 0 none); --color-zinc-100: oklch(96.7% .001 286.375); --color-zinc-200: oklch(92% .004 286.32); --color-zinc-300: oklch(87.1% .006 286.286); --color-zinc-400: oklch(70.5% .015 286.067); --color-zinc-500: oklch(55.2% .016 285.938); --color-zinc-600: oklch(44.2% .017 285.786); --color-zinc-700: oklch(37% .013 285.805); --color-zinc-800: oklch(27.4% .006 286.033); --color-zinc-900: oklch(21% .006 285.885); --color-zinc-950: oklch(14.1% .005 285.823); --color-neutral-50: oklch(98.5% 0 none); --color-neutral-100: oklch(97% 0 none); --color-neutral-200: oklch(92.2% 0 none); --color-neutral-300: oklch(87% 0 none); --color-neutral-400: oklch(70.8% 0 none); --color-neutral-500: oklch(55.6% 0 none); --color-neutral-600: oklch(43.9% 0 none); --color-neutral-700: oklch(37.1% 0 none); --color-neutral-800: oklch(26.9% 0 none); --color-neutral-900: oklch(20.5% 0 none); --color-neutral-950: oklch(14.5% 0 none); --color-stone-50: oklch(98.5% .001 106.423); --color-stone-100: oklch(97% .001 106.424); --color-stone-200: oklch(92.3% .003 48.717); --color-stone-300: oklch(86.9% .005 56.366); --color-stone-400: oklch(70.9% .01 56.259); --color-stone-500: oklch(55.3% .013 58.071); --color-stone-600: oklch(44.4% .011 73.639); --color-stone-700: oklch(37.4% .01 67.558); --color-stone-800: oklch(26.8% .007 34.298); --color-stone-900: oklch(21.6% .006 56.043); --color-stone-950: oklch(14.7% .004 49.25); --color-mauve-50: oklch(98.5% 0 none); --color-mauve-100: oklch(96% .003 325.6); --color-mauve-200: oklch(92.2% .005 325.62); --color-mauve-300: oklch(86.5% .012 325.68); --color-mauve-400: oklch(71.1% .019 323.02); --color-mauve-500: oklch(54.2% .034 322.5); --color-mauve-600: oklch(43.5% .029 321.78); --color-mauve-700: oklch(36.4% .029 323.89); --color-mauve-800: oklch(26.3% .024 320.12); --color-mauve-900: oklch(21.2% .019 322.12); --color-mauve-950: oklch(14.5% .008 326); --color-olive-50: oklch(98.8% .003 106.5); --color-olive-100: oklch(96.6% .005 106.5); --color-olive-200: oklch(93% .007 106.5); --color-olive-300: oklch(88% .011 106.6); --color-olive-400: oklch(73.7% .021 106.9); --color-olive-500: oklch(58% .031 107.3); --color-olive-600: oklch(46.6% .025 107.3); --color-olive-700: oklch(39.4% .023 107.4); --color-olive-800: oklch(28.6% .016 107.4); --color-olive-900: oklch(22.8% .013 107.4); --color-olive-950: oklch(15.3% .006 107.1); --color-mist-50: oklch(98.7% .002 197.1); --color-mist-100: oklch(96.3% .002 197.1); --color-mist-200: oklch(92.5% .005 214.3); --color-mist-300: oklch(87.2% .007 219.6); --color-mist-400: oklch(72.3% .014 214.4); --color-mist-500: oklch(56% .021 213.5); --color-mist-600: oklch(45% .017 213.2); --color-mist-700: oklch(37.8% .015 216); --color-mist-800: oklch(27.5% .011 216.9); --color-mist-900: oklch(21.8% .008 223.9); --color-mist-950: oklch(14.8% .004 228.8); --color-taupe-50: oklch(98.6% .002 67.8); --color-taupe-100: oklch(96% .002 17.2); --color-taupe-200: oklch(92.2% .005 34.3); --color-taupe-300: oklch(86.8% .007 39.5); --color-taupe-400: oklch(71.4% .014 41.2); --color-taupe-500: oklch(54.7% .021 43.1); --color-taupe-600: oklch(43.8% .017 39.3); --color-taupe-700: oklch(36.7% .016 35.7); --color-taupe-800: oklch(26.8% .011 36.5); --color-taupe-900: oklch(21.4% .009 43.1); --color-taupe-950: oklch(14.7% .004 49.3); --color-black: #000; --color-white: #fff; --spacing: .25rem; --breakpoint-sm: 40rem; --breakpoint-md: 48rem; --breakpoint-lg: 64rem; --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; --container-3xs: 16rem; --container-2xs: 18rem; --container-xs: 20rem; --container-sm: 24rem; --container-md: 28rem; --container-lg: 32rem; --container-xl: 36rem; --container-2xl: 42rem; --container-3xl: 48rem; --container-4xl: 56rem; --container-5xl: 64rem; --container-6xl: 72rem; --container-7xl: 80rem; --text-xs: .75rem; --text-xs--line-height: calc(1/.75); --text-sm: .875rem; --text-sm--line-height: calc(1.25/.875); --text-base: 1rem; --text-base--line-height: calc(1.5/1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75/1.125); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75/1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2/1.5); --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25/1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5/2.25); --text-5xl: 3rem; --text-5xl--line-height: 1; --text-6xl: 3.75rem; --text-6xl--line-height: 1; --text-7xl: 4.5rem; --text-7xl--line-height: 1; --text-8xl: 6rem; --text-8xl--line-height: 1; --text-9xl: 8rem; --text-9xl--line-height: 1; --font-weight-thin: 100; --font-weight-extralight: 200; --font-weight-light: 300; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --font-weight-extrabold: 800; --font-weight-black: 900; --tracking-tighter: -.05em; --tracking-tight: -.025em; --tracking-normal: 0em; --tracking-wide: .025em; --tracking-wider: .05em; --tracking-widest: .1em; --leading-tight: 1.25; --leading-snug: 1.375; --leading-normal: 1.5; --leading-relaxed: 1.625; --leading-loose: 2; --radius-xs: .125rem; --radius-sm: .25rem; --radius-md: .375rem; --radius-lg: .5rem; --radius-xl: .75rem; --radius-2xl: 1rem; --radius-3xl: 1.5rem; --radius-4xl: 2rem; --shadow-2xs: 0 1px #0000000d; --shadow-xs: 0 1px 2px 0 #0000000d; --shadow-sm: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-md: 0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a; --shadow-lg: 0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a; --shadow-xl: 0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a; --shadow-2xl: 0 25px 50px -12px #00000040; --inset-shadow-2xs: inset 0 1px #0000000d; --inset-shadow-xs: inset 0 1px 1px #0000000d; --inset-shadow-sm: inset 0 2px 4px #0000000d; --drop-shadow-xs: 0 1px 1px #0000000d; --drop-shadow-sm: 0 1px 2px #00000026; --drop-shadow-md: 0 3px 3px #0000001f; --drop-shadow-lg: 0 4px 4px #00000026; --drop-shadow-xl: 0 9px 7px #0000001a; --drop-shadow-2xl: 0 25px 25px #00000026; --text-shadow-2xs: 0px 1px 0px #00000026; --text-shadow-xs: 0px 1px 1px #0003; --text-shadow-sm: 0px 1px 0px #00000013,0px 1px 1px #00000013,0px 2px 2px #00000013; --text-shadow-md: 0px 1px 1px #0000001a,0px 1px 2px #0000001a,0px 2px 4px #0000001a; --text-shadow-lg: 0px 1px 2px #0000001a,0px 3px 2px #0000001a,0px 4px 8px #0000001a; --ease-in: cubic-bezier(.4,0,1,1); --ease-out: cubic-bezier(0,0,.2,1); --ease-in-out: cubic-bezier(.4,0,.2,1); --animate-spin: spin 1s linear infinite; --animate-ping: ping 1s cubic-bezier(0,0,.2,1)infinite; --animate-pulse: pulse 2s cubic-bezier(.4,0,.6,1)infinite; --animate-bounce: bounce 1s infinite; @keyframes spin { to { transform: rotate(360deg); }}@keyframes ping { 75%,100% { transform: scale(2); opacity: 0; }}@keyframes pulse { 50% { opacity: .5; }}@keyframes bounce { 0%,100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(.8,0,1,1); }50% { transform: none; animation-timing-function: cubic-bezier(0,0,.2,1); }}--blur-xs: 4px; --blur-sm: 8px; --blur-md: 12px; --blur-lg: 16px; --blur-xl: 24px; --blur-2xl: 40px; --blur-3xl: 64px; --perspective-dramatic: 100px; --perspective-near: 300px; --perspective-normal: 500px; --perspective-midrange: 800px; --perspective-distant: 1200px; --aspect-video: 16/9; --default-transition-duration: .15s; --default-transition-timing-function: cubic-bezier(.4,0,.2,1); --default-font-family: --theme(--font-sans,initial); --default-font-feature-settings: --theme(--font-sans--font-feature-settings,initial); --default-font-variation-settings: --theme(--font-sans--font-variation-settings,initial); --default-mono-font-family: --theme(--font-mono,initial); --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings,initial); --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings,initial);}@theme default inline reference{--blur: 8px; --shadow: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-inner: inset 0 2px 4px 0 #0000000d; --drop-shadow: 0 1px 2px #0000001a,0 1px 1px #0000000f; --radius: .25rem; --max-width-prose: 65ch;}}@layer base{*,:after,:before{box-sizing:border-box;border:0 solid;margin:0;padding:0}::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::-webkit-file-upload-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:--theme(--default-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:--theme(--default-font-feature-settings,normal);font-variation-settings:--theme(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:--theme(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:--theme(--default-mono-font-feature-settings,normal);font-variation-settings:--theme(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{position:relative;vertical-align:baseline;font-size:75%;line-height:0}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::-webkit-file-upload-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:-webkit-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:-moz-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:-webkit-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:-moz-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::-webkit-file-upload-button{margin-inline-end:4px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports ( not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{text-align:inherit;min-height:1lh}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-year-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-month-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-day-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-hour-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-minute-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-second-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-millisecond-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-meridiem-field{padding-block-start:0;padding-block-end:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button{appearance:button}input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-file-upload-button{appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{@tailwind utilities;}:root{--buncss-light: ;--buncss-dark:initial;color-scheme:dark;--page:oklch(.16 .02 307);--surface:oklch(.19 .022 307);--surface-2:oklch(.235 .026 307);--border:oklch(1 0 0/9%);--border-strong:oklch(1 0 0/13%);--text:oklch(.92 .01 307);--muted:oklch(.71 .016 307);--dim:oklch(.53 .018 307);--accent:oklch(.674 .23 341);--accent-fg:oklch(.2 .07 341);--accent-muted:oklch(.674 .23 341/18%);--link:oklch(.817 .112 205);--success:oklch(.76 .14 150);--danger:oklch(.66 .19 25);--warning:oklch(.79 .14 85);--text-primary:var(--text);--text-secondary:var(--muted);--text-muted:var(--dim);--border-subtle:var(--border);--accent-red:var(--danger);--accent-green:var(--success);--accent-cyan:var(--link);--bg-hover:var(--surface-2);--bg-elevated:var(--surface-2);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--radius-sm:3px;--radius-md:6px;--radius-lg:8px}@media (prefers-color-scheme:light){:root:not([data-theme]){--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}}[data-theme=light]{--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}@layer base{*{box-sizing:border-box}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background:var(--page);color:var(--text);min-height:100vh;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}button:focus-visible{outline:2px solid var(--link);outline-offset:2px}a:focus-visible{outline:2px solid var(--link);outline-offset:2px}[tabindex]:focus-visible{outline:2px solid var(--link);outline-offset:2px}}.stats-app-container{display:flex;background:var(--page);color:var(--text);min-height:100vh}.stats-nav-rail{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;width:240px}.stats-desktop-nav{display:flex}@media (max-width:1023px){.stats-desktop-nav{display:none}}.stats-nav-rail-header{border-bottom:1px solid var(--border);display:flex;align-items: center;height:56px;padding:20px 24px}.stats-logo-container{display:flex;flex-direction:column}.stats-logo-text{letter-spacing:.1em;color:var(--text);font-size:14px;font-weight:700}.stats-logo-subtext{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-size:10px}.stats-nav-rail-menu{display:flex;flex-direction:column;flex:1;gap:4px;padding:16px 12px}.stats-nav-rail-item{display:flex;color:var(--muted);border-radius:var(--radius-md);cursor:pointer;text-align:left;background:0 0;border:none;align-items: center;gap:12px;padding:10px 16px;transition:all .15s;font-size:13px;font-weight:500}.stats-nav-rail-item:hover{color:var(--text);background:var(--surface-2)}.stats-nav-rail-item[data-active=true]{color:var(--accent);background:var(--accent-muted);position:relative}.stats-nav-rail-item[data-active=true]:before{content:"";position:absolute;background:var(--accent);border-radius:0 3px 3px 0;width:3px;height:18px;top:50%;left:0;transform:translateY(-50%)}.stats-nav-rail-item-icon{flex-shrink:0}.stats-nav-rail-footer{border-top:1px solid var(--border);padding:16px 24px}.stats-version-tag{color:var(--dim);font-size:11px}.stats-main-pane{display:flex;flex-direction:column;flex:1;min-width:0}.stats-top-bar{background:var(--surface);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;height:56px;padding:0 24px}@media (max-width:767px){.stats-top-bar{flex-wrap:wrap;gap:12px;height:auto;min-height:56px;padding:8px 16px}}.stats-top-bar-left{display:flex;align-items: center;gap:12px}.stats-mobile-menu-btn{display:none;color:var(--text);cursor:pointer;background:0 0;border:none;padding:4px}@media (max-width:1023px){.stats-mobile-menu-btn{display:inline-flex;justify-content:center;align-items: center}}.stats-page-title{color:var(--text);margin:0;font-size:16px;font-weight:600}.stats-top-bar-right{display:flex;align-items: center;gap:16px}@media (max-width:767px){.stats-top-bar-right{justify-content:space-between;gap:8px;width:100%}}.stats-top-bar-meta{display:flex;align-items: center}.stats-last-updated{color:var(--dim);font-variant-numeric:tabular-nums;font-size:11px}.stats-content-area{overflow-y:auto;flex:1}.stats-content-inner{display:flex;flex-direction:column;gap:24px;max-width:1560px;margin:0 auto;padding:24px}@media (max-width:767px){.stats-content-inner{gap:16px;padding:16px}}.stats-mobile-drawer-overlay{position:fixed;backdrop-filter:none;z-index:100;background:#0006;inset:0}.stats-mobile-drawer{position:absolute;background:var(--surface);border-right:1px solid var(--border);display:flex;z-index:101;animation:.2s ease-out slideIn;flex-direction:column;width:260px;top:0;bottom:0;left:0}@keyframes slideIn{0%{transform:translate(-100%)}to{transform:translate(0)}}.stats-mobile-drawer-header{display:flex;border-bottom:1px solid var(--border);justify-content:space-between;align-items: center;height:56px;padding:0 16px 0 24px}.stats-mobile-nav{display:flex;height:calc(100% - 56px)}.stats-button{display:inline-flex;border-radius:var(--radius-md);cursor:pointer;border:1px solid #0000;justify-content:center;align-items: center;gap:8px;padding:8px 14px;transition:all .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-button:active:not(:disabled){transform:translateY(1px)}.stats-button-primary{background:var(--accent);color:var(--accent-fg)}.stats-button-primary:hover:not(:disabled){opacity:.9}.stats-button-primary:disabled{opacity:.5;cursor:not-allowed}.stats-button-secondary{background:var(--surface-2);color:var(--text);border-color:var(--border)}.stats-button-secondary:hover:not(:disabled){background:var(--border)}.stats-button-secondary:disabled{opacity:.5;cursor:not-allowed}.stats-range-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:2px}.stats-range-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;background:0 0;border:none;padding:4px 10px;transition:all .1s;font-size:12px;font-weight:500}.stats-range-control-btn:hover{color:var(--text)}.stats-range-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-select{appearance:none;color:var(--text);background-color:var(--surface-2);background-image:var(--select-chevron);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;text-overflow:ellipsis;background-position:right 10px center;background-repeat:no-repeat;max-width:100%;padding:6px 28px 6px 12px;transition:border-color .15s,background-color .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-select:hover:not(:disabled){border-color:var(--border-strong);background-color:var(--bg-hover)}.stats-select:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-select:disabled{opacity:.5;cursor:not-allowed}@media (pointer:coarse){.stats-select{font-size:16px}}.stats-select option{background-color:var(--surface);color:var(--text)}@media (forced-colors:active){.stats-select{appearance:auto}}.stats-sync-container{display:flex;align-items: center;gap:12px}.stats-sync-status-msg{font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;font-size:11px}.stats-sync-status-msg[data-type=success]{color:var(--success)}.stats-sync-status-msg[data-type=error]{color:var(--danger)}.stats-sync-icon{flex-shrink:0}.stats-spin{animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.stats-panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column}.stats-panel-header{border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;gap:16px;padding:16px 20px}@media (max-width:480px){.stats-panel-header{flex-direction:column;align-items: flex-start;gap:8px}}.stats-panel-header-titles{display:flex;flex-direction:column}.stats-panel-title{color:var(--text);margin:0;font-size:14px;font-weight:600}.stats-panel-subtitle{color:var(--muted);margin:2px 0 0;font-size:12px}.stats-panel-actions{display:flex;align-items: center;gap:12px}.stats-panel-body{padding:20px}.stats-metric-cluster{display:flex;flex-direction:column;gap:16px}.stats-metric-primary-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:16px}@media (max-width:1023px){.stats-metric-primary-grid{grid-template-columns:repeat(2,1fr)}}@media (max-width:480px){.stats-metric-primary-grid{grid-template-columns:1fr}}.stats-metric-secondary-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:12px}@media (max-width:1279px){.stats-metric-secondary-grid{grid-template-columns:repeat(4,1fr)}}@media (max-width:767px){.stats-metric-secondary-grid{grid-template-columns:repeat(2,1fr)}}.stats-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:space-between;padding:16px}.stats-metric-card.primary{padding:20px 18px}.stats-metric-card.primary .stats-metric-label{color:var(--accent)}.stats-metric-card.secondary{padding:12px 14px}.stats-metric-label{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:8px;font-size:11px;font-weight:600}.stats-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:20px;font-weight:700}.stats-metric-card.secondary .stats-metric-value{font-size:15px;font-weight:600}.stats-table-wrapper{width:100%}.stats-table-container{overflow-x:auto;width:100%}.stats-table{border-collapse:collapse;font-variant-numeric:tabular-nums;width:100%;font-size:13px}.stats-table-th{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);border-bottom:2px solid var(--border);padding:12px 16px;font-size:11px;font-weight:600}.stats-table-td{border-bottom:1px solid var(--border);color:var(--text);padding:10px 16px}.stats-table-tr{background:0 0}.stats-table-tr-clickable{cursor:pointer;transition:background .12s,box-shadow .12s}.stats-table-tr-clickable:hover{background:var(--surface-2);box-shadow:inset 2px 0 0 var(--accent)}.stats-text-left{text-align:left}.stats-text-right{text-align:right}.stats-table-empty{text-align:center;color:var(--muted);border:1px dashed var(--border);border-radius:var(--radius-lg);padding:40px;font-size:13px}.stats-progress-bar-track{background:var(--surface-2);overflow:hidden;border-radius:999px}.stats-progress-bar-fill{border-radius:inherit;height:100%;transition:width .18s}.stats-progress-bar-fill[data-variant=link]{background:var(--link)}.stats-progress-bar-fill[data-variant=success]{background:var(--success)}.stats-table-mobile-only{display:none}.stats-table-desktop-only{overflow-x:auto;width:100%}@media (max-width:767px){.stats-table-desktop-only{display:none}.stats-table-mobile-only{display:block}}.stats-table-mobile-list{display:flex;flex-direction:column;gap:12px}.stats-table-mobile-card-wrapper{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:14px}.stats-status-pill{display:inline-flex;border-radius:100px;justify-content:center;align-items: center;padding:2px 8px;font-size:11px;font-weight:600;line-height:1.2}.stats-status-pill[data-variant=success]{background:color-mix(in srgb,var(--success)18%,transparent);color:var(--success)}.stats-status-pill[data-variant=danger]{background:color-mix(in srgb,var(--danger)18%,transparent);color:var(--danger)}.stats-status-pill[data-variant=warning]{background:color-mix(in srgb,var(--warning)18%,transparent);color:var(--warning)}.stats-status-pill[data-variant=info]{background:color-mix(in srgb,var(--link)18%,transparent);color:var(--link)}.stats-status-pill[data-variant=default]{background:var(--surface-2);color:var(--muted)}.stats-segmented-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);gap:2px;padding:2px}.stats-segmented-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:6px 12px;transition:all .1s;font-size:12px;font-weight:500}.stats-segmented-control-btn:hover{color:var(--text)}.stats-segmented-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-drawer-overlay{position:fixed;z-index:100;display:flex;background:#00000080;justify-content:flex-end;inset:0}.stats-drawer{background:var(--surface);border-left:1px solid var(--border);display:flex;animation:.25s cubic-bezier(.16,1,.3,1) drawerSlide;flex-direction:column;width:600px;max-width:100%;height:100%;box-shadow:-4px 0 24px #00000040}@keyframes drawerSlide{0%{transform:translate(100%)}to{transform:translate(0)}}.stats-drawer-header{border-bottom:1px solid var(--border);display:flex;background:var(--surface);flex-shrink:0;justify-content:space-between;align-items: center;height:56px;padding:0 24px}.stats-drawer-header-left{display:flex;align-items:baseline;gap:12px}.stats-drawer-title{color:var(--text);margin:0;font-size:15px;font-weight:600}.stats-drawer-id{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px}.stats-drawer-close-btn{color:var(--muted);cursor:pointer;display:inline-flex;border-radius:var(--radius-sm);background:0 0;border:none;justify-content:center;align-items: center;padding:4px;transition:all .1s}.stats-drawer-close-btn:hover{color:var(--text);background:var(--surface-2)}.stats-drawer-body{overflow-y:auto;flex:1;padding:24px}.stats-drawer-content{display:flex;flex-direction:column;gap:24px}.stats-drawer-status-card{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px}.stats-drawer-status-row{display:flex;justify-content:space-between;align-items: flex-start;gap:16px}.stats-drawer-model{color:var(--text);font-size:16px;font-weight:700}.stats-drawer-provider{color:var(--muted);margin-top:2px;font-size:12px}.stats-drawer-error-block{border-top:1px solid var(--border);margin-top:14px;padding-top:12px}.stats-drawer-error-label{text-transform:uppercase;letter-spacing:.05em;color:var(--danger);margin-bottom:4px;font-size:11px;font-weight:600}.stats-drawer-error-text{color:var(--text);white-space:pre-wrap;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px}.stats-drawer-metrics-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.stats-drawer-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:center;padding:14px}.stats-drawer-metric-label{display:flex;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);align-items: center;gap:6px;margin-bottom:6px;font-size:11px;font-weight:600}.stats-drawer-metric-icon{color:var(--dim)}.stats-drawer-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:16px;font-weight:700}.stats-drawer-metric-sub{color:var(--muted);font-variant-numeric:tabular-nums;margin-top:2px;font-size:11px}.stats-drawer-json-blocks{display:flex;flex-direction:column;gap:16px}.stats-json-block{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);overflow:hidden}.stats-json-block-header{background:var(--surface-2);border-bottom:1px solid var(--border);display:flex;cursor:pointer;user-select:none;justify-content:space-between;align-items: center;padding:10px 16px;font-size:12px;font-weight:600}.stats-json-block-header:hover{background:var(--border)}.stats-json-block-title{color:var(--text)}.stats-json-block-toggle-indicator{color:var(--muted)}.stats-json-block-content-wrapper{background:var(--page);overflow-x:auto;padding:16px}.stats-json-block-content{color:var(--text);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5}.stats-skeleton{background:var(--surface-2);position:relative;overflow:hidden;border-radius:var(--radius-sm)}.stats-skeleton[data-variant=text]{height:14px;margin-top:4px;margin-bottom:4px}.stats-skeleton[data-variant=circle]{border-radius:50%}.stats-skeleton:after{content:"";position:absolute;animation:1.5s infinite shimmer;background:linear-gradient(90deg,#0000 0%,#ffffff0d 50%,#0000 100%);inset:0;transform:translate(-100%)}@media (prefers-color-scheme:light){.stats-skeleton:after{background:linear-gradient(90deg,#0000 0%,#00000008 50%,#0000 100%)}}@keyframes shimmer{to{transform:translate(100%)}}@media (prefers-reduced-motion:reduce){.stats-skeleton:after{animation:none}}.stats-boundary-skeleton{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);padding:20px}.stats-empty-state,.stats-error-state{display:flex;text-align:center;background:var(--surface);border:1px dashed var(--border);border-radius:var(--radius-lg);flex-direction:column;justify-content:center;align-items: center;padding:48px 24px}.stats-empty-state-message{color:var(--muted);margin:0;font-size:13px}.stats-error-state-content{max-width:360px}.stats-error-state-title{color:var(--text);margin:0 0 8px;font-size:14px;font-weight:600}.stats-error-state-message{color:var(--danger);word-break:break-word;margin:0 0 16px;font-size:12px}.stats-error-state-btn{margin:0 auto}.stats-text-primary{color:var(--text)}.stats-text-secondary{color:var(--muted)}.stats-text-muted{color:var(--dim)}.stats-text-warning{color:var(--warning)}.stats-text-xs{font-size:12px}.stats-font-medium{font-weight:500}.stats-font-semibold{font-weight:600}*{scrollbar-width:thin;scrollbar-color:var(--border-strong)transparent}*::-webkit-scrollbar{width:10px;height:10px}*::-webkit-scrollbar-track{background:0 0}*::-webkit-scrollbar-thumb{background:var(--border-strong);background-clip:padding-box;border:2px solid #0000;border-radius:999px}*::-webkit-scrollbar-thumb:hover{background:var(--dim);background-clip:padding-box;border:2px solid #0000}*::-webkit-scrollbar-corner{background:0 0}@media (prefers-reduced-motion:reduce){.stats-button,.stats-table-tr-clickable,.stats-nav-rail-item,.stats-range-control-btn,.stats-segmented-control-btn{transition:none}.stats-button:active:not(:disabled){transform:none}.stats-mobile-drawer,.stats-drawer{animation:none}}.stats-logo-container{display:grid;grid-template-columns:auto 1fr;align-items: center;column-gap:10px}.stats-logo-container:before{content:"";grid-row:1/3;background:linear-gradient(135deg,#ed4abf 0%,#9b4dff 50%,#5ad8e6 100%);border-radius:4px;width:16px;height:16px}.stats-logo-text,.stats-logo-subtext{grid-column:2}.stats-json-actions{display:flex;align-items: center;gap:12px}.stats-json-copy-btn{display:inline-flex;color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;background:0 0;align-items: center;gap:5px;padding:3px 8px;transition:color .12s,background .12s;font-family:inherit;font-size:11px;font-weight:600}.stats-json-copy-btn:hover{color:var(--text);background:var(--surface-2)}.stats-empty-state-icon{color:var(--dim);margin-bottom:12px}@media (prefers-reduced-motion:reduce){.stats-json-copy-btn{transition:none}}.stats-theme-toggle{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface);color:var(--muted);cursor:pointer;flex-shrink:0;justify-content:center;align-items: center;width:32px;height:32px;transition:color .14s,background .14s,border-color .14s}.stats-theme-toggle:hover{color:var(--text);border-color:var(--border-strong)}@media (prefers-reduced-motion:reduce){.stats-theme-toggle{transition:none}}.stats-trace-view{display:flex;flex-direction:column;gap:14px}.stats-trace-header{display:flex;flex-wrap:wrap;align-items: center;gap:12px;min-width:0}.stats-trace-back{display:inline-flex;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;align-items: center;gap:6px;padding:6px 12px;transition:color .14s,border-color .14s;font-size:12px}.stats-trace-back:hover{color:var(--text);border-color:var(--border-strong)}.stats-trace-back:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-title{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;margin:0;font-size:15px;font-weight:600}.stats-trace-cwd{color:var(--dim);border:1px solid var(--border);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:280px;padding:3px 10px;font-size:11px}.stats-trace-summary{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;flex-wrap:wrap}.stats-trace-summary-cell{display:flex;border-right:1px solid var(--border);flex-direction:column;gap:3px;min-width:76px;padding:10px 16px}.stats-trace-summary-cell:last-child{border-right:none}.stats-trace-summary-label{text-transform:uppercase;letter-spacing:.07em;color:var(--dim);font-size:9px}.stats-trace-summary-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:13px;font-weight:500}.stats-trace-toolbar{display:flex;flex-wrap:wrap;align-items: center;gap:12px}.stats-trace-toolbar-hint{color:var(--dim);margin-left:auto;font-size:10px}.stats-trace-input{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text);padding:6px 10px;font-size:12px}.stats-trace-input:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-trace-input::placeholder{color:var(--dim)}.stats-trace-check{display:inline-flex;color:var(--muted);cursor:pointer;user-select:none;align-items: center;gap:6px;font-size:12px}.stats-trace-icon-btn{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);color:var(--muted);cursor:pointer;background:0 0;justify-content:center;align-items: center;width:26px;height:26px;transition:color .14s,border-color .14s}.stats-trace-icon-btn:hover:not(:disabled){color:var(--text);border-color:var(--border-strong)}.stats-trace-icon-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-icon-btn:disabled{opacity:.4;cursor:default}.stats-trace-timeline-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;gap:10px;padding:12px}.stats-trace-timeline{position:relative;display:flex;overflow-y:auto;overflow-x:hidden;max-height:560px}.stats-trace-canvas{cursor:grab;outline:none;touch-action:none;border-radius:var(--radius-sm)}.stats-trace-canvas:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-canvas:active{cursor:grabbing}.stats-trace-chevron{display:inline-flex;cursor:pointer;color:var(--muted);background:0 0;border:none;padding:0}.stats-trace-chevron:hover{color:var(--text)}.stats-trace-gutter-track{color:var(--text);font-size:11px;font-weight:500}.stats-trace-gutter-model{color:var(--dim);font-size:9px}.stats-trace-gutter-lane{position:absolute;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-size:8px}.stats-trace-list{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow-y:auto;height:340px}.stats-trace-row{display:flex;border:none;border-bottom:1px solid var(--border);text-align:left;cursor:pointer;background:0 0;align-items: center;gap:10px;width:100%;height:28px;padding:0 12px;transition:background .1s}.stats-trace-row:hover{background:var(--bg-hover)}.stats-trace-row:focus-visible{outline:2px solid var(--link);outline-offset:-2px}.stats-trace-row[data-selected=true]{background:var(--accent-muted)}.stats-trace-row-static{cursor:default}.stats-trace-row-static:hover{background:0 0}.stats-trace-kind{text-transform:uppercase;letter-spacing:.06em;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:3px;flex-shrink:0;width:58px;padding:2px 0;font-size:8px}.stats-trace-row-meta{color:var(--dim);font-variant-numeric:tabular-nums;display:flex;flex-shrink:0;gap:10px;margin-left:auto;font-size:10px}@media (prefers-reduced-motion:reduce){.stats-trace-back,.stats-trace-icon-btn,.stats-trace-row{transition:none}}
|
|
1
|
+
@layer theme,base,components,utilities;@layer theme{@theme default{--font-sans: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; --font-serif: ui-serif,Georgia,Cambria,"Times New Roman",Times,serif; --font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; --color-red-50: oklch(97.1% .013 17.38); --color-red-100: oklch(93.6% .032 17.717); --color-red-200: oklch(88.5% .062 18.334); --color-red-300: oklch(80.8% .114 19.571); --color-red-400: oklch(70.4% .191 22.216); --color-red-500: oklch(63.7% .237 25.331); --color-red-600: oklch(57.7% .245 27.325); --color-red-700: oklch(50.5% .213 27.518); --color-red-800: oklch(44.4% .177 26.899); --color-red-900: oklch(39.6% .141 25.723); --color-red-950: oklch(25.8% .092 26.042); --color-orange-50: oklch(98% .016 73.684); --color-orange-100: oklch(95.4% .038 75.164); --color-orange-200: oklch(90.1% .076 70.697); --color-orange-300: oklch(83.7% .128 66.29); --color-orange-400: oklch(75% .183 55.934); --color-orange-500: oklch(70.5% .213 47.604); --color-orange-600: oklch(64.6% .222 41.116); --color-orange-700: oklch(55.3% .195 38.402); --color-orange-800: oklch(47% .157 37.304); --color-orange-900: oklch(40.8% .123 38.172); --color-orange-950: oklch(26.6% .079 36.259); --color-amber-50: oklch(98.7% .022 95.277); --color-amber-100: oklch(96.2% .059 95.617); --color-amber-200: oklch(92.4% .12 95.746); --color-amber-300: oklch(87.9% .169 91.605); --color-amber-400: oklch(82.8% .189 84.429); --color-amber-500: oklch(76.9% .188 70.08); --color-amber-600: oklch(66.6% .179 58.318); --color-amber-700: oklch(55.5% .163 48.998); --color-amber-800: oklch(47.3% .137 46.201); --color-amber-900: oklch(41.4% .112 45.904); --color-amber-950: oklch(27.9% .077 45.635); --color-yellow-50: oklch(98.7% .026 102.212); --color-yellow-100: oklch(97.3% .071 103.193); --color-yellow-200: oklch(94.5% .129 101.54); --color-yellow-300: oklch(90.5% .182 98.111); --color-yellow-400: oklch(85.2% .199 91.936); --color-yellow-500: oklch(79.5% .184 86.047); --color-yellow-600: oklch(68.1% .162 75.834); --color-yellow-700: oklch(55.4% .135 66.442); --color-yellow-800: oklch(47.6% .114 61.907); --color-yellow-900: oklch(42.1% .095 57.708); --color-yellow-950: oklch(28.6% .066 53.813); --color-lime-50: oklch(98.6% .031 120.757); --color-lime-100: oklch(96.7% .067 122.328); --color-lime-200: oklch(93.8% .127 124.321); --color-lime-300: oklch(89.7% .196 126.665); --color-lime-400: oklch(84.1% .238 128.85); --color-lime-500: oklch(76.8% .233 130.85); --color-lime-600: oklch(64.8% .2 131.684); --color-lime-700: oklch(53.2% .157 131.589); --color-lime-800: oklch(45.3% .124 130.933); --color-lime-900: oklch(40.5% .101 131.063); --color-lime-950: oklch(27.4% .072 132.109); --color-green-50: oklch(98.2% .018 155.826); --color-green-100: oklch(96.2% .044 156.743); --color-green-200: oklch(92.5% .084 155.995); --color-green-300: oklch(87.1% .15 154.449); --color-green-400: oklch(79.2% .209 151.711); --color-green-500: oklch(72.3% .219 149.579); --color-green-600: oklch(62.7% .194 149.214); --color-green-700: oklch(52.7% .154 150.069); --color-green-800: oklch(44.8% .119 151.328); --color-green-900: oklch(39.3% .095 152.535); --color-green-950: oklch(26.6% .065 152.934); --color-emerald-50: oklch(97.9% .021 166.113); --color-emerald-100: oklch(95% .052 163.051); --color-emerald-200: oklch(90.5% .093 164.15); --color-emerald-300: oklch(84.5% .143 164.978); --color-emerald-400: oklch(76.5% .177 163.223); --color-emerald-500: oklch(69.6% .17 162.48); --color-emerald-600: oklch(59.6% .145 163.225); --color-emerald-700: oklch(50.8% .118 165.612); --color-emerald-800: oklch(43.2% .095 166.913); --color-emerald-900: oklch(37.8% .077 168.94); --color-emerald-950: oklch(26.2% .051 172.552); --color-teal-50: oklch(98.4% .014 180.72); --color-teal-100: oklch(95.3% .051 180.801); --color-teal-200: oklch(91% .096 180.426); --color-teal-300: oklch(85.5% .138 181.071); --color-teal-400: oklch(77.7% .152 181.912); --color-teal-500: oklch(70.4% .14 182.503); --color-teal-600: oklch(60% .118 184.704); --color-teal-700: oklch(51.1% .096 186.391); --color-teal-800: oklch(43.7% .078 188.216); --color-teal-900: oklch(38.6% .063 188.416); --color-teal-950: oklch(27.7% .046 192.524); --color-cyan-50: oklch(98.4% .019 200.873); --color-cyan-100: oklch(95.6% .045 203.388); --color-cyan-200: oklch(91.7% .08 205.041); --color-cyan-300: oklch(86.5% .127 207.078); --color-cyan-400: oklch(78.9% .154 211.53); --color-cyan-500: oklch(71.5% .143 215.221); --color-cyan-600: oklch(60.9% .126 221.723); --color-cyan-700: oklch(52% .105 223.128); --color-cyan-800: oklch(45% .085 224.283); --color-cyan-900: oklch(39.8% .07 227.392); --color-cyan-950: oklch(30.2% .056 229.695); --color-sky-50: oklch(97.7% .013 236.62); --color-sky-100: oklch(95.1% .026 236.824); --color-sky-200: oklch(90.1% .058 230.902); --color-sky-300: oklch(82.8% .111 230.318); --color-sky-400: oklch(74.6% .16 232.661); --color-sky-500: oklch(68.5% .169 237.323); --color-sky-600: oklch(58.8% .158 241.966); --color-sky-700: oklch(50% .134 242.749); --color-sky-800: oklch(44.3% .11 240.79); --color-sky-900: oklch(39.1% .09 240.876); --color-sky-950: oklch(29.3% .066 243.157); --color-blue-50: oklch(97% .014 254.604); --color-blue-100: oklch(93.2% .032 255.585); --color-blue-200: oklch(88.2% .059 254.128); --color-blue-300: oklch(80.9% .105 251.813); --color-blue-400: oklch(70.7% .165 254.624); --color-blue-500: oklch(62.3% .214 259.815); --color-blue-600: oklch(54.6% .245 262.881); --color-blue-700: oklch(48.8% .243 264.376); --color-blue-800: oklch(42.4% .199 265.638); --color-blue-900: oklch(37.9% .146 265.522); --color-blue-950: oklch(28.2% .091 267.935); --color-indigo-50: oklch(96.2% .018 272.314); --color-indigo-100: oklch(93% .034 272.788); --color-indigo-200: oklch(87% .065 274.039); --color-indigo-300: oklch(78.5% .115 274.713); --color-indigo-400: oklch(67.3% .182 276.935); --color-indigo-500: oklch(58.5% .233 277.117); --color-indigo-600: oklch(51.1% .262 276.966); --color-indigo-700: oklch(45.7% .24 277.023); --color-indigo-800: oklch(39.8% .195 277.366); --color-indigo-900: oklch(35.9% .144 278.697); --color-indigo-950: oklch(25.7% .09 281.288); --color-violet-50: oklch(96.9% .016 293.756); --color-violet-100: oklch(94.3% .029 294.588); --color-violet-200: oklch(89.4% .057 293.283); --color-violet-300: oklch(81.1% .111 293.571); --color-violet-400: oklch(70.2% .183 293.541); --color-violet-500: oklch(60.6% .25 292.717); --color-violet-600: oklch(54.1% .281 293.009); --color-violet-700: oklch(49.1% .27 292.581); --color-violet-800: oklch(43.2% .232 292.759); --color-violet-900: oklch(38% .189 293.745); --color-violet-950: oklch(28.3% .141 291.089); --color-purple-50: oklch(97.7% .014 308.299); --color-purple-100: oklch(94.6% .033 307.174); --color-purple-200: oklch(90.2% .063 306.703); --color-purple-300: oklch(82.7% .119 306.383); --color-purple-400: oklch(71.4% .203 305.504); --color-purple-500: oklch(62.7% .265 303.9); --color-purple-600: oklch(55.8% .288 302.321); --color-purple-700: oklch(49.6% .265 301.924); --color-purple-800: oklch(43.8% .218 303.724); --color-purple-900: oklch(38.1% .176 304.987); --color-purple-950: oklch(29.1% .149 302.717); --color-fuchsia-50: oklch(97.7% .017 320.058); --color-fuchsia-100: oklch(95.2% .037 318.852); --color-fuchsia-200: oklch(90.3% .076 319.62); --color-fuchsia-300: oklch(83.3% .145 321.434); --color-fuchsia-400: oklch(74% .238 322.16); --color-fuchsia-500: oklch(66.7% .295 322.15); --color-fuchsia-600: oklch(59.1% .293 322.896); --color-fuchsia-700: oklch(51.8% .253 323.949); --color-fuchsia-800: oklch(45.2% .211 324.591); --color-fuchsia-900: oklch(40.1% .17 325.612); --color-fuchsia-950: oklch(29.3% .136 325.661); --color-pink-50: oklch(97.1% .014 343.198); --color-pink-100: oklch(94.8% .028 342.258); --color-pink-200: oklch(89.9% .061 343.231); --color-pink-300: oklch(82.3% .12 346.018); --color-pink-400: oklch(71.8% .202 349.761); --color-pink-500: oklch(65.6% .241 354.308); --color-pink-600: oklch(59.2% .249 .584); --color-pink-700: oklch(52.5% .223 3.958); --color-pink-800: oklch(45.9% .187 3.815); --color-pink-900: oklch(40.8% .153 2.432); --color-pink-950: oklch(28.4% .109 3.907); --color-rose-50: oklch(96.9% .015 12.422); --color-rose-100: oklch(94.1% .03 12.58); --color-rose-200: oklch(89.2% .058 10.001); --color-rose-300: oklch(81% .117 11.638); --color-rose-400: oklch(71.2% .194 13.428); --color-rose-500: oklch(64.5% .246 16.439); --color-rose-600: oklch(58.6% .253 17.585); --color-rose-700: oklch(51.4% .222 16.935); --color-rose-800: oklch(45.5% .188 13.697); --color-rose-900: oklch(41% .159 10.272); --color-rose-950: oklch(27.1% .105 12.094); --color-slate-50: oklch(98.4% .003 247.858); --color-slate-100: oklch(96.8% .007 247.896); --color-slate-200: oklch(92.9% .013 255.508); --color-slate-300: oklch(86.9% .022 252.894); --color-slate-400: oklch(70.4% .04 256.788); --color-slate-500: oklch(55.4% .046 257.417); --color-slate-600: oklch(44.6% .043 257.281); --color-slate-700: oklch(37.2% .044 257.287); --color-slate-800: oklch(27.9% .041 260.031); --color-slate-900: oklch(20.8% .042 265.755); --color-slate-950: oklch(12.9% .042 264.695); --color-gray-50: oklch(98.5% .002 247.839); --color-gray-100: oklch(96.7% .003 264.542); --color-gray-200: oklch(92.8% .006 264.531); --color-gray-300: oklch(87.2% .01 258.338); --color-gray-400: oklch(70.7% .022 261.325); --color-gray-500: oklch(55.1% .027 264.364); --color-gray-600: oklch(44.6% .03 256.802); --color-gray-700: oklch(37.3% .034 259.733); --color-gray-800: oklch(27.8% .033 256.848); --color-gray-900: oklch(21% .034 264.665); --color-gray-950: oklch(13% .028 261.692); --color-zinc-50: oklch(98.5% 0 none); --color-zinc-100: oklch(96.7% .001 286.375); --color-zinc-200: oklch(92% .004 286.32); --color-zinc-300: oklch(87.1% .006 286.286); --color-zinc-400: oklch(70.5% .015 286.067); --color-zinc-500: oklch(55.2% .016 285.938); --color-zinc-600: oklch(44.2% .017 285.786); --color-zinc-700: oklch(37% .013 285.805); --color-zinc-800: oklch(27.4% .006 286.033); --color-zinc-900: oklch(21% .006 285.885); --color-zinc-950: oklch(14.1% .005 285.823); --color-neutral-50: oklch(98.5% 0 none); --color-neutral-100: oklch(97% 0 none); --color-neutral-200: oklch(92.2% 0 none); --color-neutral-300: oklch(87% 0 none); --color-neutral-400: oklch(70.8% 0 none); --color-neutral-500: oklch(55.6% 0 none); --color-neutral-600: oklch(43.9% 0 none); --color-neutral-700: oklch(37.1% 0 none); --color-neutral-800: oklch(26.9% 0 none); --color-neutral-900: oklch(20.5% 0 none); --color-neutral-950: oklch(14.5% 0 none); --color-stone-50: oklch(98.5% .001 106.423); --color-stone-100: oklch(97% .001 106.424); --color-stone-200: oklch(92.3% .003 48.717); --color-stone-300: oklch(86.9% .005 56.366); --color-stone-400: oklch(70.9% .01 56.259); --color-stone-500: oklch(55.3% .013 58.071); --color-stone-600: oklch(44.4% .011 73.639); --color-stone-700: oklch(37.4% .01 67.558); --color-stone-800: oklch(26.8% .007 34.298); --color-stone-900: oklch(21.6% .006 56.043); --color-stone-950: oklch(14.7% .004 49.25); --color-mauve-50: oklch(98.5% 0 none); --color-mauve-100: oklch(96% .003 325.6); --color-mauve-200: oklch(92.2% .005 325.62); --color-mauve-300: oklch(86.5% .012 325.68); --color-mauve-400: oklch(71.1% .019 323.02); --color-mauve-500: oklch(54.2% .034 322.5); --color-mauve-600: oklch(43.5% .029 321.78); --color-mauve-700: oklch(36.4% .029 323.89); --color-mauve-800: oklch(26.3% .024 320.12); --color-mauve-900: oklch(21.2% .019 322.12); --color-mauve-950: oklch(14.5% .008 326); --color-olive-50: oklch(98.8% .003 106.5); --color-olive-100: oklch(96.6% .005 106.5); --color-olive-200: oklch(93% .007 106.5); --color-olive-300: oklch(88% .011 106.6); --color-olive-400: oklch(73.7% .021 106.9); --color-olive-500: oklch(58% .031 107.3); --color-olive-600: oklch(46.6% .025 107.3); --color-olive-700: oklch(39.4% .023 107.4); --color-olive-800: oklch(28.6% .016 107.4); --color-olive-900: oklch(22.8% .013 107.4); --color-olive-950: oklch(15.3% .006 107.1); --color-mist-50: oklch(98.7% .002 197.1); --color-mist-100: oklch(96.3% .002 197.1); --color-mist-200: oklch(92.5% .005 214.3); --color-mist-300: oklch(87.2% .007 219.6); --color-mist-400: oklch(72.3% .014 214.4); --color-mist-500: oklch(56% .021 213.5); --color-mist-600: oklch(45% .017 213.2); --color-mist-700: oklch(37.8% .015 216); --color-mist-800: oklch(27.5% .011 216.9); --color-mist-900: oklch(21.8% .008 223.9); --color-mist-950: oklch(14.8% .004 228.8); --color-taupe-50: oklch(98.6% .002 67.8); --color-taupe-100: oklch(96% .002 17.2); --color-taupe-200: oklch(92.2% .005 34.3); --color-taupe-300: oklch(86.8% .007 39.5); --color-taupe-400: oklch(71.4% .014 41.2); --color-taupe-500: oklch(54.7% .021 43.1); --color-taupe-600: oklch(43.8% .017 39.3); --color-taupe-700: oklch(36.7% .016 35.7); --color-taupe-800: oklch(26.8% .011 36.5); --color-taupe-900: oklch(21.4% .009 43.1); --color-taupe-950: oklch(14.7% .004 49.3); --color-black: #000; --color-white: #fff; --spacing: .25rem; --breakpoint-sm: 40rem; --breakpoint-md: 48rem; --breakpoint-lg: 64rem; --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; --container-3xs: 16rem; --container-2xs: 18rem; --container-xs: 20rem; --container-sm: 24rem; --container-md: 28rem; --container-lg: 32rem; --container-xl: 36rem; --container-2xl: 42rem; --container-3xl: 48rem; --container-4xl: 56rem; --container-5xl: 64rem; --container-6xl: 72rem; --container-7xl: 80rem; --text-xs: .75rem; --text-xs--line-height: calc(1/.75); --text-sm: .875rem; --text-sm--line-height: calc(1.25/.875); --text-base: 1rem; --text-base--line-height: calc(1.5/1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75/1.125); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75/1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2/1.5); --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25/1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5/2.25); --text-5xl: 3rem; --text-5xl--line-height: 1; --text-6xl: 3.75rem; --text-6xl--line-height: 1; --text-7xl: 4.5rem; --text-7xl--line-height: 1; --text-8xl: 6rem; --text-8xl--line-height: 1; --text-9xl: 8rem; --text-9xl--line-height: 1; --font-weight-thin: 100; --font-weight-extralight: 200; --font-weight-light: 300; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --font-weight-extrabold: 800; --font-weight-black: 900; --tracking-tighter: -.05em; --tracking-tight: -.025em; --tracking-normal: 0em; --tracking-wide: .025em; --tracking-wider: .05em; --tracking-widest: .1em; --leading-tight: 1.25; --leading-snug: 1.375; --leading-normal: 1.5; --leading-relaxed: 1.625; --leading-loose: 2; --radius-xs: .125rem; --radius-sm: .25rem; --radius-md: .375rem; --radius-lg: .5rem; --radius-xl: .75rem; --radius-2xl: 1rem; --radius-3xl: 1.5rem; --radius-4xl: 2rem; --shadow-2xs: 0 1px #0000000d; --shadow-xs: 0 1px 2px 0 #0000000d; --shadow-sm: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-md: 0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a; --shadow-lg: 0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a; --shadow-xl: 0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a; --shadow-2xl: 0 25px 50px -12px #00000040; --inset-shadow-2xs: inset 0 1px #0000000d; --inset-shadow-xs: inset 0 1px 1px #0000000d; --inset-shadow-sm: inset 0 2px 4px #0000000d; --drop-shadow-xs: 0 1px 1px #0000000d; --drop-shadow-sm: 0 1px 2px #00000026; --drop-shadow-md: 0 3px 3px #0000001f; --drop-shadow-lg: 0 4px 4px #00000026; --drop-shadow-xl: 0 9px 7px #0000001a; --drop-shadow-2xl: 0 25px 25px #00000026; --text-shadow-2xs: 0px 1px 0px #00000026; --text-shadow-xs: 0px 1px 1px #0003; --text-shadow-sm: 0px 1px 0px #00000013,0px 1px 1px #00000013,0px 2px 2px #00000013; --text-shadow-md: 0px 1px 1px #0000001a,0px 1px 2px #0000001a,0px 2px 4px #0000001a; --text-shadow-lg: 0px 1px 2px #0000001a,0px 3px 2px #0000001a,0px 4px 8px #0000001a; --ease-in: cubic-bezier(.4,0,1,1); --ease-out: cubic-bezier(0,0,.2,1); --ease-in-out: cubic-bezier(.4,0,.2,1); --animate-spin: spin 1s linear infinite; --animate-ping: ping 1s cubic-bezier(0,0,.2,1)infinite; --animate-pulse: pulse 2s cubic-bezier(.4,0,.6,1)infinite; --animate-bounce: bounce 1s infinite; @keyframes spin { to { transform: rotate(360deg); }}@keyframes ping { 75%,100% { transform: scale(2); opacity: 0; }}@keyframes pulse { 50% { opacity: .5; }}@keyframes bounce { 0%,100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(.8,0,1,1); }50% { transform: none; animation-timing-function: cubic-bezier(0,0,.2,1); }}--blur-xs: 4px; --blur-sm: 8px; --blur-md: 12px; --blur-lg: 16px; --blur-xl: 24px; --blur-2xl: 40px; --blur-3xl: 64px; --perspective-dramatic: 100px; --perspective-near: 300px; --perspective-normal: 500px; --perspective-midrange: 800px; --perspective-distant: 1200px; --aspect-video: 16/9; --default-transition-duration: .15s; --default-transition-timing-function: cubic-bezier(.4,0,.2,1); --default-font-family: --theme(--font-sans,initial); --default-font-feature-settings: --theme(--font-sans--font-feature-settings,initial); --default-font-variation-settings: --theme(--font-sans--font-variation-settings,initial); --default-mono-font-family: --theme(--font-mono,initial); --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings,initial); --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings,initial);}@theme default inline reference{--blur: 8px; --shadow: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-inner: inset 0 2px 4px 0 #0000000d; --drop-shadow: 0 1px 2px #0000001a,0 1px 1px #0000000f; --radius: .25rem; --max-width-prose: 65ch;}}@layer base{*,:after,:before{box-sizing:border-box;border:0 solid;margin:0;padding:0}::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::-webkit-file-upload-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:--theme(--default-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:--theme(--default-font-feature-settings,normal);font-variation-settings:--theme(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:--theme(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:--theme(--default-mono-font-feature-settings,normal);font-variation-settings:--theme(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{position:relative;vertical-align:baseline;font-size:75%;line-height:0}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::-webkit-file-upload-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:-webkit-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:-moz-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:-webkit-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:-moz-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::-webkit-file-upload-button{margin-inline-end:4px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports ( not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{text-align:inherit;min-height:1lh}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-year-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-month-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-day-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-hour-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-minute-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-second-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-millisecond-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-meridiem-field{padding-block-start:0;padding-block-end:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button{appearance:button}input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-file-upload-button{appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{@tailwind utilities;}:root{--buncss-light: ;--buncss-dark:initial;color-scheme:dark;--page:oklch(.16 .02 307);--surface:oklch(.19 .022 307);--surface-2:oklch(.235 .026 307);--border:oklch(1 0 0/9%);--border-strong:oklch(1 0 0/13%);--text:oklch(.92 .01 307);--muted:oklch(.71 .016 307);--dim:oklch(.53 .018 307);--accent:oklch(.674 .23 341);--accent-fg:oklch(.2 .07 341);--accent-muted:oklch(.674 .23 341/18%);--link:oklch(.817 .112 205);--success:oklch(.76 .14 150);--danger:oklch(.66 .19 25);--warning:oklch(.79 .14 85);--text-primary:var(--text);--text-secondary:var(--muted);--text-muted:var(--dim);--border-subtle:var(--border);--accent-red:var(--danger);--accent-green:var(--success);--accent-cyan:var(--link);--bg-hover:var(--surface-2);--bg-elevated:var(--surface-2);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--radius-sm:3px;--radius-md:6px;--radius-lg:8px}@media (prefers-color-scheme:light){:root:not([data-theme]){--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}}[data-theme=light]{--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}@layer base{*{box-sizing:border-box}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background:var(--page);color:var(--text);min-height:100vh;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}button:focus-visible{outline:2px solid var(--link);outline-offset:2px}a:focus-visible{outline:2px solid var(--link);outline-offset:2px}[tabindex]:focus-visible{outline:2px solid var(--link);outline-offset:2px}}.stats-app-container{display:flex;background:var(--page);color:var(--text);min-height:100vh}.stats-nav-rail{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;width:240px}.stats-desktop-nav{display:flex}@media (max-width:1023px){.stats-desktop-nav{display:none}}.stats-nav-rail-header{border-bottom:1px solid var(--border);display:flex;align-items: center;height:56px;padding:20px 24px}.stats-logo-container{display:flex;flex-direction:column}.stats-logo-text{letter-spacing:.1em;color:var(--text);font-size:14px;font-weight:700}.stats-logo-subtext{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-size:10px}.stats-nav-rail-menu{display:flex;flex-direction:column;flex:1;gap:4px;padding:16px 12px}.stats-nav-rail-item{display:flex;color:var(--muted);border-radius:var(--radius-md);cursor:pointer;text-align:left;background:0 0;border:none;align-items: center;gap:12px;padding:10px 16px;transition:all .15s;font-size:13px;font-weight:500}.stats-nav-rail-item:hover{color:var(--text);background:var(--surface-2)}.stats-nav-rail-item[data-active=true]{color:var(--accent);background:var(--accent-muted);position:relative}.stats-nav-rail-item[data-active=true]:before{content:"";position:absolute;background:var(--accent);border-radius:0 3px 3px 0;width:3px;height:18px;top:50%;left:0;transform:translateY(-50%)}.stats-nav-rail-item-icon{flex-shrink:0}.stats-nav-rail-footer{border-top:1px solid var(--border);padding:16px 24px}.stats-version-tag{color:var(--dim);font-size:11px}.stats-main-pane{display:flex;flex-direction:column;flex:1;min-width:0}.stats-top-bar{background:var(--surface);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;height:56px;padding:0 24px}@media (max-width:767px){.stats-top-bar{flex-wrap:wrap;gap:12px;height:auto;min-height:56px;padding:8px 16px}}.stats-top-bar-left{display:flex;align-items: center;gap:12px}.stats-mobile-menu-btn{display:none;color:var(--text);cursor:pointer;background:0 0;border:none;padding:4px}@media (max-width:1023px){.stats-mobile-menu-btn{display:inline-flex;justify-content:center;align-items: center}}.stats-page-title{color:var(--text);margin:0;font-size:16px;font-weight:600}.stats-top-bar-right{display:flex;align-items: center;gap:16px}@media (max-width:767px){.stats-top-bar-right{justify-content:space-between;gap:8px;width:100%}}.stats-top-bar-meta{display:flex;align-items: center}.stats-last-updated{color:var(--dim);font-variant-numeric:tabular-nums;font-size:11px}.stats-content-area{overflow-y:auto;flex:1}.stats-content-inner{display:flex;flex-direction:column;gap:24px;max-width:1560px;margin:0 auto;padding:24px}@media (max-width:767px){.stats-content-inner{gap:16px;padding:16px}}.stats-mobile-drawer-overlay{position:fixed;backdrop-filter:none;z-index:100;background:#0006;inset:0}.stats-mobile-drawer{position:absolute;background:var(--surface);border-right:1px solid var(--border);display:flex;z-index:101;animation:.2s ease-out slideIn;flex-direction:column;width:260px;top:0;bottom:0;left:0}@keyframes slideIn{0%{transform:translate(-100%)}to{transform:translate(0)}}.stats-mobile-drawer-header{display:flex;border-bottom:1px solid var(--border);justify-content:space-between;align-items: center;height:56px;padding:0 16px 0 24px}.stats-mobile-nav{display:flex;height:calc(100% - 56px)}.stats-button{display:inline-flex;border-radius:var(--radius-md);cursor:pointer;border:1px solid #0000;justify-content:center;align-items: center;gap:8px;padding:8px 14px;transition:all .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-button:active:not(:disabled){transform:translateY(1px)}.stats-button-primary{background:var(--accent);color:var(--accent-fg)}.stats-button-primary:hover:not(:disabled){opacity:.9}.stats-button-primary:disabled{opacity:.5;cursor:not-allowed}.stats-button-secondary{background:var(--surface-2);color:var(--text);border-color:var(--border)}.stats-button-secondary:hover:not(:disabled){background:var(--border)}.stats-button-secondary:disabled{opacity:.5;cursor:not-allowed}.stats-range-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:2px}.stats-range-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;background:0 0;border:none;padding:4px 10px;transition:all .1s;font-size:12px;font-weight:500}.stats-range-control-btn:hover{color:var(--text)}.stats-range-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-select{appearance:none;color:var(--text);background-color:var(--surface-2);background-image:var(--select-chevron);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;text-overflow:ellipsis;background-position:right 10px center;background-repeat:no-repeat;max-width:100%;padding:6px 28px 6px 12px;transition:border-color .15s,background-color .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-select:hover:not(:disabled){border-color:var(--border-strong);background-color:var(--bg-hover)}.stats-select:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-select:disabled{opacity:.5;cursor:not-allowed}@media (pointer:coarse){.stats-select{font-size:16px}}.stats-select option{background-color:var(--surface);color:var(--text)}@media (forced-colors:active){.stats-select{appearance:auto}}.stats-sync-container{display:flex;align-items: center;gap:12px}.stats-sync-status-msg{font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;font-size:11px}.stats-sync-status-msg[data-type=success]{color:var(--success)}.stats-sync-status-msg[data-type=error]{color:var(--danger)}.stats-sync-icon{flex-shrink:0}.stats-spin{animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.stats-panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column}.stats-panel-header{border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;gap:16px;padding:16px 20px}@media (max-width:480px){.stats-panel-header{flex-direction:column;align-items: flex-start;gap:8px}}.stats-panel-header-titles{display:flex;flex-direction:column}.stats-panel-title{color:var(--text);margin:0;font-size:14px;font-weight:600}.stats-panel-subtitle{color:var(--muted);margin:2px 0 0;font-size:12px}.stats-panel-actions{display:flex;align-items: center;gap:12px}.stats-panel-body{padding:20px}.stats-metric-cluster{display:flex;flex-direction:column;gap:16px}.stats-metric-primary-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:16px}@media (max-width:1023px){.stats-metric-primary-grid{grid-template-columns:repeat(2,1fr)}}@media (max-width:480px){.stats-metric-primary-grid{grid-template-columns:1fr}}.stats-metric-secondary-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:12px}@media (max-width:1279px){.stats-metric-secondary-grid{grid-template-columns:repeat(4,1fr)}}@media (max-width:767px){.stats-metric-secondary-grid{grid-template-columns:repeat(2,1fr)}}.stats-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:space-between;padding:16px}.stats-metric-card.primary{padding:20px 18px}.stats-metric-card.primary .stats-metric-label{color:var(--accent)}.stats-metric-card.secondary{padding:12px 14px}.stats-metric-label{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:8px;font-size:11px;font-weight:600}.stats-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:20px;font-weight:700}.stats-metric-card.secondary .stats-metric-value{font-size:15px;font-weight:600}.stats-table-wrapper{width:100%}.stats-table-container{overflow-x:auto;width:100%}.stats-table{border-collapse:collapse;font-variant-numeric:tabular-nums;width:100%;font-size:13px}.stats-table-th{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);border-bottom:2px solid var(--border);padding:12px 16px;font-size:11px;font-weight:600}.stats-table-td{border-bottom:1px solid var(--border);color:var(--text);padding:10px 16px}.stats-table-tr{background:0 0}.stats-table-tr-clickable{cursor:pointer;transition:background .12s,box-shadow .12s}.stats-table-tr-clickable:hover{background:var(--surface-2);box-shadow:inset 2px 0 0 var(--accent)}.stats-text-left{text-align:left}.stats-text-right{text-align:right}.stats-table-empty{text-align:center;color:var(--muted);border:1px dashed var(--border);border-radius:var(--radius-lg);padding:40px;font-size:13px}.stats-progress-bar-track{background:var(--surface-2);overflow:hidden;border-radius:999px}.stats-progress-bar-fill{border-radius:inherit;height:100%;transition:width .18s}.stats-progress-bar-fill[data-variant=link]{background:var(--link)}.stats-progress-bar-fill[data-variant=success]{background:var(--success)}.stats-table-mobile-only{display:none}.stats-table-desktop-only{overflow-x:auto;width:100%}@media (max-width:767px){.stats-table-desktop-only{display:none}.stats-table-mobile-only{display:block}}.stats-table-mobile-list{display:flex;flex-direction:column;gap:12px}.stats-table-mobile-card-wrapper{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:14px}.stats-status-pill{display:inline-flex;border-radius:100px;justify-content:center;align-items: center;padding:2px 8px;font-size:11px;font-weight:600;line-height:1.2}.stats-status-pill[data-variant=success]{background:color-mix(in srgb,var(--success)18%,transparent);color:var(--success)}.stats-status-pill[data-variant=danger]{background:color-mix(in srgb,var(--danger)18%,transparent);color:var(--danger)}.stats-status-pill[data-variant=warning]{background:color-mix(in srgb,var(--warning)18%,transparent);color:var(--warning)}.stats-status-pill[data-variant=info]{background:color-mix(in srgb,var(--link)18%,transparent);color:var(--link)}.stats-status-pill[data-variant=default]{background:var(--surface-2);color:var(--muted)}.stats-segmented-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);gap:2px;padding:2px}.stats-segmented-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:6px 12px;transition:all .1s;font-size:12px;font-weight:500}.stats-segmented-control-btn:hover{color:var(--text)}.stats-segmented-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-drawer-overlay{position:fixed;z-index:100;display:flex;background:#00000080;justify-content:flex-end;inset:0}.stats-drawer{background:var(--surface);border-left:1px solid var(--border);display:flex;animation:.25s cubic-bezier(.16,1,.3,1) drawerSlide;flex-direction:column;width:600px;max-width:100%;height:100%;box-shadow:-4px 0 24px #00000040}@keyframes drawerSlide{0%{transform:translate(100%)}to{transform:translate(0)}}.stats-drawer-header{border-bottom:1px solid var(--border);display:flex;background:var(--surface);flex-shrink:0;justify-content:space-between;align-items: center;height:56px;padding:0 24px}.stats-drawer-header-left{display:flex;align-items:baseline;gap:12px}.stats-drawer-title{color:var(--text);margin:0;font-size:15px;font-weight:600}.stats-drawer-id{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px}.stats-drawer-close-btn{color:var(--muted);cursor:pointer;display:inline-flex;border-radius:var(--radius-sm);background:0 0;border:none;justify-content:center;align-items: center;padding:4px;transition:all .1s}.stats-drawer-close-btn:hover{color:var(--text);background:var(--surface-2)}.stats-drawer-body{overflow-y:auto;flex:1;padding:24px}.stats-drawer-content{display:flex;flex-direction:column;gap:24px}.stats-drawer-status-card{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px}.stats-drawer-status-row{display:flex;justify-content:space-between;align-items: flex-start;gap:16px}.stats-drawer-model{color:var(--text);font-size:16px;font-weight:700}.stats-drawer-provider{color:var(--muted);margin-top:2px;font-size:12px}.stats-drawer-error-block{border-top:1px solid var(--border);margin-top:14px;padding-top:12px}.stats-drawer-error-label{text-transform:uppercase;letter-spacing:.05em;color:var(--danger);margin-bottom:4px;font-size:11px;font-weight:600}.stats-drawer-error-text{color:var(--text);white-space:pre-wrap;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px}.stats-drawer-metrics-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.stats-drawer-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:center;padding:14px}.stats-drawer-metric-label{display:flex;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);align-items: center;gap:6px;margin-bottom:6px;font-size:11px;font-weight:600}.stats-drawer-metric-icon{color:var(--dim)}.stats-drawer-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:16px;font-weight:700}.stats-drawer-metric-sub{color:var(--muted);font-variant-numeric:tabular-nums;margin-top:2px;font-size:11px}.stats-drawer-json-blocks{display:flex;flex-direction:column;gap:16px}.stats-json-block{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);overflow:hidden}.stats-json-block-header{background:var(--surface-2);border-bottom:1px solid var(--border);display:flex;cursor:pointer;user-select:none;justify-content:space-between;align-items: center;padding:10px 16px;font-size:12px;font-weight:600}.stats-json-block-header:hover{background:var(--border)}.stats-json-block-title{color:var(--text)}.stats-json-block-toggle-indicator{color:var(--muted)}.stats-json-block-content-wrapper{background:var(--page);overflow-x:auto;padding:16px}.stats-json-block-content{color:var(--text);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5}.stats-skeleton{background:var(--surface-2);position:relative;overflow:hidden;border-radius:var(--radius-sm)}.stats-skeleton[data-variant=text]{height:14px;margin-top:4px;margin-bottom:4px}.stats-skeleton[data-variant=circle]{border-radius:50%}.stats-skeleton:after{content:"";position:absolute;animation:1.5s infinite shimmer;background:linear-gradient(90deg,#0000 0%,#ffffff0d 50%,#0000 100%);inset:0;transform:translate(-100%)}@media (prefers-color-scheme:light){.stats-skeleton:after{background:linear-gradient(90deg,#0000 0%,#00000008 50%,#0000 100%)}}@keyframes shimmer{to{transform:translate(100%)}}@media (prefers-reduced-motion:reduce){.stats-skeleton:after{animation:none}}.stats-boundary-skeleton{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);padding:20px}.stats-empty-state,.stats-error-state{display:flex;text-align:center;background:var(--surface);border:1px dashed var(--border);border-radius:var(--radius-lg);flex-direction:column;justify-content:center;align-items: center;padding:48px 24px}.stats-empty-state-message{color:var(--muted);margin:0;font-size:13px}.stats-error-state-content{max-width:360px}.stats-error-state-title{color:var(--text);margin:0 0 8px;font-size:14px;font-weight:600}.stats-error-state-message{color:var(--danger);word-break:break-word;margin:0 0 16px;font-size:12px}.stats-error-state-btn{margin:0 auto}.stats-text-primary{color:var(--text)}.stats-text-secondary{color:var(--muted)}.stats-text-muted{color:var(--dim)}.stats-text-warning{color:var(--warning)}.stats-text-xs{font-size:12px}.stats-font-medium{font-weight:500}.stats-font-semibold{font-weight:600}*{scrollbar-width:thin;scrollbar-color:var(--border-strong)transparent}*::-webkit-scrollbar{width:10px;height:10px}*::-webkit-scrollbar-track{background:0 0}*::-webkit-scrollbar-thumb{background:var(--border-strong);background-clip:padding-box;border:2px solid #0000;border-radius:999px}*::-webkit-scrollbar-thumb:hover{background:var(--dim);background-clip:padding-box;border:2px solid #0000}*::-webkit-scrollbar-corner{background:0 0}@media (prefers-reduced-motion:reduce){.stats-button,.stats-table-tr-clickable,.stats-nav-rail-item,.stats-range-control-btn,.stats-segmented-control-btn{transition:none}.stats-button:active:not(:disabled){transform:none}.stats-mobile-drawer,.stats-drawer{animation:none}}.stats-logo-container{display:grid;grid-template-columns:auto 1fr;align-items: center;column-gap:10px}.stats-logo-container:before{content:"";grid-row:1/3;background:linear-gradient(135deg,#ed4abf 0%,#9b4dff 50%,#5ad8e6 100%);border-radius:4px;width:16px;height:16px}.stats-logo-text,.stats-logo-subtext{grid-column:2}.stats-json-actions{display:flex;align-items: center;gap:12px}.stats-json-copy-btn{display:inline-flex;color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;background:0 0;align-items: center;gap:5px;padding:3px 8px;transition:color .12s,background .12s;font-family:inherit;font-size:11px;font-weight:600}.stats-json-copy-btn:hover{color:var(--text);background:var(--surface-2)}.stats-empty-state-icon{color:var(--dim);margin-bottom:12px}@media (prefers-reduced-motion:reduce){.stats-json-copy-btn{transition:none}}.stats-theme-toggle{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface);color:var(--muted);cursor:pointer;flex-shrink:0;justify-content:center;align-items: center;width:32px;height:32px;transition:color .14s,background .14s,border-color .14s}.stats-theme-toggle:hover{color:var(--text);border-color:var(--border-strong)}@media (prefers-reduced-motion:reduce){.stats-theme-toggle{transition:none}}.stats-trace-view{display:flex;flex-direction:column;gap:14px}.stats-trace-header{display:flex;flex-wrap:wrap;align-items: center;gap:12px;min-width:0}.stats-trace-back{display:inline-flex;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;align-items: center;gap:6px;padding:6px 12px;transition:color .14s,border-color .14s;font-size:12px}.stats-trace-back:hover{color:var(--text);border-color:var(--border-strong)}.stats-trace-back:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-title{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;margin:0;font-size:15px;font-weight:600}.stats-trace-cwd{color:var(--dim);border:1px solid var(--border);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:280px;padding:3px 10px;font-size:11px}.stats-trace-summary{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;flex-wrap:wrap}.stats-trace-summary-cell{display:flex;border-right:1px solid var(--border);flex-direction:column;gap:3px;min-width:76px;padding:10px 16px}.stats-trace-summary-cell:last-child{border-right:none}.stats-trace-summary-label{text-transform:uppercase;letter-spacing:.07em;color:var(--dim);font-size:9px}.stats-trace-summary-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:13px;font-weight:500}.stats-trace-toolbar{display:flex;flex-wrap:wrap;align-items: center;gap:12px}.stats-trace-toolbar-hint{color:var(--dim);margin-left:auto;font-size:10px}.stats-trace-input{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text);padding:6px 10px;font-family:inherit;font-size:12px}@media (pointer:coarse){.stats-trace-input{font-size:16px}}.stats-trace-input::-webkit-search-cancel-button{filter:invert(1)}@media (prefers-color-scheme:light){:root:not([data-theme]) .stats-trace-input::-webkit-search-cancel-button{filter:none}}[data-theme=light] .stats-trace-input::-webkit-search-cancel-button{filter:none}.stats-trace-input:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-trace-input::placeholder{color:var(--dim)}.stats-trace-check{display:inline-flex;color:var(--muted);cursor:pointer;user-select:none;align-items: center;gap:6px;font-size:12px}.stats-trace-check input[type=checkbox]{accent-color:var(--accent)}.stats-trace-icon-btn{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);color:var(--muted);cursor:pointer;background:0 0;justify-content:center;align-items: center;width:26px;height:26px;transition:color .14s,border-color .14s}.stats-trace-icon-btn:hover:not(:disabled){color:var(--text);border-color:var(--border-strong)}.stats-trace-icon-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-icon-btn:disabled{opacity:.4;cursor:default}.stats-trace-timeline-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;gap:10px;padding:12px}.stats-trace-timeline{position:relative;display:flex;overflow-y:auto;overflow-x:hidden;max-height:560px}.stats-trace-canvas{cursor:grab;outline:none;touch-action:none;border-radius:var(--radius-sm)}.stats-trace-canvas:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-canvas:active{cursor:grabbing}.stats-trace-chevron{display:inline-flex;cursor:pointer;color:var(--muted);background:0 0;border:none;padding:0}.stats-trace-chevron:hover{color:var(--text)}.stats-trace-gutter-track{color:var(--text);font-size:11px;font-weight:500}.stats-trace-gutter-model{color:var(--dim);font-size:9px}.stats-trace-gutter-lane{position:absolute;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-size:8px}.stats-trace-list{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow-y:auto;height:340px}.stats-trace-row{display:flex;border:none;border-bottom:1px solid var(--border);text-align:left;cursor:pointer;background:0 0;align-items: center;gap:10px;width:100%;height:28px;padding:0 12px;transition:background .1s}.stats-trace-row:hover{background:var(--bg-hover)}.stats-trace-row:focus-visible{outline:2px solid var(--link);outline-offset:-2px}.stats-trace-row[data-selected=true]{background:var(--accent-muted)}.stats-trace-row-static{cursor:default}.stats-trace-row-static:hover{background:0 0}.stats-trace-kind{text-transform:uppercase;letter-spacing:.06em;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:3px;flex-shrink:0;width:58px;padding:2px 0;font-size:8px}.stats-trace-row-meta{color:var(--dim);font-variant-numeric:tabular-nums;display:flex;flex-shrink:0;gap:10px;margin-left:auto;font-size:10px}@media (prefers-reduced-motion:reduce){.stats-trace-back,.stats-trace-icon-btn,.stats-trace-row{transition:none}}
|
package/dist/client/styles.css
CHANGED
|
@@ -1732,9 +1732,26 @@
|
|
|
1732
1732
|
border: 1px solid var(--border);
|
|
1733
1733
|
border-radius: var(--radius-md);
|
|
1734
1734
|
color: var(--text);
|
|
1735
|
+
font-family: inherit;
|
|
1735
1736
|
font-size: 12px;
|
|
1736
1737
|
padding: 6px 10px;
|
|
1737
1738
|
}
|
|
1739
|
+
@media (pointer: coarse) {
|
|
1740
|
+
.stats-trace-input {
|
|
1741
|
+
font-size: 16px;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
.stats-trace-input::-webkit-search-cancel-button {
|
|
1745
|
+
filter: invert(1);
|
|
1746
|
+
}
|
|
1747
|
+
@media (prefers-color-scheme: light) {
|
|
1748
|
+
:root:not([data-theme]) .stats-trace-input::-webkit-search-cancel-button {
|
|
1749
|
+
filter: none;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
[data-theme="light"] .stats-trace-input::-webkit-search-cancel-button {
|
|
1753
|
+
filter: none;
|
|
1754
|
+
}
|
|
1738
1755
|
.stats-trace-input:focus-visible {
|
|
1739
1756
|
outline: 2px solid var(--link);
|
|
1740
1757
|
outline-offset: 1px;
|
|
@@ -1751,6 +1768,9 @@
|
|
|
1751
1768
|
cursor: pointer;
|
|
1752
1769
|
user-select: none;
|
|
1753
1770
|
}
|
|
1771
|
+
.stats-trace-check input[type="checkbox"] {
|
|
1772
|
+
accent-color: var(--accent);
|
|
1773
|
+
}
|
|
1754
1774
|
.stats-trace-icon-btn {
|
|
1755
1775
|
display: inline-flex;
|
|
1756
1776
|
align-items: center;
|
package/dist/types/db.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { type ParseSessionResult, type SessionParserState } from "./parser.js";
|
|
2
3
|
import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, DailyActivityPoint, FolderStats, MessageStats, MessageStatsInput, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
|
|
3
4
|
/**
|
|
4
5
|
* Initialize the database and create tables.
|
|
@@ -12,11 +13,18 @@ export declare function isScheduledCatalogModel(provider: string, modelId: strin
|
|
|
12
13
|
export declare function getFileOffset(sessionFile: string): {
|
|
13
14
|
offset: number;
|
|
14
15
|
lastModified: number;
|
|
16
|
+
parserState?: SessionParserState;
|
|
15
17
|
} | null;
|
|
16
18
|
/**
|
|
17
19
|
* Update the stored offset for a session file.
|
|
18
20
|
*/
|
|
19
|
-
export declare function setFileOffset(sessionFile: string, offset: number, lastModified: number): void;
|
|
21
|
+
export declare function setFileOffset(sessionFile: string, offset: number, lastModified: number, parserState?: SessionParserState): void;
|
|
22
|
+
export declare function applySessionParseResult(sessionFile: string, result: ParseSessionResult, rebuild?: boolean): {
|
|
23
|
+
processed: number;
|
|
24
|
+
reconcile: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function prepareSessionSync(): boolean;
|
|
27
|
+
export declare function completeSessionSync(reconcile: boolean): void;
|
|
20
28
|
/**
|
|
21
29
|
* Insert message stats into the database.
|
|
22
30
|
*
|
package/dist/types/parser.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type * as nodeFs from "node:fs";
|
|
2
|
+
import { type ServiceTierByFamily } from "@oh-my-pi/pi-ai";
|
|
1
3
|
import type { AgentType, MessageStatsInput, SessionEntry, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types.js";
|
|
2
4
|
/**
|
|
3
5
|
* Classify which agent produced a transcript from its path within the sessions
|
|
@@ -44,22 +46,17 @@ export interface UsageBucketView {
|
|
|
44
46
|
export declare function resolveUsageTotal(usage: UsageBucketView | null | undefined): number;
|
|
45
47
|
/** Parse every well-formed entry in a transcript buffer (malformed lines skipped). */
|
|
46
48
|
export declare function parseAllSessionEntries(bytes: Uint8Array): SessionEntry[];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* `fromOffset > 0` we therefore scan the bytes preceding `fromOffset`
|
|
59
|
-
* for the latest service-tier value before parsing the unprocessed tail.
|
|
60
|
-
* The scan only keeps the current tier and does not materialize prefix
|
|
61
|
-
* entries, preserving offset-based memory behavior for large sessions.
|
|
62
|
-
*/
|
|
49
|
+
export interface SessionParserState {
|
|
50
|
+
version: 1;
|
|
51
|
+
offset: number;
|
|
52
|
+
dev: number;
|
|
53
|
+
ino: number;
|
|
54
|
+
birthtimeMs: number;
|
|
55
|
+
size: number;
|
|
56
|
+
mtimeMs: number;
|
|
57
|
+
checkpoint: string;
|
|
58
|
+
serviceTier: ServiceTierByFamily | null;
|
|
59
|
+
}
|
|
63
60
|
export interface ParseSessionResult {
|
|
64
61
|
stats: MessageStatsInput[];
|
|
65
62
|
userStats: UserMessageStats[];
|
|
@@ -67,8 +64,12 @@ export interface ParseSessionResult {
|
|
|
67
64
|
toolCalls: ToolCallStats[];
|
|
68
65
|
toolResults: ToolResultLink[];
|
|
69
66
|
newOffset: number;
|
|
67
|
+
parserState?: SessionParserState;
|
|
68
|
+
reset?: boolean;
|
|
70
69
|
}
|
|
71
|
-
export declare function
|
|
70
|
+
export declare function matchesSessionFile(state: SessionParserState, info: nodeFs.Stats): boolean;
|
|
71
|
+
/** Offset-only callers reconstruct service-tier state once; persisted cursors read only the appended tail. */
|
|
72
|
+
export declare function parseSessionFile(sessionPath: string, fromOffset?: number, state?: SessionParserState, replay?: boolean): Promise<ParseSessionResult>;
|
|
72
73
|
/**
|
|
73
74
|
* List all session directories (folders).
|
|
74
75
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stateless parse worker for `syncAllSessions`. The main thread owns the
|
|
3
|
-
* SQLite handle; workers receive
|
|
3
|
+
* SQLite handle; workers receive a session path, offset, and parser state, run
|
|
4
4
|
* `parseSessionFile` (which is pure I/O + CPU, no DB), and post the
|
|
5
5
|
* structured-clone-safe result back. One in-flight request per worker so
|
|
6
6
|
* the main thread can fan jobs out 1:1 with the pool size.
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* worker actually spawns and runs in compiled binaries (regression coverage
|
|
11
11
|
* for issue #1011 / PR #1027, where the worker silently failed to load).
|
|
12
12
|
*/
|
|
13
|
-
import { type ParseSessionResult } from "./parser.js";
|
|
13
|
+
import { type ParseSessionResult, type SessionParserState } from "./parser.js";
|
|
14
14
|
export type SyncWorkerRequest = {
|
|
15
15
|
kind?: "parse";
|
|
16
16
|
sessionFile: string;
|
|
17
17
|
fromOffset: number;
|
|
18
|
+
parserState?: SessionParserState;
|
|
19
|
+
replay?: boolean;
|
|
18
20
|
} | {
|
|
19
21
|
kind: "ping";
|
|
20
22
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "18.2.
|
|
4
|
+
"version": "18.2.2",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "18.2.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
42
|
+
"@oh-my-pi/pi-ai": "18.2.2",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "18.2.2",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.2.2",
|
|
45
45
|
"@tailwindcss/node": "^4.3.2",
|
|
46
46
|
"chart.js": "^4.5.1",
|
|
47
47
|
"lucide-react": "^1.24.0",
|
package/src/aggregator.ts
CHANGED
|
@@ -3,6 +3,8 @@ import * as path from "node:path";
|
|
|
3
3
|
import { getStatsDbPath, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
|
|
5
5
|
import {
|
|
6
|
+
applySessionParseResult,
|
|
7
|
+
completeSessionSync,
|
|
6
8
|
getRecentErrors as dbGetRecentErrors,
|
|
7
9
|
getRecentRequests as dbGetRecentRequests,
|
|
8
10
|
getBehaviorByModel,
|
|
@@ -26,15 +28,17 @@ import {
|
|
|
26
28
|
getToolStatsByModel,
|
|
27
29
|
getToolTimeSeries,
|
|
28
30
|
initDb,
|
|
29
|
-
insertMessageStats,
|
|
30
|
-
insertToolCalls,
|
|
31
|
-
insertUserMessageStats,
|
|
32
31
|
markSessionBackfillsComplete,
|
|
33
|
-
|
|
34
|
-
updateToolResults,
|
|
35
|
-
updateUserMessageLinks,
|
|
32
|
+
prepareSessionSync,
|
|
36
33
|
} from "./db";
|
|
37
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
getSessionEntry,
|
|
36
|
+
listAllSessionFiles,
|
|
37
|
+
matchesSessionFile,
|
|
38
|
+
type ParseSessionResult,
|
|
39
|
+
parseSessionFile,
|
|
40
|
+
type SessionParserState,
|
|
41
|
+
} from "./parser";
|
|
38
42
|
import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
|
|
39
43
|
// Coding-agent binary/bundle workers route through the CLI entrypoint with a
|
|
40
44
|
// hidden argv mode, so the compiled binary and npm bundle only need one
|
|
@@ -69,20 +73,6 @@ export async function withStatsSyncLock<T>(dbPath: string, fn: () => Promise<T>)
|
|
|
69
73
|
});
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
/**
|
|
73
|
-
* Apply a freshly parsed result to the database. Runs entirely on the
|
|
74
|
-
* main thread so the single SQLite handle owns every write.
|
|
75
|
-
*/
|
|
76
|
-
function applyParseResult(sessionFile: string, lastModified: number, result: ParseSessionResult): number {
|
|
77
|
-
if (result.stats.length > 0) insertMessageStats(result.stats);
|
|
78
|
-
if (result.userStats.length > 0) insertUserMessageStats(result.userStats);
|
|
79
|
-
if (result.userLinks.length > 0) updateUserMessageLinks(result.userLinks);
|
|
80
|
-
if (result.toolCalls.length > 0) insertToolCalls(result.toolCalls);
|
|
81
|
-
if (result.toolResults.length > 0) updateToolResults(result.toolResults);
|
|
82
|
-
setFileOffset(sessionFile, result.newOffset, lastModified);
|
|
83
|
-
return result.stats.length + result.userStats.length;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
/**
|
|
87
77
|
* Progress event emitted after each session file is fully processed.
|
|
88
78
|
* `current` is the number of files completed (skipped + parsed),
|
|
@@ -239,20 +229,34 @@ export async function smokeTestSyncWorker({ timeoutMs = 5_000 }: { timeoutMs?: n
|
|
|
239
229
|
* bar walks at a steady rate).
|
|
240
230
|
*/
|
|
241
231
|
export async function syncAllSessions(opts?: SyncOptions): Promise<{ processed: number; files: number }> {
|
|
242
|
-
return withStatsSyncLock(getStatsDbPath(), () =>
|
|
232
|
+
return withStatsSyncLock(getStatsDbPath(), async () => {
|
|
233
|
+
let processed = 0;
|
|
234
|
+
let files = 0;
|
|
235
|
+
while (true) {
|
|
236
|
+
const result = await syncAllSessionsLocked(opts);
|
|
237
|
+
processed += result.processed;
|
|
238
|
+
files += result.files;
|
|
239
|
+
if (!result.reconcile) return { processed, files };
|
|
240
|
+
}
|
|
241
|
+
});
|
|
243
242
|
}
|
|
244
243
|
|
|
245
|
-
async function syncAllSessionsLocked(
|
|
244
|
+
async function syncAllSessionsLocked(
|
|
245
|
+
opts?: SyncOptions,
|
|
246
|
+
): Promise<{ processed: number; files: number; reconcile: boolean }> {
|
|
246
247
|
await initDb();
|
|
248
|
+
const replay = prepareSessionSync();
|
|
247
249
|
|
|
248
250
|
const files = await listAllSessionFiles();
|
|
249
251
|
let totalProcessed = 0;
|
|
250
252
|
let filesProcessed = 0;
|
|
251
253
|
let completed = 0;
|
|
252
254
|
let cursor = 0;
|
|
255
|
+
let reconcile = false;
|
|
253
256
|
const finish = () => {
|
|
257
|
+
completeSessionSync(reconcile);
|
|
254
258
|
markSessionBackfillsComplete();
|
|
255
|
-
return { processed: totalProcessed, files: filesProcessed };
|
|
259
|
+
return { processed: totalProcessed, files: filesProcessed, reconcile };
|
|
256
260
|
};
|
|
257
261
|
if (files.length === 0) return finish();
|
|
258
262
|
|
|
@@ -268,7 +272,12 @@ async function syncAllSessionsLocked(opts?: SyncOptions): Promise<{ processed: n
|
|
|
268
272
|
|
|
269
273
|
const processFile = async (
|
|
270
274
|
sessionFile: string,
|
|
271
|
-
parse: (
|
|
275
|
+
parse: (
|
|
276
|
+
sessionFile: string,
|
|
277
|
+
fromOffset: number,
|
|
278
|
+
state?: SessionParserState,
|
|
279
|
+
replay?: boolean,
|
|
280
|
+
) => Promise<ParseSessionResult>,
|
|
272
281
|
): Promise<void> => {
|
|
273
282
|
let fileStats: fs.Stats;
|
|
274
283
|
try {
|
|
@@ -279,14 +288,24 @@ async function syncAllSessionsLocked(opts?: SyncOptions): Promise<{ processed: n
|
|
|
279
288
|
}
|
|
280
289
|
const lastModified = fileStats.mtimeMs;
|
|
281
290
|
const stored = getFileOffset(sessionFile);
|
|
282
|
-
if (
|
|
291
|
+
if (
|
|
292
|
+
!replay &&
|
|
293
|
+
stored?.parserState &&
|
|
294
|
+
stored.lastModified === lastModified &&
|
|
295
|
+
stored.parserState.size === fileStats.size &&
|
|
296
|
+
matchesSessionFile(stored.parserState, fileStats)
|
|
297
|
+
) {
|
|
283
298
|
report(sessionFile);
|
|
284
299
|
return;
|
|
285
300
|
}
|
|
286
301
|
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
const
|
|
302
|
+
const unknownIdentity = stored !== null && !stored.parserState;
|
|
303
|
+
const fromOffset = unknownIdentity ? 0 : (stored?.offset ?? 0);
|
|
304
|
+
const result = await parse(sessionFile, fromOffset, stored?.parserState, replay);
|
|
305
|
+
if (unknownIdentity && result.parserState) result.reset = true;
|
|
306
|
+
const applied = applySessionParseResult(sessionFile, result, replay || !stored?.parserState);
|
|
307
|
+
const inserted = applied.processed;
|
|
308
|
+
if (applied.reconcile) reconcile = true;
|
|
290
309
|
if (inserted > 0) {
|
|
291
310
|
totalProcessed += inserted;
|
|
292
311
|
filesProcessed++;
|
|
@@ -312,7 +331,9 @@ async function syncAllSessionsLocked(opts?: SyncOptions): Promise<{ processed: n
|
|
|
312
331
|
const idx = cursor++;
|
|
313
332
|
if (idx >= files.length) return;
|
|
314
333
|
const sessionFile = files[idx];
|
|
315
|
-
await processFile(sessionFile, (file, fromOffset
|
|
334
|
+
await processFile(sessionFile, (file, fromOffset, parserState, replay) =>
|
|
335
|
+
dispatch(handle, { sessionFile: file, fromOffset, parserState, replay }),
|
|
336
|
+
);
|
|
316
337
|
}
|
|
317
338
|
}
|
|
318
339
|
|
package/src/client/styles.css
CHANGED
|
@@ -1509,10 +1509,35 @@
|
|
|
1509
1509
|
border: 1px solid var(--border);
|
|
1510
1510
|
border-radius: var(--radius-md);
|
|
1511
1511
|
color: var(--text);
|
|
1512
|
+
font-family: inherit;
|
|
1512
1513
|
font-size: 12px;
|
|
1513
1514
|
padding: 6px 10px;
|
|
1514
1515
|
}
|
|
1515
1516
|
|
|
1517
|
+
/* Prevent iOS from zooming the viewport when a search input receives focus
|
|
1518
|
+
(mirrors the .stats-select coarse-pointer treatment). */
|
|
1519
|
+
@media (pointer: coarse) {
|
|
1520
|
+
.stats-trace-input {
|
|
1521
|
+
font-size: 16px;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/* Native clear button is a dark glyph in WebKit; keep it visible in the dark
|
|
1526
|
+
theme by inverting it (no-op in light). */
|
|
1527
|
+
.stats-trace-input::-webkit-search-cancel-button {
|
|
1528
|
+
filter: invert(1);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
@media (prefers-color-scheme: light) {
|
|
1532
|
+
:root:not([data-theme]) .stats-trace-input::-webkit-search-cancel-button {
|
|
1533
|
+
filter: none;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
[data-theme="light"] .stats-trace-input::-webkit-search-cancel-button {
|
|
1538
|
+
filter: none;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1516
1541
|
.stats-trace-input:focus-visible {
|
|
1517
1542
|
outline: 2px solid var(--link);
|
|
1518
1543
|
outline-offset: 1px;
|
|
@@ -1532,6 +1557,11 @@
|
|
|
1532
1557
|
user-select: none;
|
|
1533
1558
|
}
|
|
1534
1559
|
|
|
1560
|
+
/* Brand-tint the native checkbox instead of leaving the browser default. */
|
|
1561
|
+
.stats-trace-check input[type="checkbox"] {
|
|
1562
|
+
accent-color: var(--accent);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1535
1565
|
.stats-trace-icon-btn {
|
|
1536
1566
|
display: inline-flex;
|
|
1537
1567
|
align-items: center;
|
package/src/db.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@oh-my-pi/pi-catalog/models";
|
|
10
10
|
import type { ModelCost } from "@oh-my-pi/pi-catalog/types";
|
|
11
11
|
import { getConfigRootDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
|
|
12
|
-
import { classifyAgentType } from "./parser";
|
|
12
|
+
import { classifyAgentType, type ParseSessionResult, type SessionParserState } from "./parser";
|
|
13
13
|
import type {
|
|
14
14
|
AgentType,
|
|
15
15
|
AgentTypeStats,
|
|
@@ -144,7 +144,10 @@ const USER_MESSAGE_LINKS_REPAIR_KEY = "user_message_links_v1";
|
|
|
144
144
|
const PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY = "premium_requests_priority_v1";
|
|
145
145
|
const AGENT_TYPE_BACKFILL_KEY = "agent_type_v1";
|
|
146
146
|
const FORK_DEDUPE_KEY = "fork_dedupe_v1";
|
|
147
|
-
|
|
147
|
+
// v2: tool-name sanitization at ingest (see `sanitizeToolName` in parser.ts)
|
|
148
|
+
// collapses provider-side garbage names; a full re-parse replaces the polluted
|
|
149
|
+
// rows already stored in existing databases.
|
|
150
|
+
const TOOL_CALLS_BACKFILL_KEY = "tool_calls_v2";
|
|
148
151
|
// Older ingests dropped `Usage.orchestration` (never a stored column) when
|
|
149
152
|
// pricing, so subscription models billed on orchestration tokens — multi-agent
|
|
150
153
|
// Grok most notably — were priced from conversation buckets alone and could not
|
|
@@ -222,7 +225,8 @@ export async function initDb(): Promise<Database> {
|
|
|
222
225
|
CREATE TABLE IF NOT EXISTS file_offsets (
|
|
223
226
|
session_file TEXT PRIMARY KEY,
|
|
224
227
|
offset INTEGER NOT NULL,
|
|
225
|
-
last_modified INTEGER NOT NULL
|
|
228
|
+
last_modified INTEGER NOT NULL,
|
|
229
|
+
parser_state TEXT
|
|
226
230
|
);
|
|
227
231
|
|
|
228
232
|
CREATE TABLE IF NOT EXISTS user_messages (
|
|
@@ -274,6 +278,10 @@ export async function initDb(): Promise<Database> {
|
|
|
274
278
|
);
|
|
275
279
|
`);
|
|
276
280
|
|
|
281
|
+
const offsetColumns = db.prepare("PRAGMA table_info(file_offsets)").all() as { name: string }[];
|
|
282
|
+
if (!offsetColumns.some(column => column.name === "parser_state")) {
|
|
283
|
+
db.run("ALTER TABLE file_offsets ADD COLUMN parser_state TEXT");
|
|
284
|
+
}
|
|
277
285
|
const messageColumns = db.prepare("PRAGMA table_info(messages)").all() as { name: string }[];
|
|
278
286
|
if (!messageColumns.some(column => column.name === "premium_requests")) {
|
|
279
287
|
db.run("ALTER TABLE messages ADD COLUMN premium_requests REAL NOT NULL DEFAULT 0");
|
|
@@ -590,26 +598,104 @@ function backfillNoCacheInputCosts(database: Database): void {
|
|
|
590
598
|
/**
|
|
591
599
|
* Get the stored offset for a session file.
|
|
592
600
|
*/
|
|
593
|
-
export function getFileOffset(
|
|
601
|
+
export function getFileOffset(
|
|
602
|
+
sessionFile: string,
|
|
603
|
+
): { offset: number; lastModified: number; parserState?: SessionParserState } | null {
|
|
594
604
|
if (!db) return null;
|
|
595
605
|
|
|
596
|
-
const stmt = db.prepare("SELECT offset, last_modified FROM file_offsets WHERE session_file = ?");
|
|
597
|
-
const row = stmt.get(sessionFile) as
|
|
598
|
-
|
|
599
|
-
|
|
606
|
+
const stmt = db.prepare("SELECT offset, last_modified, parser_state FROM file_offsets WHERE session_file = ?");
|
|
607
|
+
const row = stmt.get(sessionFile) as
|
|
608
|
+
| { offset: number; last_modified: number; parser_state: string | null }
|
|
609
|
+
| undefined;
|
|
610
|
+
if (!row) return null;
|
|
611
|
+
let parserState: SessionParserState | undefined;
|
|
612
|
+
if (row.parser_state) {
|
|
613
|
+
try {
|
|
614
|
+
const state = JSON.parse(row.parser_state) as SessionParserState;
|
|
615
|
+
if (state?.version === 1 && state.offset === row.offset) parserState = state;
|
|
616
|
+
} catch {
|
|
617
|
+
/* A missing cursor is reconstructed from the transcript. */
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return { offset: row.offset, lastModified: row.last_modified, parserState };
|
|
600
621
|
}
|
|
601
622
|
|
|
602
623
|
/**
|
|
603
624
|
* Update the stored offset for a session file.
|
|
604
625
|
*/
|
|
605
|
-
export function setFileOffset(
|
|
626
|
+
export function setFileOffset(
|
|
627
|
+
sessionFile: string,
|
|
628
|
+
offset: number,
|
|
629
|
+
lastModified: number,
|
|
630
|
+
parserState?: SessionParserState,
|
|
631
|
+
): void {
|
|
606
632
|
if (!db) return;
|
|
607
633
|
|
|
608
634
|
const stmt = db.prepare(`
|
|
609
|
-
INSERT OR REPLACE INTO file_offsets (session_file, offset, last_modified)
|
|
610
|
-
VALUES (?, ?, ?)
|
|
635
|
+
INSERT OR REPLACE INTO file_offsets (session_file, offset, last_modified, parser_state)
|
|
636
|
+
VALUES (?, ?, ?, ?)
|
|
611
637
|
`);
|
|
612
|
-
stmt.run(sessionFile, offset, lastModified);
|
|
638
|
+
stmt.run(sessionFile, offset, lastModified, parserState ? JSON.stringify(parserState) : null);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function applySessionParseResult(
|
|
642
|
+
sessionFile: string,
|
|
643
|
+
result: ParseSessionResult,
|
|
644
|
+
rebuild = false,
|
|
645
|
+
): { processed: number; reconcile: boolean } {
|
|
646
|
+
const parserState = result.parserState;
|
|
647
|
+
if (!db || !parserState) return { processed: 0, reconcile: false };
|
|
648
|
+
const database = db;
|
|
649
|
+
return database.transaction(() => {
|
|
650
|
+
let reconcile = result.reset ?? false;
|
|
651
|
+
if (result.reset || rebuild) {
|
|
652
|
+
const retainedMessages = new Set(result.stats.map(row => JSON.stringify([row.entryId, row.timestamp])));
|
|
653
|
+
const retainedUsers = new Set(result.userStats.map(row => JSON.stringify([row.entryId, row.timestamp])));
|
|
654
|
+
const retainedTools = new Set(
|
|
655
|
+
result.toolCalls.map(row => JSON.stringify([row.entryId, row.timestamp, row.toolCallId])),
|
|
656
|
+
);
|
|
657
|
+
const messages = database
|
|
658
|
+
.prepare("SELECT entry_id, timestamp FROM messages WHERE session_file = ?")
|
|
659
|
+
.all(sessionFile) as {
|
|
660
|
+
entry_id: string;
|
|
661
|
+
timestamp: number;
|
|
662
|
+
}[];
|
|
663
|
+
const users = database
|
|
664
|
+
.prepare("SELECT entry_id, timestamp FROM user_messages WHERE session_file = ?")
|
|
665
|
+
.all(sessionFile) as { entry_id: string; timestamp: number }[];
|
|
666
|
+
const tools = database
|
|
667
|
+
.prepare("SELECT entry_id, timestamp, tool_call_id FROM tool_calls WHERE session_file = ?")
|
|
668
|
+
.all(sessionFile) as { entry_id: string; timestamp: number; tool_call_id: string }[];
|
|
669
|
+
// A removed owner may have surviving fork copies skipped earlier in this pass.
|
|
670
|
+
reconcile ||=
|
|
671
|
+
messages.some(row => !retainedMessages.has(JSON.stringify([row.entry_id, row.timestamp]))) ||
|
|
672
|
+
users.some(row => !retainedUsers.has(JSON.stringify([row.entry_id, row.timestamp]))) ||
|
|
673
|
+
tools.some(row => !retainedTools.has(JSON.stringify([row.entry_id, row.timestamp, row.tool_call_id])));
|
|
674
|
+
database.prepare("DELETE FROM messages WHERE session_file = ?").run(sessionFile);
|
|
675
|
+
database.prepare("DELETE FROM user_messages WHERE session_file = ?").run(sessionFile);
|
|
676
|
+
database.prepare("DELETE FROM tool_calls WHERE session_file = ?").run(sessionFile);
|
|
677
|
+
}
|
|
678
|
+
if (reconcile) {
|
|
679
|
+
database
|
|
680
|
+
.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('session_reconciliation', 'pending')")
|
|
681
|
+
.run();
|
|
682
|
+
}
|
|
683
|
+
if (result.stats.length > 0) insertMessageStats(result.stats);
|
|
684
|
+
if (result.userStats.length > 0) insertUserMessageStats(result.userStats);
|
|
685
|
+
if (result.userLinks.length > 0) updateUserMessageLinks(result.userLinks);
|
|
686
|
+
if (result.toolCalls.length > 0) insertToolCalls(result.toolCalls);
|
|
687
|
+
if (result.toolResults.length > 0) updateToolResults(result.toolResults);
|
|
688
|
+
setFileOffset(sessionFile, result.newOffset, parserState.mtimeMs, parserState);
|
|
689
|
+
return { processed: result.stats.length + result.userStats.length, reconcile };
|
|
690
|
+
})();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function prepareSessionSync(): boolean {
|
|
694
|
+
return Boolean(db?.prepare("SELECT 1 FROM meta WHERE key = 'session_reconciliation'").get());
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function completeSessionSync(reconcile: boolean): void {
|
|
698
|
+
if (!reconcile) db?.prepare("DELETE FROM meta WHERE key = 'session_reconciliation'").run();
|
|
613
699
|
}
|
|
614
700
|
|
|
615
701
|
/**
|
package/src/parser.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as nodeFs from "node:fs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import {
|
|
@@ -29,6 +30,9 @@ import { computeUserMessageMetrics } from "./user-metrics";
|
|
|
29
30
|
/** Basename of an advisor agent's transcript inside a session artifacts dir. */
|
|
30
31
|
const ADVISOR_TRANSCRIPT_BASENAME = "__advisor.jsonl";
|
|
31
32
|
|
|
33
|
+
/** Characters a persisted tool name may consist of without sanitization. */
|
|
34
|
+
const TOOL_NAME_PATTERN = /^[\w.:-]+$/;
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Classify which agent produced a transcript from its path within the sessions
|
|
34
38
|
* directory. Layout: `<sessionsDir>/<project>/<file>.jsonl` is the `main`
|
|
@@ -354,27 +358,51 @@ function extractToolCalls(
|
|
|
354
358
|
);
|
|
355
359
|
if (blocks.length === 0) return [];
|
|
356
360
|
|
|
357
|
-
|
|
361
|
+
const calls: ToolCallStats[] = [];
|
|
362
|
+
for (const block of blocks) {
|
|
363
|
+
// Names reduced to nothing by sanitization carry no tool identity:
|
|
364
|
+
// skip them rather than attributing usage to garbage (see
|
|
365
|
+
// sanitizeToolName). callsInTurn still counts the raw block total.
|
|
366
|
+
const toolName = sanitizeToolName(block.name);
|
|
367
|
+
if (toolName === null) continue;
|
|
358
368
|
let argsChars = 0;
|
|
359
369
|
try {
|
|
360
370
|
argsChars = JSON.stringify(block.arguments ?? {}).length;
|
|
361
371
|
} catch {
|
|
362
372
|
// Non-serializable arguments (shouldn't happen in persisted JSONL); size unknown.
|
|
363
373
|
}
|
|
364
|
-
|
|
374
|
+
calls.push({
|
|
365
375
|
sessionFile,
|
|
366
376
|
entryId: entry.id,
|
|
367
377
|
toolCallId: block.id,
|
|
368
378
|
folder,
|
|
369
|
-
toolName
|
|
379
|
+
toolName,
|
|
370
380
|
model: msg.model,
|
|
371
381
|
provider: msg.provider,
|
|
372
382
|
timestamp: coerceEntryTimestamp(msg.timestamp, entry),
|
|
373
383
|
agentType,
|
|
374
384
|
callsInTurn: blocks.length,
|
|
375
385
|
argsChars,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return calls;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Tool names as persisted can be polluted by provider-side parse garbage — a
|
|
393
|
+
* gateway may hand the model's whole invocation text back as the function
|
|
394
|
+
* name (e.g. `bash command="ls -la …"` with a stray in-band closer), which
|
|
395
|
+
* then shows up verbatim in every `GROUP BY tool_name` aggregate and the
|
|
396
|
+
* dashboard tool filter. Reduce such names to their leading identifier token;
|
|
397
|
+
* names that yield no identifier at all carry no tool identity and are
|
|
398
|
+
* returned as `null` so the row is skipped.
|
|
399
|
+
*/
|
|
400
|
+
function sanitizeToolName(name: string): string | null {
|
|
401
|
+
const trimmed = name.trim();
|
|
402
|
+
if (trimmed.length === 0) return null;
|
|
403
|
+
if (TOOL_NAME_PATTERN.test(trimmed)) return trimmed;
|
|
404
|
+
const candidate = trimmed.split(/[^\w.:-]/)[0] ?? "";
|
|
405
|
+
return candidate.length > 0 ? candidate : null;
|
|
378
406
|
}
|
|
379
407
|
|
|
380
408
|
/**
|
|
@@ -454,22 +482,18 @@ function scanLastServiceTier(bytes: Uint8Array): ServiceTierByFamily | undefined
|
|
|
454
482
|
});
|
|
455
483
|
return currentServiceTier;
|
|
456
484
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
* for the latest service-tier value before parsing the unprocessed tail.
|
|
470
|
-
* The scan only keeps the current tier and does not materialize prefix
|
|
471
|
-
* entries, preserving offset-based memory behavior for large sessions.
|
|
472
|
-
*/
|
|
485
|
+
export interface SessionParserState {
|
|
486
|
+
version: 1;
|
|
487
|
+
offset: number;
|
|
488
|
+
dev: number;
|
|
489
|
+
ino: number;
|
|
490
|
+
birthtimeMs: number;
|
|
491
|
+
size: number;
|
|
492
|
+
mtimeMs: number;
|
|
493
|
+
checkpoint: string;
|
|
494
|
+
serviceTier: ServiceTierByFamily | null;
|
|
495
|
+
}
|
|
496
|
+
|
|
473
497
|
export interface ParseSessionResult {
|
|
474
498
|
stats: MessageStatsInput[];
|
|
475
499
|
userStats: UserMessageStats[];
|
|
@@ -477,11 +501,80 @@ export interface ParseSessionResult {
|
|
|
477
501
|
toolCalls: ToolCallStats[];
|
|
478
502
|
toolResults: ToolResultLink[];
|
|
479
503
|
newOffset: number;
|
|
504
|
+
parserState?: SessionParserState;
|
|
505
|
+
reset?: boolean;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const CHECKPOINT_BYTES = 256;
|
|
509
|
+
|
|
510
|
+
async function readCheckpoint(handle: fs.FileHandle, end: number): Promise<Uint8Array> {
|
|
511
|
+
// Positional reads keep the descriptor at zero for Bun.file(fd)'s subsequent tail read.
|
|
512
|
+
const start = Math.max(0, end - CHECKPOINT_BYTES);
|
|
513
|
+
const bytes = new Uint8Array(end - start);
|
|
514
|
+
let read = 0;
|
|
515
|
+
while (read < bytes.length) {
|
|
516
|
+
const result = await handle.read(bytes, read, bytes.length - read, start + read);
|
|
517
|
+
if (result.bytesRead === 0) break;
|
|
518
|
+
read += result.bytesRead;
|
|
519
|
+
}
|
|
520
|
+
return bytes.subarray(0, read);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function matchesSessionFile(state: SessionParserState, info: nodeFs.Stats): boolean {
|
|
524
|
+
return state.dev === info.dev && state.ino === info.ino && state.birthtimeMs === info.birthtimeMs;
|
|
480
525
|
}
|
|
481
|
-
|
|
526
|
+
|
|
527
|
+
/** Offset-only callers reconstruct service-tier state once; persisted cursors read only the appended tail. */
|
|
528
|
+
export async function parseSessionFile(
|
|
529
|
+
sessionPath: string,
|
|
530
|
+
fromOffset = 0,
|
|
531
|
+
state?: SessionParserState,
|
|
532
|
+
replay = false,
|
|
533
|
+
): Promise<ParseSessionResult> {
|
|
482
534
|
let bytes: Uint8Array;
|
|
535
|
+
let start = fromOffset;
|
|
536
|
+
let reset = false;
|
|
537
|
+
let currentServiceTier: ServiceTierByFamily | undefined;
|
|
538
|
+
let info: nodeFs.Stats;
|
|
539
|
+
let checkpoint: string;
|
|
540
|
+
let read: number;
|
|
541
|
+
let entries: SessionEntry[];
|
|
483
542
|
try {
|
|
484
|
-
|
|
543
|
+
const handle = await fs.open(sessionPath, "r");
|
|
544
|
+
try {
|
|
545
|
+
info = await handle.stat();
|
|
546
|
+
const file = Bun.file(handle.fd);
|
|
547
|
+
let resume = state?.version === 1 && state.offset === fromOffset;
|
|
548
|
+
if (resume && state) {
|
|
549
|
+
reset =
|
|
550
|
+
!matchesSessionFile(state, info) ||
|
|
551
|
+
info.size < state.size ||
|
|
552
|
+
(info.size === state.size && info.mtimeMs !== state.mtimeMs);
|
|
553
|
+
if (!reset) {
|
|
554
|
+
const previous = await readCheckpoint(handle, fromOffset);
|
|
555
|
+
reset = Bun.hash(previous).toString(16) !== state.checkpoint;
|
|
556
|
+
}
|
|
557
|
+
resume = !reset;
|
|
558
|
+
}
|
|
559
|
+
if (fromOffset > info.size) reset = true;
|
|
560
|
+
if (replay) resume = false;
|
|
561
|
+
start = reset || replay ? 0 : Math.max(0, fromOffset);
|
|
562
|
+
const readStart = resume ? start : 0;
|
|
563
|
+
bytes = await file.slice(readStart, info.size).bytes();
|
|
564
|
+
currentServiceTier = resume
|
|
565
|
+
? (state?.serviceTier ?? undefined)
|
|
566
|
+
: scanLastServiceTier(bytes.subarray(0, start));
|
|
567
|
+
({ entries, read } = parseSessionEntriesLenient(bytes.subarray(start - readStart)));
|
|
568
|
+
const newOffset = start + read;
|
|
569
|
+
const checkpointStart = Math.max(0, newOffset - CHECKPOINT_BYTES);
|
|
570
|
+
const previous =
|
|
571
|
+
checkpointStart >= readStart
|
|
572
|
+
? bytes.subarray(checkpointStart - readStart, newOffset - readStart)
|
|
573
|
+
: await readCheckpoint(handle, newOffset);
|
|
574
|
+
checkpoint = Bun.hash(previous).toString(16);
|
|
575
|
+
} finally {
|
|
576
|
+
await handle.close();
|
|
577
|
+
}
|
|
485
578
|
} catch (err) {
|
|
486
579
|
if (isEnoent(err))
|
|
487
580
|
return { stats: [], userStats: [], userLinks: [], toolCalls: [], toolResults: [], newOffset: fromOffset };
|
|
@@ -496,13 +589,6 @@ export async function parseSessionFile(sessionPath: string, fromOffset = 0): Pro
|
|
|
496
589
|
const toolCalls: ToolCallStats[] = [];
|
|
497
590
|
const toolResults: ToolResultLink[] = [];
|
|
498
591
|
const userByEntryId = new Map<string, UserMessageStats>();
|
|
499
|
-
const start = Math.max(0, Math.min(fromOffset, bytes.length));
|
|
500
|
-
const unprocessed = bytes.subarray(start);
|
|
501
|
-
const { entries, read } = parseSessionEntriesLenient(unprocessed);
|
|
502
|
-
let currentServiceTier: ServiceTierByFamily | undefined;
|
|
503
|
-
if (start > 0) {
|
|
504
|
-
currentServiceTier = scanLastServiceTier(bytes.subarray(0, start));
|
|
505
|
-
}
|
|
506
592
|
for (const entry of entries) {
|
|
507
593
|
if (isServiceTierChange(entry)) {
|
|
508
594
|
currentServiceTier = coerceServiceTierByFamily(entry.serviceTier);
|
|
@@ -552,7 +638,27 @@ export async function parseSessionFile(sessionPath: string, fromOffset = 0): Pro
|
|
|
552
638
|
}
|
|
553
639
|
}
|
|
554
640
|
|
|
555
|
-
|
|
641
|
+
const newOffset = start + read;
|
|
642
|
+
return {
|
|
643
|
+
stats,
|
|
644
|
+
userStats,
|
|
645
|
+
userLinks,
|
|
646
|
+
toolCalls,
|
|
647
|
+
toolResults,
|
|
648
|
+
newOffset,
|
|
649
|
+
reset,
|
|
650
|
+
parserState: {
|
|
651
|
+
version: 1,
|
|
652
|
+
offset: newOffset,
|
|
653
|
+
dev: info.dev,
|
|
654
|
+
ino: info.ino,
|
|
655
|
+
birthtimeMs: info.birthtimeMs,
|
|
656
|
+
size: info.size,
|
|
657
|
+
mtimeMs: info.mtimeMs,
|
|
658
|
+
checkpoint,
|
|
659
|
+
serviceTier: currentServiceTier ?? null,
|
|
660
|
+
},
|
|
661
|
+
};
|
|
556
662
|
}
|
|
557
663
|
|
|
558
664
|
/**
|
package/src/port-conflict.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const STATS_DASHBOARD_SECURITY_VERSION = "3";
|
|
|
26
26
|
/** IPv4 loopback address shared by the dashboard server and reuse probe. */
|
|
27
27
|
export const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
|
|
28
28
|
|
|
29
|
-
type StatsDashboardProbe = "reusable" | "occupied" | "unreachable";
|
|
29
|
+
type StatsDashboardProbe = "reusable" | "replaceable" | "occupied" | "unreachable";
|
|
30
30
|
|
|
31
31
|
async function probeStatsDashboard(port: number, hostname: string): Promise<StatsDashboardProbe> {
|
|
32
32
|
const probeHostname = hostname === "0.0.0.0" ? STATS_DASHBOARD_HOSTNAME : hostname === "::" ? "::1" : hostname;
|
|
@@ -35,13 +35,22 @@ async function probeStatsDashboard(port: number, hostname: string): Promise<Stat
|
|
|
35
35
|
const response = await fetch(`http://${urlHostname}:${port}/api/stats/models`, {
|
|
36
36
|
signal: AbortSignal.timeout(STATS_PROBE_TIMEOUT_MS),
|
|
37
37
|
});
|
|
38
|
+
const dashboardVersionHeader = response.headers.get(STATS_DASHBOARD_HEADER);
|
|
39
|
+
const dashboardVersion = dashboardVersionHeader === null ? Number.NaN : Number(dashboardVersionHeader);
|
|
40
|
+
// Never replace a newer dashboard: an older CLI must not downgrade it.
|
|
41
|
+
const replaceable =
|
|
42
|
+
response.status === 200 &&
|
|
43
|
+
Number.isSafeInteger(dashboardVersion) &&
|
|
44
|
+
dashboardVersion > 0 &&
|
|
45
|
+
dashboardVersion <= Number(STATS_DASHBOARD_SECURITY_VERSION);
|
|
38
46
|
const reusable =
|
|
39
47
|
response.status === 200 &&
|
|
40
|
-
|
|
48
|
+
dashboardVersionHeader === STATS_DASHBOARD_SECURITY_VERSION &&
|
|
41
49
|
response.headers.get(STATS_DASHBOARD_HOSTNAME_HEADER) === hostname &&
|
|
42
50
|
!response.headers.has("Access-Control-Allow-Origin");
|
|
43
51
|
await response.body?.cancel();
|
|
44
|
-
|
|
52
|
+
if (reusable) return "reusable";
|
|
53
|
+
return replaceable ? "replaceable" : "occupied";
|
|
45
54
|
} catch {
|
|
46
55
|
return "unreachable";
|
|
47
56
|
}
|
|
@@ -218,7 +227,7 @@ async function terminatePortHolder(holder: PortHolder): Promise<void> {
|
|
|
218
227
|
await Bun.sleep(PROCESS_EXIT_POLL_MS);
|
|
219
228
|
}
|
|
220
229
|
|
|
221
|
-
async function reclaimStatsPort(port: number): Promise<"retry"> {
|
|
230
|
+
async function reclaimStatsPort(port: number, hasDashboardIdentity = false): Promise<"retry"> {
|
|
222
231
|
const holder = await findPortHolder(port);
|
|
223
232
|
if (!holder) {
|
|
224
233
|
throw new Error(`Port ${port} is in use, but the listening process could not be identified.`);
|
|
@@ -238,7 +247,7 @@ async function reclaimStatsPort(port: number): Promise<"retry"> {
|
|
|
238
247
|
/\/packages\/stats\/src\/index\.ts(?:["'\s]|$)/.test(normalizedCommand) ||
|
|
239
248
|
(normalizedImage === "omp" && /(?:^|\s)stats(?:\s|$)/.test(normalizedCommand)) ||
|
|
240
249
|
/(?:^|\/)omp(?:\.exe)?["'\s]+stats(?:["'\s]|$)/.test(normalizedCommand);
|
|
241
|
-
if (!STATS_RUNTIME_IMAGES[normalizedImage] || !hasStatsIdentity) {
|
|
250
|
+
if (!STATS_RUNTIME_IMAGES[normalizedImage] || (!hasStatsIdentity && !hasDashboardIdentity)) {
|
|
242
251
|
throw new Error(
|
|
243
252
|
`Port ${port} is in use by ${holder.image} (PID ${holder.pid}), which is not identifiable as an omp stats dashboard; refusing to stop it.`,
|
|
244
253
|
);
|
|
@@ -257,12 +266,14 @@ export async function prepareStatsPort(port: number, hostname = STATS_DASHBOARD_
|
|
|
257
266
|
if (port === 0) return "retry";
|
|
258
267
|
const probe = await probeStatsDashboard(port, hostname);
|
|
259
268
|
if (probe === "reusable") return "reuse";
|
|
269
|
+
if (probe === "replaceable") return reclaimStatsPort(port, true);
|
|
260
270
|
if (probe === "occupied") return reclaimStatsPort(port);
|
|
261
271
|
return "retry";
|
|
262
272
|
}
|
|
263
273
|
|
|
264
274
|
/** Reuse or reclaim a listener found after the server bind reports EADDRINUSE. */
|
|
265
275
|
export async function recoverStatsPort(port: number, hostname = STATS_DASHBOARD_HOSTNAME): Promise<"retry" | "reuse"> {
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
const probe = await probeStatsDashboard(port, hostname);
|
|
277
|
+
if (probe === "reusable") return "reuse";
|
|
278
|
+
return reclaimStatsPort(port, probe === "replaceable");
|
|
268
279
|
}
|
package/src/sync-worker.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stateless parse worker for `syncAllSessions`. The main thread owns the
|
|
3
|
-
* SQLite handle; workers receive
|
|
3
|
+
* SQLite handle; workers receive a session path, offset, and parser state, run
|
|
4
4
|
* `parseSessionFile` (which is pure I/O + CPU, no DB), and post the
|
|
5
5
|
* structured-clone-safe result back. One in-flight request per worker so
|
|
6
6
|
* the main thread can fan jobs out 1:1 with the pool size.
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* for issue #1011 / PR #1027, where the worker silently failed to load).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { type ParseSessionResult, parseSessionFile } from "./parser";
|
|
14
|
+
import { type ParseSessionResult, parseSessionFile, type SessionParserState } from "./parser";
|
|
15
15
|
|
|
16
|
-
export type SyncWorkerRequest =
|
|
16
|
+
export type SyncWorkerRequest =
|
|
17
|
+
| { kind?: "parse"; sessionFile: string; fromOffset: number; parserState?: SessionParserState; replay?: boolean }
|
|
18
|
+
| { kind: "ping" };
|
|
17
19
|
|
|
18
20
|
export type SyncWorkerResponse =
|
|
19
21
|
| { ok: true; kind?: "parse"; result: ParseSessionResult }
|
|
@@ -31,7 +33,12 @@ self.onmessage = async event => {
|
|
|
31
33
|
self.postMessage({ ok: true, kind: "pong" } satisfies SyncWorkerResponse);
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
|
-
const result = await parseSessionFile(
|
|
36
|
+
const result = await parseSessionFile(
|
|
37
|
+
request.sessionFile,
|
|
38
|
+
request.fromOffset,
|
|
39
|
+
request.parserState,
|
|
40
|
+
request.replay,
|
|
41
|
+
);
|
|
35
42
|
self.postMessage({ ok: true, result } satisfies SyncWorkerResponse);
|
|
36
43
|
} catch (err) {
|
|
37
44
|
const error = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
package/src/trace.ts
CHANGED
|
@@ -854,22 +854,54 @@ export async function getTraceEntry(fileParam: string, entryId: string): Promise
|
|
|
854
854
|
// ---------------------------------------------------------------------------
|
|
855
855
|
// Session list
|
|
856
856
|
|
|
857
|
-
|
|
857
|
+
interface SessionMetadata {
|
|
858
|
+
title: string | null;
|
|
859
|
+
cwd: string | null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const metadataCache = new Map<string, SessionMetadata & { mtimeMs: number }>();
|
|
858
863
|
|
|
859
|
-
/** Read
|
|
860
|
-
async function
|
|
864
|
+
/** Read enough decompressed transcript bytes to cover the title slot and session header. */
|
|
865
|
+
async function readSessionMetadataHead(file: string): Promise<string> {
|
|
866
|
+
if (!file.endsWith(".gz")) return Bun.file(file).slice(0, TITLE_SCAN_BYTES).text();
|
|
867
|
+
|
|
868
|
+
const reader = Bun.file(file).stream().pipeThrough(new DecompressionStream("gzip")).getReader();
|
|
869
|
+
const decoder = new TextDecoder();
|
|
870
|
+
let remaining = TITLE_SCAN_BYTES;
|
|
871
|
+
let head = "";
|
|
872
|
+
try {
|
|
873
|
+
while (remaining > 0) {
|
|
874
|
+
const { done, value } = await reader.read();
|
|
875
|
+
if (done) {
|
|
876
|
+
head += decoder.decode();
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
const chunk = value.subarray(0, remaining);
|
|
880
|
+
remaining -= chunk.byteLength;
|
|
881
|
+
head += decoder.decode(chunk, { stream: remaining > 0 && chunk.byteLength === value.byteLength });
|
|
882
|
+
if (chunk.byteLength < value.byteLength) break;
|
|
883
|
+
}
|
|
884
|
+
} finally {
|
|
885
|
+
await reader.cancel();
|
|
886
|
+
}
|
|
887
|
+
return head;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/** Read list metadata from the fixed title slot and session header. */
|
|
891
|
+
async function readSessionMetadata(file: string): Promise<SessionMetadata> {
|
|
861
892
|
let mtimeMs: number;
|
|
862
893
|
try {
|
|
863
894
|
mtimeMs = (await fs.stat(file)).mtimeMs;
|
|
864
895
|
} catch {
|
|
865
|
-
return null;
|
|
896
|
+
return { title: null, cwd: null };
|
|
866
897
|
}
|
|
867
|
-
const cached =
|
|
868
|
-
if (cached && cached.mtimeMs === mtimeMs) return cached
|
|
898
|
+
const cached = metadataCache.get(file);
|
|
899
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached;
|
|
869
900
|
|
|
870
901
|
let title: string | null = null;
|
|
902
|
+
let cwd: string | null = null;
|
|
871
903
|
try {
|
|
872
|
-
const head = await
|
|
904
|
+
const head = await readSessionMetadataHead(file);
|
|
873
905
|
for (const line of head.split("\n")) {
|
|
874
906
|
if (!line.trim()) continue;
|
|
875
907
|
let parsed: EntryView;
|
|
@@ -881,18 +913,20 @@ async function readSessionTitle(file: string): Promise<string | null> {
|
|
|
881
913
|
}
|
|
882
914
|
if (parsed.type === "title" && typeof parsed.title === "string") {
|
|
883
915
|
title = parsed.title;
|
|
884
|
-
|
|
916
|
+
continue;
|
|
885
917
|
}
|
|
886
918
|
if (parsed.type === "session") {
|
|
887
|
-
if (typeof parsed.title === "string") title = parsed.title;
|
|
919
|
+
if (title === null && typeof parsed.title === "string") title = parsed.title;
|
|
920
|
+
if (typeof parsed.cwd === "string") cwd = parsed.cwd;
|
|
888
921
|
break;
|
|
889
922
|
}
|
|
890
923
|
}
|
|
891
924
|
} catch {
|
|
892
|
-
// Unreadable head (gc'd, gz, permission) → keep the row
|
|
925
|
+
// Unreadable head (gc'd, gz, permission) → keep the row with path-derived metadata.
|
|
893
926
|
}
|
|
894
|
-
|
|
895
|
-
|
|
927
|
+
const metadata = { mtimeMs, title, cwd };
|
|
928
|
+
metadataCache.set(file, metadata);
|
|
929
|
+
return metadata;
|
|
896
930
|
}
|
|
897
931
|
|
|
898
932
|
interface SummaryFold extends SessionSummary {
|
|
@@ -1027,7 +1061,9 @@ export async function listSessionSummaries(limit = 100, q?: string): Promise<Ses
|
|
|
1027
1061
|
const folds = [...byRoot.values()].sort((a, b) => b.endedAt - a.endedAt).slice(0, LIST_FOLD_LIMIT);
|
|
1028
1062
|
await Promise.all(
|
|
1029
1063
|
folds.map(async fold => {
|
|
1030
|
-
|
|
1064
|
+
const metadata = await readSessionMetadata(fold.file);
|
|
1065
|
+
fold.title = metadata.title;
|
|
1066
|
+
fold.folder = metadata.cwd ?? extractFolderFromPath(fold.file);
|
|
1031
1067
|
fold.models = [...fold.modelSet].sort();
|
|
1032
1068
|
}),
|
|
1033
1069
|
);
|