fleetlens 0.13.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +4 -1
  3. package/app/apps/web/.next/build-manifest.json +3 -3
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +21 -3
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  14. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/app/apps/web/.next/server/app/api/ask/route.js +3 -3
  17. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +4 -4
  19. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +4 -4
  21. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +2 -2
  23. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +4 -4
  25. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +2 -2
  27. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +4 -4
  29. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/settings/route.js +4 -3
  33. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/team/autostart/route/app-paths-manifest.json +3 -0
  35. package/app/apps/web/.next/server/app/api/team/autostart/route.js +7 -0
  36. package/app/apps/web/.next/server/app/api/team/autostart/route.js.nft.json +1 -0
  37. package/app/apps/web/.next/server/app/api/team/autostart/route_client-reference-manifest.js +3 -0
  38. package/app/apps/web/.next/server/app/api/team/onboarding/start/route/app-paths-manifest.json +3 -0
  39. package/app/apps/web/.next/server/app/api/team/onboarding/start/route/build-manifest.json +9 -0
  40. package/app/apps/web/.next/server/app/api/team/onboarding/start/route/server-reference-manifest.json +4 -0
  41. package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js +8 -0
  42. package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js.map +5 -0
  43. package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js.nft.json +1 -0
  44. package/app/apps/web/.next/server/app/api/team/onboarding/start/route_client-reference-manifest.js +3 -0
  45. package/app/apps/web/.next/server/app/api/team/sync-projects/route/app-paths-manifest.json +3 -0
  46. package/app/apps/web/.next/server/app/api/team/sync-projects/route/build-manifest.json +9 -0
  47. package/app/apps/web/.next/server/app/api/team/sync-projects/route/server-reference-manifest.json +4 -0
  48. package/app/apps/web/.next/server/app/api/team/sync-projects/route.js +8 -0
  49. package/app/apps/web/.next/server/app/api/team/sync-projects/route.js.map +5 -0
  50. package/app/apps/web/.next/server/app/api/team/sync-projects/route.js.nft.json +1 -0
  51. package/app/apps/web/.next/server/app/api/team/sync-projects/route_client-reference-manifest.js +3 -0
  52. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  53. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  54. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  55. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  56. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  57. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  58. package/app/apps/web/.next/server/app/day/page.js +1 -1
  59. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  60. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  61. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  62. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  63. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  64. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  65. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  66. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  67. package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
  68. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  69. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  70. package/app/apps/web/.next/server/app/insights/page.js +1 -1
  71. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  72. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  73. package/app/apps/web/.next/server/app/page.js +2 -2
  74. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  75. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  76. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  77. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  78. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  79. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  80. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  81. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  82. package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -2
  83. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  84. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  85. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  86. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  87. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  88. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  89. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  90. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  91. package/app/apps/web/.next/server/app/sessions/[id]/page.js +3 -3
  92. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  93. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  94. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  95. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  96. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  97. package/app/apps/web/.next/server/app/settings/page.js +2 -2
  98. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  99. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  100. package/app/apps/web/.next/server/app/team/onboarding/page/app-paths-manifest.json +3 -0
  101. package/app/apps/web/.next/server/app/team/onboarding/page/build-manifest.json +17 -0
  102. package/app/apps/web/.next/server/app/team/onboarding/page/next-font-manifest.json +6 -0
  103. package/app/apps/web/.next/server/app/team/onboarding/page/react-loadable-manifest.json +1 -0
  104. package/app/apps/web/.next/server/app/team/onboarding/page/server-reference-manifest.json +4 -0
  105. package/app/apps/web/.next/server/app/team/onboarding/page.js +19 -0
  106. package/app/apps/web/.next/server/app/team/onboarding/page.js.map +5 -0
  107. package/app/apps/web/.next/server/app/team/onboarding/page.js.nft.json +1 -0
  108. package/app/apps/web/.next/server/app/team/onboarding/page_client-reference-manifest.js +3 -0
  109. package/app/apps/web/.next/server/app/team/page.js +3 -2
  110. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  111. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  112. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  113. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  114. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  115. package/app/apps/web/.next/server/app-paths-manifest.json +4 -1
  116. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_team_onboarding_start_route_actions_0zeptl_.js +3 -0
  117. package/app/apps/web/.next/server/chunks/0fuv_zod_v4_classic_external_0.z.vap.js +39 -0
  118. package/app/apps/web/.next/server/chunks/[root-of-the-server]__038-.1r._.js +6 -0
  119. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0~utv~v._.js → [root-of-the-server]__03ewxxu._.js} +2 -2
  120. package/app/apps/web/.next/server/chunks/[root-of-the-server]__03rmbz6._.js +3 -0
  121. package/app/apps/web/.next/server/chunks/[root-of-the-server]__065lib8._.js +3 -0
  122. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0e.f-t.._.js → [root-of-the-server]__0a0~7d6._.js} +2 -2
  123. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0gril0u._.js → [root-of-the-server]__0acikq.._.js} +2 -2
  124. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0et2sqb._.js +3 -0
  125. package/app/apps/web/.next/server/chunks/{[externals]__09v47rg._.js → [root-of-the-server]__0jbc7tz._.js} +2 -2
  126. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0mq0or2._.js +3 -0
  127. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0mvl0qn._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0n79hpm._.js → [root-of-the-server]__0ow1kn1._.js} +4 -4
  129. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~xt99s._.js +3 -0
  130. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_team_autostart_route_actions_10-4s71.js +3 -0
  131. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_team_sync-projects_route_actions_0r_2f5n.js +3 -0
  132. package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +13 -0
  133. package/app/apps/web/.next/server/chunks/packages_09wmnv.._.js +3 -0
  134. package/app/apps/web/.next/server/chunks/packages_0o6.gpk._.js +3 -0
  135. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  136. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  137. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0z7mg0b._.js → [root-of-the-server]__08l86e-._.js} +2 -2
  138. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0a7n44-._.js +3 -0
  139. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0c00i76._.js +3 -0
  140. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0i.q-1h._.js +3 -0
  141. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
  142. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0stu~~r._.js +3 -0
  143. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ufi3.w._.js +3 -0
  144. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0o03ymy._.js → [root-of-the-server]__10~b4bf._.js} +2 -2
  145. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  146. package/app/apps/web/.next/server/chunks/ssr/apps_web_0.97l5.._.js +3 -0
  147. package/app/apps/web/.next/server/chunks/ssr/{apps_web_app_settings_0u6z6rj._.js → apps_web_0q2x4a~._.js} +2 -2
  148. package/app/apps/web/.next/server/chunks/ssr/apps_web_114yo0v._.js +3 -0
  149. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_team_onboarding_page_actions_0pss~.k.js +3 -0
  150. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0c-tpft._.js +1 -1
  151. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
  152. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  153. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  154. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_team-config_ts_10egg08._.js +39 -0
  155. package/app/apps/web/.next/server/chunks/ssr/{node_modules_next_dist_0t-hj0x._.js → node_modules_0yrkyef._.js} +2 -2
  156. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js +2 -2
  157. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0jxvcfi.js +2 -2
  158. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0s5yhs4.js +4 -0
  159. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  160. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  161. package/app/apps/web/.next/server/functions-config-manifest.json +3 -1
  162. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  163. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  164. package/app/apps/web/.next/server/pages/500.html +1 -1
  165. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  166. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  167. package/app/apps/web/.next/static/chunks/{0wwcx42ng8i2e.js → 0-3_u.90d79wi.js} +1 -1
  168. package/app/apps/web/.next/static/chunks/00a9oyw~h5i1p.js +1 -0
  169. package/app/apps/web/.next/static/chunks/040s42ypidk7s.css +1 -0
  170. package/app/apps/web/.next/static/chunks/06eiwghjkkbnh.js +1 -0
  171. package/app/apps/web/.next/static/chunks/06qeg4pt5jdqs.js +1 -0
  172. package/app/apps/web/.next/static/chunks/{0e_fzy0n8ij-o.js → 0u6gxpqhl0gc7.js} +1 -1
  173. package/app/apps/web/.next/static/chunks/18cj08-la37o-.js +1 -0
  174. package/app/apps/web/app/api/team/autostart/route.ts +41 -0
  175. package/app/apps/web/app/api/team/onboarding/start/route.ts +96 -0
  176. package/app/apps/web/app/api/team/sync-projects/route.ts +39 -0
  177. package/app/apps/web/app/globals.css +82 -0
  178. package/app/apps/web/app/settings/ai-features-form.tsx +1 -1
  179. package/app/apps/web/app/settings/page.tsx +21 -7
  180. package/app/apps/web/app/{settings → team}/force-sync-button.tsx +2 -4
  181. package/app/apps/web/app/team/onboarding/onboarding-wizard.tsx +503 -0
  182. package/app/apps/web/app/team/onboarding/page.tsx +28 -0
  183. package/app/apps/web/app/team/page.tsx +60 -81
  184. package/app/apps/web/app/{settings/team-connection-section.tsx → team/sync-activity-section.tsx} +6 -23
  185. package/app/apps/web/app/team/synced-projects-form.tsx +201 -0
  186. package/app/apps/web/app/team/synced-projects-section.tsx +37 -0
  187. package/app/apps/web/components/autostart-toggle.tsx +59 -0
  188. package/app/apps/web/components/previous-cycles-trend.tsx +4 -1
  189. package/app/apps/web/components/project-sync-picker.tsx +109 -0
  190. package/app/apps/web/components/team-chip.tsx +1 -1
  191. package/app/apps/web/components/team-welcome-banner.tsx +1 -1
  192. package/app/apps/web/lib/autostart-data.ts +31 -0
  193. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  194. package/app/apps/web/lib/sync-projects-data.ts +36 -0
  195. package/app/apps/web/lib/team-config.ts +21 -4
  196. package/app/apps/web/package.json +1 -1
  197. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  198. package/dist/daemon-worker.js +2089 -1622
  199. package/dist/index.js +1462 -230
  200. package/package.json +1 -1
  201. package/app/apps/web/.next/server/app/api/team-config/route/app-paths-manifest.json +0 -3
  202. package/app/apps/web/.next/server/app/api/team-config/route.js +0 -8
  203. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +0 -1
  204. package/app/apps/web/.next/server/app/api/team-config/route_client-reference-manifest.js +0 -3
  205. package/app/apps/web/.next/server/chunks/[root-of-the-server]__08agijy._.js +0 -3
  206. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0atn.dt._.js +0 -3
  207. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0b.b7~1._.js +0 -3
  208. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0i06ito._.js +0 -3
  209. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0y5p73y._.js +0 -3
  210. package/app/apps/web/.next/server/chunks/_0euqdko._.js +0 -49
  211. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_team-config_route_actions_0ad2gak.js +0 -3
  212. package/app/apps/web/.next/server/chunks/packages_0c-sz9n._.js +0 -3
  213. package/app/apps/web/.next/server/chunks/packages_0wywuu.._.js +0 -3
  214. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__09yxy53._.js +0 -3
  215. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +0 -3
  216. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +0 -3
  217. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0q9qy0z._.js +0 -3
  218. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_team_team-sync-form_tsx_0choz0h._.js +0 -3
  219. package/app/apps/web/.next/static/chunks/083~hzkv577.5.css +0 -1
  220. package/app/apps/web/.next/static/chunks/0h.ua.-myj.6z.js +0 -1
  221. package/app/apps/web/.next/static/chunks/0uyctl-m8iydb.js +0 -1
  222. package/app/apps/web/.next/static/chunks/14joqbsr9ihhu.js +0 -1
  223. package/app/apps/web/app/api/team-config/route.ts +0 -39
  224. package/app/apps/web/app/team/team-sync-form.tsx +0 -144
  225. /package/app/apps/web/.next/server/app/api/{team-config → team/autostart}/route/build-manifest.json +0 -0
  226. /package/app/apps/web/.next/server/app/api/{team-config → team/autostart}/route/server-reference-manifest.json +0 -0
  227. /package/app/apps/web/.next/server/app/api/{team-config → team/autostart}/route.js.map +0 -0
  228. /package/app/apps/web/.next/static/{bZs5UyzsYBqC6MFY5n-M_ → C4y6rare_ZCcXXQLc8ysA}/_buildManifest.js +0 -0
  229. /package/app/apps/web/.next/static/{bZs5UyzsYBqC6MFY5n-M_ → C4y6rare_ZCcXXQLc8ysA}/_clientMiddlewareManifest.js +0 -0
  230. /package/app/apps/web/.next/static/{bZs5UyzsYBqC6MFY5n-M_ → C4y6rare_ZCcXXQLc8ysA}/_ssgManifest.js +0 -0
@@ -1,19 +1,15 @@
1
1
  /**
2
- * Team-sync consent page.
2
+ * Team-sync status page.
3
3
  *
4
- * Shows the user which team their daemon is paired with, what data flows
5
- * over the wire on the next push, and lets them toggle the two opt-outs
6
- * the bridge respects: per-project privacy and LLM-enriched extras.
7
- *
8
- * The page works against the same on-disk team-config.json the daemon
9
- * reads, so a change here propagates to the next `fleetlens team push`.
4
+ * Shows which team this machine's daemon is paired with and what data flows
5
+ * over the wire on the next push. All LLM-enriched fields are shared for
6
+ * every synced project; which projects are synced is the member's own
7
+ * choice (Settings' Synced-projects selection, seeded by the onboarding
8
+ * wizard). The page reads the same on-disk team-config.json the daemon reads.
10
9
  */
11
- import Link from "next/link";
12
- import { groupByProject, type SessionMeta } from "@claude-lens/parser";
13
- import { listSessions } from "@/lib/data";
14
10
  import { readTeamConfig, toTeamConfigView } from "@/lib/team-config";
15
- import { TeamSyncForm } from "./team-sync-form";
16
- import { prettyProjectName } from "@/lib/format";
11
+ import { SyncedProjectsSection } from "./synced-projects-section";
12
+ import { SyncActivitySection } from "./sync-activity-section";
17
13
 
18
14
  export const dynamic = "force-dynamic";
19
15
 
@@ -36,33 +32,14 @@ function lastSyncedSnapshotLabel(iso: string | undefined): string {
36
32
  return new Date(ms).toLocaleString();
37
33
  }
38
34
 
39
- function recentDaysFilter<T extends SessionMeta>(sessions: T[], days: number): T[] {
40
- const cutoff = Date.now() - days * 86_400_000;
41
- return sessions.filter((s) => {
42
- if (!s.firstTimestamp) return false;
43
- const ms = Date.parse(s.firstTimestamp);
44
- return !Number.isNaN(ms) && ms >= cutoff;
45
- });
46
- }
47
-
48
- export default async function TeamPage() {
35
+ export default function TeamPage() {
49
36
  const config = readTeamConfig();
50
- const allSessions = await listSessions();
51
- const recent = recentDaysFilter(allSessions, 30);
52
- const projects = groupByProject(recent)
53
- .map((p) => ({
54
- name: p.projectName,
55
- pretty: prettyProjectName(p.projectName),
56
- sessions: p.sessions.length,
57
- worktreeCount: p.worktreeCount,
58
- }))
59
- .sort((a, b) => b.sessions - a.sessions);
60
37
 
61
38
  if (!config) {
62
39
  return (
63
40
  <main className="mx-auto max-w-3xl p-6 space-y-6">
64
41
  <h1 className="text-2xl font-semibold">Team sync</h1>
65
- <section className="border border-dashed rounded-lg p-6 text-sm space-y-3">
42
+ <section className="af-card border-dashed text-sm space-y-3">
66
43
  <p className="font-medium">Not paired with any team.</p>
67
44
  <p className="text-gray-600">
68
45
  To pair this machine with a Fleetlens team server, run from a
@@ -81,20 +58,30 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
81
58
  }
82
59
 
83
60
  const view = toTeamConfigView(config);
84
- const privateProjects = new Set(config.privateProjects ?? []);
85
- const enrichmentOptIn = !!config.enrichmentOptIn;
86
- const sharedCount = projects.filter((p) => !privateProjects.has(p.name)).length;
61
+ const sp = config.syncProjects;
62
+ const syncSummary = sp
63
+ ? sp.autoIncludeNew
64
+ ? `Syncing: all projects except ${sp.excluded.length} excluded`
65
+ : `Syncing: only ${sp.included.length} selected project${sp.included.length === 1 ? "" : "s"}`
66
+ : null;
87
67
 
88
68
  return (
89
69
  <main className="mx-auto max-w-3xl p-6 space-y-8">
90
70
  <header>
91
71
  <h1 className="text-2xl font-semibold">Team sync</h1>
92
72
  <p className="text-sm text-gray-500 mt-1">
93
- Manage what your daemon shares with your team server.
73
+ What your daemon shares with your team server.
94
74
  </p>
95
75
  </header>
96
76
 
97
- <section className="border rounded-lg p-5 space-y-3">
77
+ {config.setupPending && (
78
+ <div className="border border-amber-400/40 bg-amber-400/10 rounded-lg p-4 text-sm">
79
+ Setup isn&rsquo;t finished — nothing is syncing yet.{" "}
80
+ <a className="underline font-medium" href="/team/onboarding">Finish onboarding</a>
81
+ </div>
82
+ )}
83
+
84
+ <section className="af-card space-y-3">
98
85
  <div className="flex items-baseline justify-between">
99
86
  <h2 className="text-lg font-medium">Pairing</h2>
100
87
  <span className="text-xs uppercase tracking-wide text-green-700 dark:text-green-400">
@@ -105,7 +92,11 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
105
92
  <dt className="text-gray-500">Team slug</dt>
106
93
  <dd className="font-mono">{view.teamSlug}</dd>
107
94
  <dt className="text-gray-500">Server</dt>
108
- <dd className="font-mono break-all">{view.serverUrl}</dd>
95
+ <dd className="font-mono break-all">
96
+ <a href={view.serverUrl} target="_blank" rel="noreferrer" className="hover:underline">
97
+ {view.serverUrl} <span aria-hidden>↗</span>
98
+ </a>
99
+ </dd>
109
100
  <dt className="text-gray-500">Member ID</dt>
110
101
  <dd className="font-mono break-all text-xs">{view.memberId}</dd>
111
102
  <dt className="text-gray-500">Bearer token</dt>
@@ -119,65 +110,53 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
119
110
  </dl>
120
111
  </section>
121
112
 
122
- <section className="border rounded-lg p-5 space-y-4">
123
- <div>
124
- <h2 className="text-lg font-medium">What gets shared</h2>
125
- <p className="text-sm text-gray-500 mt-1">
126
- Toggles below take effect on the daemon&rsquo;s next push (usually
127
- within 5 minutes). See{" "}
128
- <Link
129
- href="https://github.com/cowcow02/fleetlens"
130
- className="underline"
131
- >
132
- the bridge design spec
133
- </Link>{" "}
134
- for the full data inventory.
135
- </p>
136
- </div>
113
+ <section className="af-card">
114
+ <SyncActivitySection />
115
+ </section>
137
116
 
138
- <TeamSyncForm
139
- initial={{ enrichmentOptIn, privateProjects: Array.from(privateProjects) }}
140
- projects={projects}
141
- />
117
+ <section className="af-card">
118
+ <SyncedProjectsSection />
142
119
  </section>
143
120
 
144
- <section className="border rounded-lg p-5 space-y-3">
145
- <h2 className="text-lg font-medium">Next push will carry</h2>
121
+ <section className="af-card space-y-3">
122
+ <h2 className="text-lg font-medium">What gets shared</h2>
123
+ <p className="text-sm text-gray-500">
124
+ {syncSummary ?? "Syncing: all projects (no selection set)."}
125
+ </p>
146
126
  <ul className="text-sm space-y-2">
147
127
  <li>
148
128
  <strong>Daily rollup</strong> — agent time, session count, tool
149
- calls, turns, token totals. Always shared.
129
+ calls, turns, token totals.
150
130
  </li>
151
131
  <li>
152
- <strong>Rich rollup</strong> (Entry-derived counts) for{" "}
153
- <strong>{sharedCount}</strong> of {projects.length} active projects:
154
- working-shape distribution, top skills, subagents dispatched,
155
- long-autonomous turns, plan-mode usage, brainstorm warmups, tool
156
- errors, PRs/commits/pushes. Project labels only; never raw
157
- prompts or agent output.
132
+ <strong>Rich rollup</strong> (Entry-derived counts) for each
133
+ synced project: working-shape distribution, top skills, subagents
134
+ dispatched, long-autonomous turns, plan-mode usage, brainstorm
135
+ warmups, tool errors, PRs/commits/pushes. Project labels only;
136
+ never raw prompts or agent output.
158
137
  </li>
159
138
  <li>
160
139
  <strong>Plan utilization snapshot</strong> — current 5h + 7d
161
- utilization windows, per Anthropic&rsquo;s rate-limit API. Always
162
- shared when fresh.
140
+ utilization windows, per Anthropic&rsquo;s rate-limit API.
163
141
  </li>
164
142
  <li>
165
- <strong>Enriched extras</strong> (outcome / helpfulness / goal
166
- minute mix):{" "}
167
- {enrichmentOptIn ? (
168
- <span className="text-green-700 dark:text-green-400">opted in</span>
169
- ) : (
170
- <span className="text-gray-500">disabled</span>
171
- )}
143
+ <strong>Enriched extras</strong> — outcome / helpfulness / goal
144
+ minute mix, derived locally by your AI-features pipeline (no new
145
+ model calls ride along with the push).
172
146
  </li>
173
147
  </ul>
174
- <p className="text-xs text-gray-500 pt-2">
175
- Never shared: first-user prompts, final-agent output, raw transcript
176
- text, tool inputs/outputs.
177
- </p>
148
+ <div className="af-panel p-4 text-sm space-y-2">
149
+ <div className="font-medium">What does NOT leave your machine</div>
150
+ <ul className="list-disc list-inside text-gray-600 space-y-1">
151
+ <li>Session transcripts, prompts, or assistant responses</li>
152
+ <li>Absolute paths, file contents, or tool-call payloads</li>
153
+ <li>Anything from projects you exclude above</li>
154
+ <li>Anything from sessions older than the start-of-day rollup window</li>
155
+ </ul>
156
+ </div>
178
157
  </section>
179
158
 
180
- <section className="border rounded-lg p-5 space-y-3">
159
+ <section className="af-card space-y-3">
181
160
  <h2 className="text-lg font-medium">Disconnect</h2>
182
161
  <p className="text-sm text-gray-600">
183
162
  To leave the team and stop all syncing, run:
@@ -8,17 +8,17 @@ const HEALTH_COLORS = {
8
8
  red: "var(--af-error, #ef4444)",
9
9
  } as const;
10
10
 
11
- export function TeamConnectionSection() {
11
+ export function SyncActivitySection() {
12
12
  const conn = readTeamConnection();
13
13
  if (!conn.paired) return null;
14
14
 
15
- const { team, member, lastPush, health } = conn;
15
+ const { lastPush, health } = conn;
16
16
  const cliAvailable = Boolean(process.env.FLEETLENS_CLI_BIN);
17
17
 
18
18
  return (
19
19
  <section id="team" className="space-y-4">
20
20
  <div className="flex items-baseline gap-3">
21
- <h2 className="text-lg font-medium">Team connection</h2>
21
+ <h2 className="text-lg font-medium">Sync activity</h2>
22
22
  <span
23
23
  aria-hidden
24
24
  style={{
@@ -29,12 +29,9 @@ export function TeamConnectionSection() {
29
29
  display: "inline-block",
30
30
  }}
31
31
  />
32
- <span className="text-sm text-gray-500">{team.name}</span>
33
32
  </div>
34
33
 
35
34
  <dl className="grid grid-cols-[8rem_1fr] gap-y-1 text-sm text-gray-600">
36
- <dt>Server</dt><dd className="font-mono text-xs">{team.serverUrl}</dd>
37
- <dt>Paired</dt><dd>{new Date(member.pairedAt).toLocaleString()}</dd>
38
35
  <dt>Last sync</dt>
39
36
  <dd suppressHydrationWarning>
40
37
  {lastPush.kind === "none"
@@ -44,7 +41,7 @@ export function TeamConnectionSection() {
44
41
  </dl>
45
42
 
46
43
  {lastPush.kind === "ok" && (
47
- <div className="rounded border border-gray-200 p-4 space-y-3 text-sm">
44
+ <div className="af-panel p-4 space-y-3 text-sm">
48
45
  <div className="text-xs uppercase tracking-wide text-gray-500">
49
46
  Last push
50
47
  {lastPush.payload.dailyRollup ? ` — ${lastPush.payload.dailyRollup.day}` : ""}
@@ -93,7 +90,7 @@ export function TeamConnectionSection() {
93
90
  <summary className="cursor-pointer text-gray-500 hover:text-gray-700">
94
91
  Show raw JSON payload
95
92
  </summary>
96
- <pre className="mt-2 rounded border border-gray-200 bg-gray-50 p-3 font-mono text-[11px] whitespace-pre-wrap overflow-x-auto">
93
+ <pre className="af-panel mt-2 p-3 font-mono text-[11px] whitespace-pre-wrap overflow-x-auto">
97
94
  {JSON.stringify(lastPush.payload, null, 2)}
98
95
  </pre>
99
96
  </details>
@@ -101,27 +98,13 @@ export function TeamConnectionSection() {
101
98
  )}
102
99
 
103
100
  {lastPush.kind === "error" && (
104
- <div className="rounded border border-red-300 bg-red-50 p-4 text-sm text-red-700">
101
+ <div className="af-panel-danger p-4 text-sm">
105
102
  <div className="font-medium">Last sync failed</div>
106
103
  <div>{lastPush.error}</div>
107
104
  </div>
108
105
  )}
109
106
 
110
107
  <ForceSyncButton cliAvailable={cliAvailable} />
111
-
112
- <div className="rounded border border-gray-200 p-4 text-sm space-y-2">
113
- <div className="font-medium">What does NOT leave your machine</div>
114
- <ul className="list-disc list-inside text-gray-600 space-y-1">
115
- <li>Session transcripts, prompts, or assistant responses</li>
116
- <li>Project names, paths, or repo information</li>
117
- <li>File contents or tool-call payloads</li>
118
- <li>Anything from sessions older than the start-of-day rollup window</li>
119
- </ul>
120
- </div>
121
-
122
- <p className="text-xs text-gray-500">
123
- To disconnect from this team, run <code>fleetlens team leave</code> in your terminal.
124
- </p>
125
108
  </section>
126
109
  );
127
110
  }
@@ -0,0 +1,201 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { ProjectSyncPicker } from "@/components/project-sync-picker";
4
+ import type { SyncProjectRow } from "@/lib/sync-projects-data";
5
+ import type { SyncProjects } from "@/lib/team-config";
6
+
7
+ const ts = () => new Date().toTimeString().slice(0, 8);
8
+
9
+ export function SyncedProjectsForm({
10
+ projects,
11
+ initial,
12
+ }: {
13
+ projects: SyncProjectRow[];
14
+ initial: SyncProjects;
15
+ }) {
16
+ const [value, setValue] = useState<SyncProjects>(initial);
17
+ // Last-persisted selection — the read-only summary renders from this, so a
18
+ // cancelled edit or in-flight change never lies about what's syncing.
19
+ const [saved, setSaved] = useState<SyncProjects>(initial);
20
+ const [editing, setEditing] = useState(false);
21
+ const [saving, setSaving] = useState(false);
22
+ const [savedMsg, setSavedMsg] = useState<string | null>(null);
23
+ const [log, setLog] = useState<{ text: string; tone: "default" | "warn" | "error" }[] | null>(null);
24
+ const logRef = useRef<HTMLDivElement>(null);
25
+
26
+ useEffect(() => {
27
+ logRef.current?.scrollTo({ top: logRef.current.scrollHeight });
28
+ }, [log]);
29
+
30
+ const append = (text: string, tone: "default" | "warn" | "error" = "default") =>
31
+ setLog((prev) => [...(prev ?? []), { text: `${ts()} ${text}`, tone }]);
32
+
33
+ // Same NDJSON→SSE stream the onboarding wizard consumes; a changed
34
+ // selection re-pushes FULL history so the server matches it.
35
+ async function streamResync() {
36
+ setLog([]);
37
+ append("re-pushing your full history under the new selection…");
38
+ try {
39
+ const res = await fetch("/api/team/onboarding/start", {
40
+ method: "POST",
41
+ headers: { "content-type": "application/json" },
42
+ body: JSON.stringify(value),
43
+ });
44
+ if (!res.ok || !res.body) {
45
+ const body = await res.json().catch(() => null);
46
+ append(body?.error ?? `Error ${res.status}`, "error");
47
+ return;
48
+ }
49
+ const reader = res.body.getReader();
50
+ const decoder = new TextDecoder();
51
+ let buf = "";
52
+ for (;;) {
53
+ const { done, value: chunk } = await reader.read();
54
+ if (done) break;
55
+ buf += decoder.decode(chunk, { stream: true });
56
+ const frames = buf.split("\n\n");
57
+ buf = frames.pop() ?? "";
58
+ for (const frame of frames) {
59
+ const data = frame.split("\n").find((l) => l.startsWith("data: "))?.slice(6);
60
+ if (!data) continue;
61
+ let ev;
62
+ try {
63
+ ev = JSON.parse(data);
64
+ } catch {
65
+ continue;
66
+ }
67
+ if (ev.type === "phase" && ev.phase === "activity") append(`pushing ${ev.totalDays} day(s), newest first…`);
68
+ else if (ev.type === "day")
69
+ append(
70
+ ev.outcome === "pushed"
71
+ ? `✓ ${ev.day} pushed (${ev.index}/${ev.total})`
72
+ : `${ev.outcome === "queued" ? "⚠" : "✗"} ${ev.day} ${ev.outcome} (${ev.index}/${ev.total})`,
73
+ ev.outcome === "pushed" ? "default" : ev.outcome === "queued" ? "warn" : "error",
74
+ );
75
+ else if (ev.type === "done") append(`done — ${ev.pushed} day(s) pushed`);
76
+ else if (ev.type === "error") append(ev.message ?? "sync error", "error");
77
+ }
78
+ }
79
+ append("server now matches your selection");
80
+ } catch (err) {
81
+ append(`stream failed: ${(err as Error).message} — the daemon finishes the re-push within ~5 min`, "warn");
82
+ }
83
+ }
84
+
85
+ async function handleSave() {
86
+ setSaving(true);
87
+ setSavedMsg(null);
88
+ setLog(null);
89
+ const res = await fetch("/api/team/sync-projects", {
90
+ method: "PUT",
91
+ headers: { "content-type": "application/json" },
92
+ body: JSON.stringify(value),
93
+ });
94
+ const body = await res.json().catch(() => null);
95
+ setSaving(false);
96
+ setSavedMsg(
97
+ res.ok ? (body?.resync ? "Saved — selection changed." : "Saved.") : (body?.error ?? `Error: ${res.status}`),
98
+ );
99
+ if (res.ok) {
100
+ setSaved(value);
101
+ setEditing(false);
102
+ }
103
+ if (body?.resync) void streamResync();
104
+ }
105
+
106
+ const isSynced = (name: string) =>
107
+ saved.excluded.includes(name) ? false : saved.included.includes(name) ? true : saved.autoIncludeNew;
108
+ const synced = projects.filter((p) => isSynced(p.name));
109
+ const privateCount = projects.length - synced.length;
110
+
111
+ return (
112
+ <div className="space-y-4">
113
+ {!editing && (
114
+ <div className="space-y-3">
115
+ <p className="text-sm">
116
+ Syncing <strong>{synced.length}</strong> of {projects.length} projects
117
+ {saved.autoIncludeNew ? " — new projects sync automatically." : " — new projects stay private."}
118
+ </p>
119
+ <div className="flex flex-wrap gap-1.5">
120
+ {synced.slice(0, 8).map((p) => (
121
+ <a
122
+ key={p.name}
123
+ href={`/projects/${encodeURIComponent(p.projectDir)}`}
124
+ className="af-chip"
125
+ >
126
+ {p.name}
127
+ </a>
128
+ ))}
129
+ {synced.length > 8 && (
130
+ <span className="px-2 py-0.5 text-xs text-gray-500">+{synced.length - 8} more</span>
131
+ )}
132
+ </div>
133
+ {privateCount > 0 && (
134
+ <p className="text-xs text-gray-500">
135
+ {privateCount} project{privateCount === 1 ? " stays" : "s stay"} private on this machine.
136
+ </p>
137
+ )}
138
+ <div className="flex items-center gap-3">
139
+ <button
140
+ type="button"
141
+ onClick={() => {
142
+ setValue(saved);
143
+ setSavedMsg(null);
144
+ setEditing(true);
145
+ }}
146
+ className="af-btn"
147
+ >
148
+ Edit selection
149
+ </button>
150
+ {savedMsg && <p className="text-sm">{savedMsg}</p>}
151
+ </div>
152
+ </div>
153
+ )}
154
+ {editing && (
155
+ <>
156
+ <ProjectSyncPicker projects={projects} value={value} onChange={setValue} />
157
+ <div className="flex items-center gap-3">
158
+ <button
159
+ type="button"
160
+ onClick={handleSave}
161
+ disabled={saving}
162
+ className="af-btn-primary"
163
+ >
164
+ {saving ? "Saving…" : "Save"}
165
+ </button>
166
+ <button
167
+ type="button"
168
+ onClick={() => {
169
+ setValue(saved);
170
+ setEditing(false);
171
+ setSavedMsg(null);
172
+ }}
173
+ disabled={saving}
174
+ className="af-btn"
175
+ >
176
+ Cancel
177
+ </button>
178
+ {savedMsg && <p className="text-sm">{savedMsg}</p>}
179
+ </div>
180
+ </>
181
+ )}
182
+ {log && (
183
+ <div
184
+ ref={logRef}
185
+ className="max-h-60 overflow-y-auto rounded-lg bg-gray-950 p-3 font-mono text-xs text-gray-200"
186
+ >
187
+ {log.map((line, i) => (
188
+ <div
189
+ key={i}
190
+ className={
191
+ line.tone === "error" ? "text-red-400" : line.tone === "warn" ? "text-amber-400" : undefined
192
+ }
193
+ >
194
+ {line.text}
195
+ </div>
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ );
201
+ }
@@ -0,0 +1,37 @@
1
+ import { readTeamConfig } from "@/lib/team-config";
2
+ import { listSyncProjectRows } from "@/lib/sync-projects-data";
3
+ import { SyncedProjectsForm } from "./synced-projects-form";
4
+
5
+ export async function SyncedProjectsSection() {
6
+ const config = readTeamConfig();
7
+ if (!config) return null;
8
+
9
+ if (config.setupPending) {
10
+ return (
11
+ <section className="space-y-2">
12
+ <h2 className="text-lg font-medium">Synced projects</h2>
13
+ <p className="text-sm text-gray-500">
14
+ Setup isn&rsquo;t finished yet — finish the{" "}
15
+ <a className="underline" href="/team/onboarding">onboarding wizard</a>{" "}
16
+ to choose which projects sync to your team.
17
+ </p>
18
+ </section>
19
+ );
20
+ }
21
+
22
+ const projects = await listSyncProjectRows();
23
+ return (
24
+ <section className="space-y-3">
25
+ <h2 className="text-lg font-medium">Synced projects</h2>
26
+ <p className="text-sm text-gray-500">
27
+ Choose which projects share metrics with your team. Saving a changed
28
+ selection re-pushes your full history so the server matches it —
29
+ excluded projects disappear, newly included ones backfill.
30
+ </p>
31
+ <SyncedProjectsForm
32
+ projects={projects}
33
+ initial={config.syncProjects ?? { autoIncludeNew: true, included: [], excluded: [] }}
34
+ />
35
+ </section>
36
+ );
37
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import type { AutostartState } from "@/lib/autostart-data";
4
+
5
+ export function AutostartToggle({ initial }: { initial: AutostartState }) {
6
+ const [state, setState] = useState(initial);
7
+ const [busy, setBusy] = useState(false);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ if (!state.supported) {
11
+ return (
12
+ <p className="text-sm text-gray-500">
13
+ Auto-start uses a macOS LaunchAgent and is macOS-only for now. Add{" "}
14
+ <code className="font-mono text-xs">fleetlens daemon start</code> to your login items manually.
15
+ </p>
16
+ );
17
+ }
18
+
19
+ async function toggle(enabled: boolean) {
20
+ setBusy(true);
21
+ setError(null);
22
+ const res = await fetch("/api/team/autostart", {
23
+ method: "PUT",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify({ enabled }),
26
+ });
27
+ const body = await res.json().catch(() => null);
28
+ setBusy(false);
29
+ if (!res.ok) {
30
+ setError(body?.error ?? `Error ${res.status}`);
31
+ return;
32
+ }
33
+ setState(body);
34
+ }
35
+
36
+ return (
37
+ <div className="space-y-2">
38
+ <label className="flex cursor-pointer items-start gap-2 text-sm">
39
+ <input
40
+ type="checkbox"
41
+ checked={state.installed}
42
+ disabled={busy}
43
+ onChange={(e) => void toggle(e.target.checked)}
44
+ />
45
+ <span>
46
+ Start Fleetlens at login (dashboard + usage daemon)
47
+ <span className="mt-0.5 block text-xs text-gray-500">
48
+ {state.installed
49
+ ? "Enabled — the dashboard and reporting come back after a reboot."
50
+ : state.optedOut
51
+ ? "Disabled — you opted out; joining a team won't re-enable it."
52
+ : "Disabled — pairing with a team enables this automatically."}
53
+ </span>
54
+ </span>
55
+ </label>
56
+ {error && <p className="text-sm text-red-600">{error}</p>}
57
+ </div>
58
+ );
59
+ }
@@ -74,7 +74,10 @@ function CycleBar({
74
74
  cycle: CyclePeak;
75
75
  windowLabel: "5h" | "7d";
76
76
  }) {
77
+ // Bar height / tone stay clamped to 100; the LABEL shows the true value so
78
+ // an overage cycle reads "247%", not a misleading "100%".
77
79
  const pct = Math.max(0, Math.min(100, cycle.peakPct));
80
+ const labelPct = Math.max(0, cycle.peakPct);
78
81
  const tone = cycle.current
79
82
  ? paceToneForCycle(pct, new Date(cycle.endsAt).getTime(), WINDOW_MS[windowLabel])
80
83
  : utilizationTone(pct);
@@ -104,7 +107,7 @@ function CycleBar({
104
107
  fontVariantNumeric: "tabular-nums",
105
108
  }}
106
109
  >
107
- {pct.toFixed(0)}%
110
+ {labelPct.toFixed(0)}%
108
111
  </div>
109
112
  <div
110
113
  style={{