ccdeck 3.40.2 → 3.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/web/assets/BrowserWatchModal-xoHNkWWF.js +1 -0
- package/dist/web/assets/RadioPanel-Bii2jN0n.js +1 -0
- package/dist/web/assets/TrafficRadar-CXRcWYZ8.js +1 -0
- package/dist/web/assets/UsageHistoryModal-Ceg-2Dm9.js +1 -0
- package/dist/web/assets/ad-B18i8NGa.svg +150 -0
- package/dist/web/assets/ae-CZRtWSox.svg +6 -0
- package/dist/web/assets/af-C77Rf6cE.svg +81 -0
- package/dist/web/assets/ag-C8MykuG2.svg +14 -0
- package/dist/web/assets/ai-Dmedkf4v.svg +29 -0
- package/dist/web/assets/al-10QRkakw.svg +5 -0
- package/dist/web/assets/am-DMt4_dd4.svg +5 -0
- package/dist/web/assets/ao-tXuRa6vm.svg +13 -0
- package/dist/web/assets/aq-CF5jO-0h.svg +5 -0
- package/dist/web/assets/ar-Be8Ju1cG.svg +32 -0
- package/dist/web/assets/as-Dekqy8Of.svg +72 -0
- package/dist/web/assets/at-DGA_6m5E.svg +4 -0
- package/dist/web/assets/au-DAHDIuPI.svg +8 -0
- package/dist/web/assets/aw-W0PWLK5p.svg +186 -0
- package/dist/web/assets/ax-DvLIy84U.svg +18 -0
- package/dist/web/assets/az-Bk-bYNxy.svg +8 -0
- package/dist/web/assets/ba-WdDiSMvP.svg +12 -0
- package/dist/web/assets/bb-DJxbaxmT.svg +6 -0
- package/dist/web/assets/bd-BF9t1-60.svg +4 -0
- package/dist/web/assets/be-CLLkK3PN.svg +7 -0
- package/dist/web/assets/bf-YclsoDuF.svg +7 -0
- package/dist/web/assets/bg-GUQenraa.svg +5 -0
- package/dist/web/assets/bh-BQqEGq6F.svg +4 -0
- package/dist/web/assets/bi-CRmKY6RQ.svg +15 -0
- package/dist/web/assets/bj-14PhO9bM.svg +14 -0
- package/dist/web/assets/bl-4CI2YcwX.svg +5 -0
- package/dist/web/assets/bm-BeYgB2z9.svg +97 -0
- package/dist/web/assets/bn-B6T3O78g.svg +36 -0
- package/dist/web/assets/bo-CcUiMqkJ.svg +673 -0
- package/dist/web/assets/bq-BYpdxEeT.svg +5 -0
- package/dist/web/assets/br-Cu5YU29T.svg +45 -0
- package/dist/web/assets/bs-7Gd_oriM.svg +13 -0
- package/dist/web/assets/bt-BTo4qm10.svg +89 -0
- package/dist/web/assets/bv-wM9JLv4R.svg +13 -0
- package/dist/web/assets/bw-n5ZaAnGL.svg +7 -0
- package/dist/web/assets/by-C621sBpd.svg +18 -0
- package/dist/web/assets/bz-BCKHR4_q.svg +145 -0
- package/dist/web/assets/ca-PYUrLVUV.svg +4 -0
- package/dist/web/assets/cc-BNT6Xjzk.svg +19 -0
- package/dist/web/assets/cd-BGclsrP6.svg +5 -0
- package/dist/web/assets/cf-DRetLmp-.svg +15 -0
- package/dist/web/assets/cg-CwIyG6SE.svg +12 -0
- package/dist/web/assets/ch-sfriZoF1.svg +9 -0
- package/dist/web/assets/ci-C8Q8IYTn.svg +7 -0
- package/dist/web/assets/ck-DfXMUOTo.svg +9 -0
- package/dist/web/assets/cl-BgYYb4qP.svg +13 -0
- package/dist/web/assets/cm-D4yjdmKT.svg +15 -0
- package/dist/web/assets/cn-DifnnI3t.svg +11 -0
- package/dist/web/assets/co-DV591zMm.svg +7 -0
- package/dist/web/assets/cp-K_ay05Q_.svg +7 -0
- package/dist/web/assets/cr-BlYVN-_Q.svg +7 -0
- package/dist/web/assets/cu-L6XVZNgo.svg +13 -0
- package/dist/web/assets/cv-CPsfcOfk.svg +13 -0
- package/dist/web/assets/cw-BbrnximR.svg +14 -0
- package/dist/web/assets/cx-DpYD6n6U.svg +15 -0
- package/dist/web/assets/cy-bZuP8hmf.svg +6 -0
- package/dist/web/assets/cz-WWBC5Aeb.svg +5 -0
- package/dist/web/assets/de-B-2o-4Z9.svg +5 -0
- package/dist/web/assets/dg-CJPJrjiZ.svg +130 -0
- package/dist/web/assets/dj-hp_BwbmO.svg +13 -0
- package/dist/web/assets/dk-DmS9BCZB.svg +5 -0
- package/dist/web/assets/dm-Cbhezfe1.svg +152 -0
- package/dist/web/assets/do-B86d445t.svg +121 -0
- package/dist/web/assets/dz-Dytc1TFu.svg +5 -0
- package/dist/web/assets/ec-CaVOFQ3t.svg +138 -0
- package/dist/web/assets/ee-DufrxGIR.svg +5 -0
- package/dist/web/assets/eg-YC70hswZ.svg +38 -0
- package/dist/web/assets/eh-0awM4TVj.svg +16 -0
- package/dist/web/assets/er-X83uml6t.svg +8 -0
- package/dist/web/assets/es-d5m8M5h8.svg +544 -0
- package/dist/web/assets/et-DwdlzOIx.svg +14 -0
- package/dist/web/assets/eu-Brdgz8ab.svg +28 -0
- package/dist/web/assets/fi-DWUIkfjL.svg +5 -0
- package/dist/web/assets/fj-DEAVMg38.svg +120 -0
- package/dist/web/assets/fk-nuUF_Ak3.svg +90 -0
- package/dist/web/assets/fm-B4Z83GL0.svg +11 -0
- package/dist/web/assets/fo-DFjwyWur.svg +12 -0
- package/dist/web/assets/fr-DVvUyOqI.svg +5 -0
- package/dist/web/assets/ga-BklUhLH_.svg +7 -0
- package/dist/web/assets/gb-DTXiLQoe.svg +7 -0
- package/dist/web/assets/gd-CO-whzUe.svg +27 -0
- package/dist/web/assets/ge-B2RiL-Ih.svg +6 -0
- package/dist/web/assets/gf-DWl5zcw0.svg +5 -0
- package/dist/web/assets/gg-jdOQS2nU.svg +9 -0
- package/dist/web/assets/gh-r3LP_X7q.svg +6 -0
- package/dist/web/assets/gi-BJKE9SzW.svg +32 -0
- package/dist/web/assets/gl-CHaBnMib.svg +4 -0
- package/dist/web/assets/gm-Bu99atwn.svg +14 -0
- package/dist/web/assets/gn-1dJNy9oQ.svg +7 -0
- package/dist/web/assets/gp-DNyt_wTA.svg +5 -0
- package/dist/web/assets/gq-Cag8QTk2.svg +23 -0
- package/dist/web/assets/gr-C5PU0p9p.svg +16 -0
- package/dist/web/assets/gs-DiiNa0F5.svg +133 -0
- package/dist/web/assets/gt-CJo5DI-7.svg +204 -0
- package/dist/web/assets/gu-Di1JYREk.svg +19 -0
- package/dist/web/assets/gw-D249VY33.svg +13 -0
- package/dist/web/assets/gy-CcVYUM2E.svg +9 -0
- package/dist/web/assets/hk-CUNIaSX0.svg +8 -0
- package/dist/web/assets/hm-Dh6t_Tj3.svg +8 -0
- package/dist/web/assets/hn-CRjyS_bm.svg +18 -0
- package/dist/web/assets/hr-fzLfaANM.svg +58 -0
- package/dist/web/assets/ht-DIMg4gti.svg +116 -0
- package/dist/web/assets/hu-7Q5wwIIi.svg +7 -0
- package/dist/web/assets/ic-CSo4d8tH.svg +7 -0
- package/dist/web/assets/id-DiSP6Fmm.svg +4 -0
- package/dist/web/assets/ie-ChAXClx3.svg +7 -0
- package/dist/web/assets/il-_56OEGLa.svg +14 -0
- package/dist/web/assets/im--VPIqfkF.svg +36 -0
- package/dist/web/assets/in-Cdwu6Bq7.svg +25 -0
- package/dist/web/assets/index-2Z0Tr6cq.js +305 -0
- package/dist/web/assets/index-BgSnAnP6.css +1 -0
- package/dist/web/assets/io-13HOfeJD.svg +130 -0
- package/dist/web/assets/iq-Dp8HDzo2.svg +10 -0
- package/dist/web/assets/ir-cCIgaNf6.svg +219 -0
- package/dist/web/assets/is-CZjefTNV.svg +12 -0
- package/dist/web/assets/it-Br7q0Zh6.svg +7 -0
- package/dist/web/assets/je-DyWbhIiC.svg +62 -0
- package/dist/web/assets/jm-CItSr3iX.svg +8 -0
- package/dist/web/assets/jo-BAF1FGbm.svg +16 -0
- package/dist/web/assets/jp-BIMmfxpO.svg +11 -0
- package/dist/web/assets/ke-C8foqndp.svg +23 -0
- package/dist/web/assets/kg-B0FsxZiL.svg +4 -0
- package/dist/web/assets/kh-BeWfuE30.svg +61 -0
- package/dist/web/assets/ki-p_fAQGbS.svg +36 -0
- package/dist/web/assets/km-B5tqtGG7.svg +16 -0
- package/dist/web/assets/kn-DVPxDkNY.svg +14 -0
- package/dist/web/assets/kp-CrDKzoxe.svg +15 -0
- package/dist/web/assets/kr-BCXH1Hao.svg +24 -0
- package/dist/web/assets/kw-fSBzmd30.svg +13 -0
- package/dist/web/assets/ky-Dpsu1myA.svg +103 -0
- package/dist/web/assets/kz-CwKXYZ8s.svg +36 -0
- package/dist/web/assets/la-CuJflhIW.svg +12 -0
- package/dist/web/assets/lb-BSjpYEoo.svg +15 -0
- package/dist/web/assets/lc-CNvab8Ae.svg +8 -0
- package/dist/web/assets/li-CHdhvNcr.svg +43 -0
- package/dist/web/assets/lk-DUkgV9Tq.svg +22 -0
- package/dist/web/assets/lr-B84vu3Ds.svg +14 -0
- package/dist/web/assets/ls-5Xk3Mxq5.svg +8 -0
- package/dist/web/assets/lt-DoukV-Sm.svg +7 -0
- package/dist/web/assets/lu-DOI02Msy.svg +5 -0
- package/dist/web/assets/lv-C-KfY8Yc.svg +6 -0
- package/dist/web/assets/ly-BWpTK3ux.svg +13 -0
- package/dist/web/assets/ma-BTRNTRUj.svg +4 -0
- package/dist/web/assets/mc-PK078JHl.svg +6 -0
- package/dist/web/assets/md-DRlxvNwm.svg +70 -0
- package/dist/web/assets/me-Cv4Gwqah.svg +116 -0
- package/dist/web/assets/mf-BaAGWwAq.svg +5 -0
- package/dist/web/assets/mg-C168LHXW.svg +7 -0
- package/dist/web/assets/mh-gxuIp6Wk.svg +7 -0
- package/dist/web/assets/mk-D9SIMr-a.svg +5 -0
- package/dist/web/assets/ml-DVf6ujpi.svg +7 -0
- package/dist/web/assets/mm-lwT09MQ0.svg +12 -0
- package/dist/web/assets/mn-CgXyw0O9.svg +14 -0
- package/dist/web/assets/mo-BAtCjuYA.svg +9 -0
- package/dist/web/assets/mp-CrOApEqW.svg +86 -0
- package/dist/web/assets/mq-BFnJ22KI.svg +5 -0
- package/dist/web/assets/mr-D6G1wpeZ.svg +6 -0
- package/dist/web/assets/ms-DxciGbUu.svg +29 -0
- package/dist/web/assets/mt-YqzKx9xl.svg +58 -0
- package/dist/web/assets/mu-mcq7cUFl.svg +8 -0
- package/dist/web/assets/mv-BynAllfM.svg +6 -0
- package/dist/web/assets/mw-C15nc1xZ.svg +10 -0
- package/dist/web/assets/mx-Cc8Ccfe8.svg +382 -0
- package/dist/web/assets/my-Co4JeeyE.svg +26 -0
- package/dist/web/assets/mz-Drlr_USV.svg +21 -0
- package/dist/web/assets/na-D79ffb4Z.svg +16 -0
- package/dist/web/assets/nc-5j7wEmDR.svg +13 -0
- package/dist/web/assets/ne-B1jPOYkl.svg +6 -0
- package/dist/web/assets/nf-Dl00mlk2.svg +9 -0
- package/dist/web/assets/ng-su4NM9If.svg +6 -0
- package/dist/web/assets/ni-CcFCSQxm.svg +129 -0
- package/dist/web/assets/nl-BnOa6UiA.svg +5 -0
- package/dist/web/assets/no-qf2JPO73.svg +7 -0
- package/dist/web/assets/np-CIuq5GKd.svg +13 -0
- package/dist/web/assets/nr-DERIdzkN.svg +12 -0
- package/dist/web/assets/nu-BfgWvGcd.svg +10 -0
- package/dist/web/assets/nz-5vODdBjz.svg +36 -0
- package/dist/web/assets/om-DcqxRdQL.svg +115 -0
- package/dist/web/assets/pa-BLNN9G2-.svg +14 -0
- package/dist/web/assets/pc-BJpYiA9b.svg +33 -0
- package/dist/web/assets/pe-BLqhuu1C.svg +4 -0
- package/dist/web/assets/pf-C8ahG68q.svg +19 -0
- package/dist/web/assets/pg-BAYpbp9Z.svg +9 -0
- package/dist/web/assets/ph-BEzTn62K.svg +6 -0
- package/dist/web/assets/pk-CWMEc3ad.svg +15 -0
- package/dist/web/assets/pl-o38JROoc.svg +6 -0
- package/dist/web/assets/pm-CHiP5UmZ.svg +5 -0
- package/dist/web/assets/pn-DgxdtieE.svg +53 -0
- package/dist/web/assets/pr-B4tMV0xm.svg +13 -0
- package/dist/web/assets/ps-DO8YKYeS.svg +6 -0
- package/dist/web/assets/pt-DZ2ADgIR.svg +57 -0
- package/dist/web/assets/pw-CQP52WMX.svg +11 -0
- package/dist/web/assets/py-mNzh0mZC.svg +157 -0
- package/dist/web/assets/qa-Dkmpc78M.svg +4 -0
- package/dist/web/assets/re-Bk4ipYK1.svg +5 -0
- package/dist/web/assets/ro-CoSeqKY1.svg +7 -0
- package/dist/web/assets/rs-BfwKwXtn.svg +292 -0
- package/dist/web/assets/ru-D-4tNwXt.svg +5 -0
- package/dist/web/assets/rw-D7nbSYKI.svg +13 -0
- package/dist/web/assets/sa-Dh79zbT9.svg +25 -0
- package/dist/web/assets/sb-BDTYjcbk.svg +13 -0
- package/dist/web/assets/sc-CRNsSLg9.svg +7 -0
- package/dist/web/assets/sd-ClziNjGr.svg +13 -0
- package/dist/web/assets/se-8C923vhy.svg +4 -0
- package/dist/web/assets/sg-DGYIMG0G.svg +13 -0
- package/dist/web/assets/sh-DNImvbrE.svg +7 -0
- package/dist/web/assets/si-BMKT-Tec.svg +18 -0
- package/dist/web/assets/sj-BFFEGknm.svg +7 -0
- package/dist/web/assets/sk-K9BNIYAO.svg +9 -0
- package/dist/web/assets/sl-DxLJY5vJ.svg +7 -0
- package/dist/web/assets/sm-DGBIRFB_.svg +75 -0
- package/dist/web/assets/sn-S8ipNF1U.svg +8 -0
- package/dist/web/assets/so-DlzA2Fco.svg +11 -0
- package/dist/web/assets/sr-Co7OKBh3.svg +6 -0
- package/dist/web/assets/ss-CxVEpdPD.svg +8 -0
- package/dist/web/assets/st-C1Nd1c3V.svg +16 -0
- package/dist/web/assets/sv-CJIHhYwF.svg +593 -0
- package/dist/web/assets/sx-nDhIaDNb.svg +56 -0
- package/dist/web/assets/sy-DwSud114.svg +6 -0
- package/dist/web/assets/sz-qxMwa2gs.svg +34 -0
- package/dist/web/assets/tc-dtelpZmc.svg +50 -0
- package/dist/web/assets/td-BsuVhZpT.svg +7 -0
- package/dist/web/assets/tf-Co33RhQH.svg +15 -0
- package/dist/web/assets/tg-CP1-sc35.svg +14 -0
- package/dist/web/assets/th-tzq84hgd.svg +7 -0
- package/dist/web/assets/tj-b-aWfOTb.svg +22 -0
- package/dist/web/assets/tk-CDucsEss.svg +5 -0
- package/dist/web/assets/tl-wpo93AGk.svg +13 -0
- package/dist/web/assets/tm-C_WSgUcv.svg +204 -0
- package/dist/web/assets/tn-BcKCZULf.svg +4 -0
- package/dist/web/assets/to-D8uVsoxb.svg +10 -0
- package/dist/web/assets/tr-Cd6FO9Bg.svg +8 -0
- package/dist/web/assets/tt-CTnr7aY5.svg +5 -0
- package/dist/web/assets/tv-DDqkVT-n.svg +9 -0
- package/dist/web/assets/tw-d-Mf-0VT.svg +34 -0
- package/dist/web/assets/tz-BjLtHeil.svg +13 -0
- package/dist/web/assets/ua-Bq0XgQqK.svg +6 -0
- package/dist/web/assets/ug-ByL2ejGl.svg +30 -0
- package/dist/web/assets/um-Bhke9Eic.svg +9 -0
- package/dist/web/assets/un-Bqg4Cbbh.svg +16 -0
- package/dist/web/assets/us-C73uVeEr.svg +9 -0
- package/dist/web/assets/uy-DD6peej-.svg +28 -0
- package/dist/web/assets/uz-C2f-Cubn.svg +30 -0
- package/dist/web/assets/va-B9-hqIE-.svg +190 -0
- package/dist/web/assets/vc-COpmFovN.svg +8 -0
- package/dist/web/assets/ve-BSqnIB9l.svg +26 -0
- package/dist/web/assets/vg-C7xY6pic.svg +59 -0
- package/dist/web/assets/vi-BC_zcciE.svg +28 -0
- package/dist/web/assets/vn-BEAEijd0.svg +11 -0
- package/dist/web/assets/vu-D6k0NQlg.svg +21 -0
- package/dist/web/assets/wf-DrxpOO_G.svg +5 -0
- package/dist/web/assets/ws-vzJNwdVm.svg +7 -0
- package/dist/web/assets/xk-Bj15g7cp.svg +5 -0
- package/dist/web/assets/xx-zm_JmrXl.svg +4 -0
- package/dist/web/assets/ye-BwST9gXC.svg +7 -0
- package/dist/web/assets/yt-DIfEG0ex.svg +5 -0
- package/dist/web/assets/za-Jz40JTrv.svg +17 -0
- package/dist/web/assets/zm-BmsW91ne.svg +27 -0
- package/dist/web/assets/zw-U0m7oJ5e.svg +21 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +44 -0
- package/src/server/ccusage-session.mjs +17 -0
- package/src/server/claude-accounts.mjs +18 -10
- package/src/server/codex-quota.mjs +7 -1
- package/src/server/index.mjs +4 -1
- package/src/server/quota-history.mjs +149 -0
- package/src/server/quota-shape.mjs +23 -20
- package/src/server/quota.mjs +7 -2
- package/src/server/request-gates.mjs +1 -0
- package/src/server/traffic-radar-config.mjs +35 -3
- package/src/server/traffic-radar-routes.mjs +13 -0
- package/src/server/traffic-radar.mjs +12 -1
- package/src/server/usage-routes.mjs +12 -0
- package/dist/web/assets/BrowserWatchModal-CEWlounF.js +0 -1
- package/dist/web/assets/RadioPanel-B7qs4g1T.js +0 -1
- package/dist/web/assets/TrafficRadar-CGsJ5yOZ.js +0 -1
- package/dist/web/assets/UsageHistoryModal-DLExexb7.js +0 -1
- package/dist/web/assets/index-B_uYtqRM.css +0 -1
- package/dist/web/assets/index-BepyDfFS.js +0 -306
|
@@ -35,7 +35,8 @@ export const WIN_7D_SEC = 604800;
|
|
|
35
35
|
* Exported for its test.
|
|
36
36
|
*/
|
|
37
37
|
export function clampPct(v) {
|
|
38
|
-
return
|
|
38
|
+
return typeof v === "number" && Number.isFinite(v)
|
|
39
|
+
? Math.min(100, Math.max(0, Math.round(v))) : null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// ISO-8601 → "Jun 19, 1:19pm" (local time, matching the CLI display format).
|
|
@@ -48,13 +49,13 @@ export function clampPct(v) {
|
|
|
48
49
|
const fmtResetIso = resetLabelIso;
|
|
49
50
|
|
|
50
51
|
function isoToSec(iso) {
|
|
51
|
-
if (!iso) return null;
|
|
52
|
+
if (typeof iso !== "string" || !iso) return null;
|
|
52
53
|
const t = new Date(iso).getTime();
|
|
53
|
-
return
|
|
54
|
+
return !Number.isFinite(t) || t <= 0 ? null : Math.floor(t / 1000);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// Map the OAuth usage JSON to our quota result shape.
|
|
57
|
-
// utilization is already a 0–100 percentage.
|
|
58
|
+
// utilization is already a 0–100 percentage. Each window stays independent.
|
|
58
59
|
//
|
|
59
60
|
// A 7d window that is absent is UNKNOWN, and published as null (#1627). The
|
|
60
61
|
// endpoint can send `seven_day: null`, and this used to fill in a zero there —
|
|
@@ -67,25 +68,26 @@ export function mapOAuthUsage(data) {
|
|
|
67
68
|
const son = data?.seven_day_sonnet;
|
|
68
69
|
const opus = data?.seven_day_opus;
|
|
69
70
|
|
|
70
|
-
const
|
|
71
|
-
|
|
71
|
+
const fivePct = clampPct(fh?.utilization);
|
|
72
|
+
const sevenPct = clampPct(sd?.utilization);
|
|
73
|
+
if (fivePct == null && sevenPct == null) return null;
|
|
72
74
|
|
|
73
75
|
const result = {
|
|
74
|
-
session5hPct:
|
|
76
|
+
session5hPct: fivePct,
|
|
75
77
|
session5hWindowSec: WIN_5H_SEC,
|
|
76
|
-
session5hReset: fmtResetIso(
|
|
77
|
-
session5hResetAt: isoToSec(
|
|
78
|
+
session5hReset: fmtResetIso(fivePct == null ? null : fh?.resets_at),
|
|
79
|
+
session5hResetAt: isoToSec(fivePct == null ? null : fh?.resets_at),
|
|
78
80
|
week7dWindowSec: WIN_7D_SEC,
|
|
79
81
|
};
|
|
80
|
-
if (
|
|
81
|
-
result.week7dPct =
|
|
82
|
+
if (sevenPct != null) {
|
|
83
|
+
result.week7dPct = sevenPct;
|
|
82
84
|
result.week7dReset = fmtResetIso(sd.resets_at);
|
|
83
85
|
result.week7dResetAt = isoToSec(sd.resets_at);
|
|
84
86
|
} else {
|
|
85
87
|
result.week7dPct = null;
|
|
86
88
|
}
|
|
87
|
-
if (son?.utilization != null) result.weekSonnetPct = clampPct(son.utilization);
|
|
88
|
-
if (opus?.utilization != null) result.weekOpusPct = clampPct(opus.utilization);
|
|
89
|
+
if (clampPct(son?.utilization) != null) result.weekSonnetPct = clampPct(son.utilization);
|
|
90
|
+
if (clampPct(opus?.utilization) != null) result.weekOpusPct = clampPct(opus.utilization);
|
|
89
91
|
|
|
90
92
|
// extra usage credits (pay-as-you-go top-up), if enabled
|
|
91
93
|
const extra = data?.extra_usage;
|
|
@@ -114,21 +116,22 @@ export function quotaFromStore(entry) {
|
|
|
114
116
|
const good = entry?.lastGood;
|
|
115
117
|
const fh = good?.five_hour;
|
|
116
118
|
const sd = good?.seven_day;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
+
const fivePct = clampPct(fh?.pct);
|
|
120
|
+
const sevenPct = clampPct(sd?.pct);
|
|
121
|
+
if (fivePct == null && sevenPct == null) return null;
|
|
119
122
|
|
|
120
123
|
const out = {
|
|
121
124
|
ok: true,
|
|
122
125
|
source: "claude-swap",
|
|
123
|
-
session5hPct:
|
|
126
|
+
session5hPct: fivePct,
|
|
124
127
|
session5hWindowSec: WIN_5H_SEC,
|
|
125
|
-
session5hReset: fmtResetIso(
|
|
126
|
-
session5hResetAt: isoToSec(
|
|
128
|
+
session5hReset: fmtResetIso(fivePct == null ? null : fh?.resets_at),
|
|
129
|
+
session5hResetAt: isoToSec(fivePct == null ? null : fh?.resets_at),
|
|
127
130
|
week7dWindowSec: WIN_7D_SEC,
|
|
128
131
|
// Unknown rather than 0 when the row has no 7d window, as mapOAuthUsage
|
|
129
132
|
// publishes it (#1627): claude-swap writes no `seven_day` at all when the
|
|
130
133
|
// endpoint sent none.
|
|
131
|
-
week7dPct:
|
|
134
|
+
week7dPct: sevenPct,
|
|
132
135
|
week7dReset: fmtResetIso(sd?.resets_at),
|
|
133
136
|
week7dResetAt: isoToSec(sd?.resets_at),
|
|
134
137
|
// The age of the DATA, not of our read of it. The panel prints this, and
|
|
@@ -139,7 +142,7 @@ export function quotaFromStore(entry) {
|
|
|
139
142
|
// claude-swap keeps per-model windows in a named list rather than fixed
|
|
140
143
|
// fields, because which ones an account has depends on its plan.
|
|
141
144
|
for (const s of Array.isArray(good.scoped) ? good.scoped : []) {
|
|
142
|
-
if (
|
|
145
|
+
if (clampPct(s?.pct) == null) continue;
|
|
143
146
|
if (/sonnet/i.test(s.name ?? "")) out.weekSonnetPct = clampPct(s.pct);
|
|
144
147
|
else if (/opus/i.test(s.name ?? "")) out.weekOpusPct = clampPct(s.pct);
|
|
145
148
|
}
|
package/src/server/quota.mjs
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
// shape in quota-shape.mjs; the token, source 2's request and the cooldown are
|
|
53
53
|
// quota-oauth.mjs's; running source 3 is quota-cli.mjs's; and the store path's
|
|
54
54
|
// reset inventory is quota-store-resets.mjs's.
|
|
55
|
+
import { noteQuotaHistory, forgetQuotaHistoryAccount } from "./quota-history.mjs";
|
|
55
56
|
import { activeAccountUsage, requestCollection } from "./claude-accounts.mjs";
|
|
56
57
|
import { quotaFromStore, readingLapsed, WIN_5H_SEC, WIN_7D_SEC } from "./quota-shape.mjs";
|
|
57
58
|
import {
|
|
@@ -254,7 +255,10 @@ function publish(gen, result, at, { good = false, who = null } = {}) {
|
|
|
254
255
|
if (gen !== _generation) return result;
|
|
255
256
|
_cache = result;
|
|
256
257
|
_cacheAt = at;
|
|
257
|
-
if (good) {
|
|
258
|
+
if (good) {
|
|
259
|
+
noteQuotaHistory("claude", who?.account, result);
|
|
260
|
+
_lastGood = result; _lastGoodFor = who;
|
|
261
|
+
}
|
|
258
262
|
return result;
|
|
259
263
|
}
|
|
260
264
|
|
|
@@ -421,7 +425,7 @@ async function _doFetch(now, force = false, gen = _generation) {
|
|
|
421
425
|
// the question is new, and it can be asked at most once per reading held.
|
|
422
426
|
const who = await readFor(store);
|
|
423
427
|
const switched = _lastGood != null && switchedSince(_lastGoodFor, who);
|
|
424
|
-
if (switched) { _lastGood = null; _lastGoodFor = null; }
|
|
428
|
+
if (switched) { _lastGood = null; _lastGoodFor = null; forgetQuotaHistoryAccount("claude"); }
|
|
425
429
|
|
|
426
430
|
if (store && now - store.fetchedAt <= STORE_TRUSTED_MS) {
|
|
427
431
|
// Keep the store moving even when the accounts panel is closed. Without
|
|
@@ -563,6 +567,7 @@ async function _doFetch(now, force = false, gen = _generation) {
|
|
|
563
567
|
* left.
|
|
564
568
|
*/
|
|
565
569
|
export function invalidateQuotaCache() {
|
|
570
|
+
forgetQuotaHistoryAccount("claude");
|
|
566
571
|
_cache = null;
|
|
567
572
|
_cacheAt = 0;
|
|
568
573
|
_lastGood = null;
|
|
@@ -447,6 +447,7 @@ export const GUARDED_READS = new Set([
|
|
|
447
447
|
// this answer (codex-quota.mjs), which is the account identity the roster
|
|
448
448
|
// route above is guarded for.
|
|
449
449
|
"/api/codex-quota",
|
|
450
|
+
"/api/quota/history",
|
|
450
451
|
// And the rest of the usage panel, by the rule /api/account-projects is here
|
|
451
452
|
// under: the active account's quota windows, Claude's spend by day and by
|
|
452
453
|
// session, Codex's token spend, and the auto-switch settings with the last
|
|
@@ -2,8 +2,10 @@ import { open } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { posix, win32 } from "node:path";
|
|
4
4
|
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
import { readSettingsForWrite, stripBom, writeFileAtomic } from "./atomic-write.mjs";
|
|
5
7
|
|
|
6
|
-
const BOOLEAN_KEYS = [
|
|
8
|
+
export const BOOLEAN_KEYS = [
|
|
7
9
|
"CLAUDE_CODE_ENABLE_TELEMETRY", "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
|
|
8
10
|
"ENABLE_ENHANCED_TELEMETRY_BETA", "OTEL_LOG_USER_PROMPTS",
|
|
9
11
|
"OTEL_LOG_ASSISTANT_RESPONSES", "OTEL_LOG_TOOL_DETAILS",
|
|
@@ -64,7 +66,7 @@ async function readBoundedJson(path) {
|
|
|
64
66
|
const buffer = Buffer.alloc(262_145);
|
|
65
67
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
66
68
|
if (bytesRead > 262_144) throw new Error("oversize");
|
|
67
|
-
return JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
69
|
+
return JSON.parse(stripBom(buffer.subarray(0, bytesRead).toString("utf8")));
|
|
68
70
|
} finally { await file.close(); }
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -86,5 +88,35 @@ export async function readRadarConfig({ home = homedir(), env = process.env, pla
|
|
|
86
88
|
return { name, status: error?.code === "ENOENT" ? "absent" : "unavailable", variables: [] };
|
|
87
89
|
}
|
|
88
90
|
}));
|
|
89
|
-
|
|
91
|
+
const variables = sources.flatMap(s => s.variables);
|
|
92
|
+
const blockedKeys = new Set(variables.filter(v => v.source !== "User settings").map(v => v.key));
|
|
93
|
+
const uncertain = sources.some(s => s.status === "unavailable");
|
|
94
|
+
return { sources: sources.map(({ name, status }) => ({ name, status })), variables: variables.map(v => ({
|
|
95
|
+
...v,
|
|
96
|
+
...(BOOLEAN_KEYS.includes(v.key) ? { editable: !uncertain && !blockedKeys.has(v.key),
|
|
97
|
+
...((uncertain || blockedKeys.has(v.key)) ? { blockedReason: uncertain ? "Settings could not be verified. Try again after they can be read." : "Controlled by organization or managed settings. Ask your administrator to change this flag." } : {}) } : {}),
|
|
98
|
+
})) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Serialize read/modify/write requests so toggling different flags preserves both.
|
|
102
|
+
let settingsWrite = Promise.resolve();
|
|
103
|
+
export function updateRadarFlag(key, enabled, options = {}) {
|
|
104
|
+
const write = settingsWrite.catch(() => {}).then(async () => {
|
|
105
|
+
if (!BOOLEAN_KEYS.includes(key) || typeof enabled !== "boolean") throw new Error("Choose a supported telemetry flag and an On or Off value.");
|
|
106
|
+
const config = await readRadarConfig(options);
|
|
107
|
+
if (config.sources.some(s => s.status === "unavailable")) throw new Error("Settings could not be verified. No changes were saved.");
|
|
108
|
+
if (config.variables.some(v => v.key === key && v.source !== "User settings")) throw new Error("This flag is controlled by organization or managed settings. Ask your administrator to change it.");
|
|
109
|
+
const { home = homedir(), env = process.env, platform = process.platform } = options;
|
|
110
|
+
const pathJoin = platform === "win32" ? win32.join : posix.join;
|
|
111
|
+
const dir = claudeConfigDir(env, home, platform);
|
|
112
|
+
const path = pathJoin(dir, "settings.json");
|
|
113
|
+
const { settings } = await readSettingsForWrite(path);
|
|
114
|
+
if (settings.env !== undefined && (!settings.env || typeof settings.env !== "object" || Array.isArray(settings.env))) throw new Error("User settings contain an invalid env object. No changes were saved.");
|
|
115
|
+
settings.env = { ...settings.env, [key]: enabled ? "1" : "0" };
|
|
116
|
+
await mkdir(dir, { recursive: true });
|
|
117
|
+
await writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n");
|
|
118
|
+
return { ok: true, config: await readRadarConfig(options) };
|
|
119
|
+
});
|
|
120
|
+
settingsWrite = write;
|
|
121
|
+
return write;
|
|
90
122
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { trafficCapture } from "./traffic-radar-capture.mjs";
|
|
2
2
|
import { CaptureSetupError } from './traffic-radar-platform.mjs';
|
|
3
|
+
import { updateRadarFlag } from './traffic-radar-config.mjs';
|
|
4
|
+
import { trafficRadar } from './traffic-radar.mjs';
|
|
3
5
|
const BASE = '/api/system/traffic-radar';
|
|
4
6
|
const loopback = req => ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.socket.remoteAddress);
|
|
5
7
|
export function isAuthorizedTrafficIngest(req, url) {
|
|
@@ -19,6 +21,17 @@ export async function handleTrafficCapture(req, res, url, capture = trafficCaptu
|
|
|
19
21
|
if (!loopback(req)) return reply(res, 403, { error: 'Telemetry contents are available on this machine only.' });
|
|
20
22
|
const action = url.pathname.slice(BASE.length);
|
|
21
23
|
try {
|
|
24
|
+
if (req.method === 'POST' && action === '/configuration') {
|
|
25
|
+
let data;
|
|
26
|
+
try { data = JSON.parse((await body(req, 4096)).toString()); }
|
|
27
|
+
catch { return reply(res, 400, { error: 'Invalid settings request.' }); }
|
|
28
|
+
try {
|
|
29
|
+
const result = await updateRadarFlag(data?.key, data?.enabled);
|
|
30
|
+
trafficRadar.updateConfig(result.config);
|
|
31
|
+
return reply(res, 200, result);
|
|
32
|
+
}
|
|
33
|
+
catch (error) { return reply(res, 400, { error: error.message }); }
|
|
34
|
+
}
|
|
22
35
|
if (req.method === 'GET' && action === '/capture') return reply(res, 200, capture.read());
|
|
23
36
|
if (req.method === 'GET' && action === '/capture/event') {
|
|
24
37
|
const detail = capture.detail(Number(url.searchParams.get('id')));
|
|
@@ -58,6 +58,8 @@ export function createTrafficRadar({ platform = process.platform, now = Date.now
|
|
|
58
58
|
const seen = new Map();
|
|
59
59
|
let alerts = [];
|
|
60
60
|
let serial = 0;
|
|
61
|
+
let configRevision = 0;
|
|
62
|
+
let savedConfig = null;
|
|
61
63
|
const addAlert = (message, at) => {
|
|
62
64
|
alerts.unshift({ id: ++serial, message, at });
|
|
63
65
|
alerts = alerts.slice(0, 8);
|
|
@@ -116,10 +118,19 @@ export function createTrafficRadar({ platform = process.platform, now = Date.now
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
return {
|
|
121
|
+
updateConfig(configuration) {
|
|
122
|
+
configRevision++;
|
|
123
|
+
savedConfig = configuration;
|
|
124
|
+
if (cached) cached = { ...cached, config: configuration };
|
|
125
|
+
},
|
|
119
126
|
read() {
|
|
120
127
|
if (pending) return pending;
|
|
121
128
|
if (cached && now() - cached.sampledAt < 3_000) return Promise.resolve(cached);
|
|
122
|
-
|
|
129
|
+
const observedRevision = configRevision;
|
|
130
|
+
pending = sample().then(result => {
|
|
131
|
+
cached = observedRevision === configRevision ? result : { ...result, config: savedConfig };
|
|
132
|
+
return cached;
|
|
133
|
+
}).finally(() => { pending = null; });
|
|
123
134
|
return pending;
|
|
124
135
|
},
|
|
125
136
|
};
|
|
@@ -12,6 +12,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
13
|
import { deckProviders } from "./deck-scope.mjs";
|
|
14
14
|
import { send } from "./http-io.mjs";
|
|
15
|
+
import { quotaHistoryReport } from "./quota-history.mjs";
|
|
15
16
|
import { validSessionLookup } from "./ccusage-session.mjs";
|
|
16
17
|
|
|
17
18
|
// Resolved the way pinned-build.mjs resolves it, from a file in the same
|
|
@@ -119,3 +120,14 @@ export async function handleCcusageSession(req, res) {
|
|
|
119
120
|
const { fetchCcusageSession } = await import(pathToFileURL(join(PKG_ROOT, "src/server/ccusage.mjs")).href);
|
|
120
121
|
send(res, 200, await fetchCcusageSession({ agent, sessionId, speed, force: url.searchParams.get("refresh") === "1" }));
|
|
121
122
|
}
|
|
123
|
+
|
|
124
|
+
/** A local history read; never fetches provider quota. */
|
|
125
|
+
export async function handleQuotaHistory(req, res) {
|
|
126
|
+
const url = new URL(req.url, "http://localhost");
|
|
127
|
+
const provider = url.searchParams.get("provider");
|
|
128
|
+
const account = url.searchParams.get("account");
|
|
129
|
+
if (!["claude", "codex"].includes(provider) || !/^[a-f0-9]{64}$/.test(account ?? "")) {
|
|
130
|
+
return send(res, 400, { ok: false, reason: "bad_history_scope" });
|
|
131
|
+
}
|
|
132
|
+
send(res, 200, await quotaHistoryReport(provider, account));
|
|
133
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{j as e,r,l as Q,m as _,n as Y,v as z,o as V,q as X,t as Z,w as ee,s as se,u as te,d as ne,x as ae,y as ie,h as re}from"./index-BepyDfFS.js";function oe({snap:s}){return e.jsxs("section",{className:"bw-feed",children:[e.jsxs("h4",{className:"bw-sec-head bw-head-row",children:["Live activity",e.jsxs("span",{className:"bw-feed-count",children:[s.log.length," ",s.log.length===1?"event":"events"]})]}),e.jsx("p",{className:"bw-since bw-feed-note",children:"Pages you opened yourself — counted to run the rule, never written down. Addresses appear under Findings, and only for pages a program opened."}),e.jsx("div",{className:"bw-log","aria-live":"polite","aria-relevant":"additions",children:s.log.length===0?e.jsx("p",{className:"bw-note",children:"The deck writes a line here each time it looks at a profile."}):s.log.map(a=>e.jsxs("div",{className:`bw-log-line ${a.level}${a.parts?"":" sys"}`,children:[e.jsx("span",{className:"bw-log-time",children:new Date(a.atMs).toLocaleTimeString("en-GB",{hour12:!1})}),a.parts?e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"bw-log-browser",children:a.parts.browser}),e.jsx("span",{className:"bw-log-profile",title:a.parts.profile,children:a.parts.profile}),e.jsxs("span",{className:"bw-log-value",children:[a.parts.value,a.parts.flagged>0&&e.jsxs("span",{className:"bw-log-flag",children:[" · ",a.parts.flagged," flagged"]})]})]}):e.jsx("span",{className:"bw-log-sys",children:a.text})]},`${a.atMs}-${a.level}-${a.text}`))})]})}const E=s=>`${s.host}-${s.startMs}`;function le(s){const a=h=>new Date(h).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1});return s.count===1||s.endMs-s.startMs<6e4?a(s.startMs):`${a(s.startMs)} → ${a(s.endMs)}`}function ce(s){return new Date(s).toLocaleDateString(void 0,{weekday:"short",day:"numeric",month:"short"})}function U(s){const a=Math.round((s.endMs-s.startMs)/6e4);return a<1?"":`${a} min`}function de({snap:s,dismiss:a,refreshRef:h}){const[c,d]=r.useState(null),u=r.useRef(new Map),t=r.useRef(null);r.useEffect(()=>{var i,w;const o=t.current;!o||s.episodes.some(b=>E(b)===o.id)||(t.current=null,Q(((i=document.activeElement)==null?void 0:i.tagName)??null)&&((w=(o.next?u.current.get(o.next):void 0)??h.current)==null||w.focus()))},[s]);const n=o=>{t.current={id:E(o),next:_(s.episodes.map(E),E(o))},a(o)},l=r.useMemo(()=>{const o=[];for(const i of s.episodes){const w=ce(i.startMs),b=o[o.length-1];b&&b.label===w?b.episodes.push(i):o.push({label:w,episodes:[i]})}return o},[s]);return e.jsxs("section",{className:"bw-findings",children:[e.jsxs("h4",{className:"bw-sec-head bw-head-row",children:["Findings",s.episodes.length>0&&e.jsxs("span",{className:"bw-feed-count",children:[s.episodes.length," ",s.episodes.length===1?"episode":"episodes"]})]}),e.jsx("div",{className:"bw-eps",children:l.length===0?e.jsxs("div",{className:"bw-empty",children:[e.jsx("p",{className:"bw-empty-head",children:"Nothing found yet"}),e.jsxs("p",{className:"bw-empty-note",children:["An episode lands here when a program opens pages in a browser nobody has touched for"," ",s.settings.quietMinutes," ",s.settings.quietMinutes===1?"minute":"minutes",". On most machines that is rare, so an empty list is the ordinary result rather than a sign something is wrong."]}),!s.settings.enabled&&e.jsx("p",{className:"bw-empty-note",children:"Watching is off, so this shows only what this deck has seen since it started. Anything an earlier run recorded comes back when you switch it on."})]}):l.map(o=>e.jsxs("div",{className:"bw-day",children:[e.jsx("h4",{className:"bw-sec-head",children:o.label}),o.episodes.map(i=>{const w=E(i),b=c===w;return e.jsxs("div",{className:`bw-ep${b?" open":""}`,children:[e.jsxs("div",{className:"bw-ep-row",children:[e.jsxs("button",{className:"bw-ep-head",ref:p=>{p?u.current.set(w,p):u.current.delete(w)},onClick:()=>d(b?null:w),"aria-expanded":b,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:b?"▾":"▸"}),e.jsx("span",{className:"bw-ep-host",children:i.host}),e.jsxs("span",{className:"bw-ep-meta",children:[le(i),U(i)&&e.jsxs(e.Fragment,{children:[" · ",U(i)]}),i.provisional&&e.jsxs(e.Fragment,{children:[" · ",e.jsx("span",{title:"Its quiet window has not closed yet. If somebody used the browser in the minutes after it, it is withdrawn; until then it is not written down and no reaction runs.",children:"provisional"})]})]}),e.jsxs("span",{className:"bw-ep-count",children:[i.count," ",i.count===1?"page":"pages"]})]}),e.jsx("button",{className:"glyph-btn bw-ep-x",onClick:()=>n(i),"aria-label":`Dismiss ${i.host}`,title:"Dismiss — it leaves this list for good, and stays in the log file",children:"×"})]}),b&&e.jsx("ul",{className:"bw-urls",children:i.urls.map((p,y)=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-url-time",children:new Date(p.timeMs).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1})}),e.jsx("span",{className:"bw-url",children:p.url})]},`${p.url}-${p.timeMs}-${y}`))})]},w)})]},o.label))})]})}function he(s){const a=/^#?([0-9a-f]{6})$/i.exec((s??"").trim());if(!a)return!1;const h=parseInt(a[1],16),c=[h>>16&255,h>>8&255,h&255].map(d=>{const u=d/255;return u<=.03928?u/12.92:Math.pow((u+.055)/1.055,2.4)});return .2126*c[0]+.7152*c[1]+.0722*c[2]>.5}const G=1e4,ue=12e3;function we({browsers:s,findings:a,watching:h,palette:c}){const d=r.useRef(null),u=r.useRef(null);return u.current={browsers:s,findings:a,watching:h},r.useEffect(()=>{var b;const t=d.current;if(!t)return;const n=t.getContext("2d");if(!n)return;const l=((b=window.matchMedia)==null?void 0:b.call(window,"(prefers-reduced-motion: reduce)").matches)??!1;let o=0,i=!1;const w=()=>{var q;if(i)return;const p=u.current;if(!p)return;const{browsers:y,findings:j,watching:P}=p,S=c["--accent"]||"#7dd3fc",g=c["--line"]||"#1f2229",v=c["--text-dim"]||"#6e727c",$=he(c["--bg"]),N=$?42:26,k=$?.85:.65,A=Date.now(),R=Math.min(window.devicePixelRatio||1,2),C=t.clientWidth,W=t.clientHeight;(t.width!==C*R||t.height!==W*R)&&(t.width=C*R,t.height=W*R),n.setTransform(R,0,0,R,0,0),n.clearRect(0,0,C,W);const m=C/2,x=W/2,M=Math.min(C,W)/2-6;n.strokeStyle=g,n.lineWidth=1,n.globalAlpha=$?1:.9;for(const f of[.4,.7,1])n.beginPath(),n.arc(m,x,M*f,0,Math.PI*2),n.stroke();n.beginPath(),n.moveTo(m-M,x),n.lineTo(m+M,x),n.moveTo(m,x-M),n.lineTo(m,x+M),n.stroke(),n.globalAlpha=1;const I=(P&&!l?A%G/G:0)*Math.PI*2-Math.PI/2;if(P){const f=(q=n.createConicGradient)==null?void 0:q.call(n,I-.85,m,x);f&&(f.addColorStop(0,"transparent"),f.addColorStop(.85/(Math.PI*2),`color-mix(in srgb, ${S} ${N}%, transparent)`),f.addColorStop(1,"transparent"),n.fillStyle=f,n.beginPath(),n.moveTo(m,x),n.arc(m,x,M,I-.85,I),n.closePath(),n.fill()),n.strokeStyle=S,n.globalAlpha=k,n.beginPath(),n.moveTo(m,x),n.lineTo(m+Math.cos(I)*M,x+Math.sin(I)*M),n.stroke(),n.globalAlpha=1}y.forEach((f,T)=>{const B=T/Math.max(y.length,1)*Math.PI*2-Math.PI/2,D=f.lastReadMs===null?1:Math.min(1,(A-f.lastReadMs)/12e4),O=M*(.42+D*.5),F=m+Math.cos(B)*O,L=x+Math.sin(B)*O,J=Math.abs((I-B+Math.PI*3)%(Math.PI*2)-Math.PI),K=P?Math.max(0,1-J/.55):0;n.beginPath(),n.arc(F,L,3+K*1.6,0,Math.PI*2),n.fillStyle=f.running?S:v,n.globalAlpha=f.running?.55+K*.45:.35,n.fill(),n.globalAlpha=1});for(const f of j){const T=(A-f.atMs)/ue;if(T<0||T>1)continue;const D=Math.max(0,y.findIndex(L=>L.key===f.browser))/Math.max(y.length,1)*Math.PI*2-Math.PI/2,O=m+Math.cos(D)*M*.6,F=x+Math.sin(D)*M*.6;n.beginPath(),n.arc(O,F,4+T*22,0,Math.PI*2),n.strokeStyle=S,n.globalAlpha=(1-T)*.8,n.lineWidth=1.5,n.stroke(),n.globalAlpha=1}o=requestAnimationFrame(w)};return w(),()=>{i=!0,cancelAnimationFrame(o)}},[c]),e.jsx("canvas",{ref:d,className:"bw-radar",role:"img","aria-label":h?`Watching ${s.length} browser${s.length===1?"":"s"}`:"Not watching"})}function me({snap:s,watching:a,palette:h}){const[c,d]=r.useState(!1),[u,t]=r.useState(()=>Date.now());r.useEffect(()=>{const l=setInterval(()=>t(Date.now()),1e3);return()=>clearInterval(l)},[]);const n=Y(s.coverage.lastHumanMs,s.coverage.quietMs,u);return e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Activity overview",e.jsx("button",{className:"bw-help",onClick:()=>d(l=>!l),"aria-expanded":c,"aria-label":"What the sweep and the dots mean",children:"?"})]}),e.jsxs("div",{className:"bw-glance",children:[e.jsx(we,{browsers:a.map(l=>{var o;return{key:l.key,name:l.name,running:l.running,lastReadMs:((o=s.profiles.find(i=>i.browser===l.key))==null?void 0:o.lastWrittenMs)??null}}),findings:s.episodes.slice(0,6).map(l=>({browser:l.browser??null,atMs:l.endMs})),watching:s.settings.enabled,palette:h}),e.jsx("ul",{className:"bw-counts",children:z(a,s.profiles).map(l=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-count-n",children:l.visits.toLocaleString("en-US")}),e.jsx("span",{className:"bw-count-of",children:l.name})]},l.key))})]}),e.jsx("p",{className:"bw-since",children:"Pages a program opened, since this deck started"}),s.coverage.checkedMs>0&&e.jsxs("p",{className:"bw-since",children:["Last checked ",V(s.coverage.checkedMs,u),s.coverage.checks>0&&e.jsxs(e.Fragment,{children:[" · ",s.coverage.checks.toLocaleString("en-US")," ",s.coverage.checks===1?"check":"checks"]})]}),n!==null&&e.jsxs("p",{className:"bw-gate",children:["You are browsing — a program page would count in ",X(n)]}),c&&e.jsxs("dl",{className:"bw-key",children:[e.jsx("dt",{children:"Sweep"}),e.jsx("dd",{children:"one poll of every watched profile"}),e.jsx("dt",{children:"Dot"}),e.jsx("dd",{children:"a browser, further out the longer since it last wrote history"}),e.jsx("dt",{children:"Ring"}),e.jsx("dd",{children:"a finding, leaving the browser it came from"})]})]})}function be({watching:s,rest:a}){const[h,c]=r.useState(!1),[d,u]=r.useState(!1);return e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Watched profiles",e.jsx("button",{className:"bw-help",onClick:()=>c(t=>!t),"aria-expanded":h,"aria-label":"What running, idle and connected to Anthropic mean",children:"?"})]}),h&&e.jsxs("dl",{className:"bw-key",children:[e.jsx("dt",{children:"Running"}),e.jsx("dd",{children:"the browser has a process on this machine right now"}),e.jsx("dt",{children:"Connected"}),e.jsx("dd",{children:"an open connection to an address Anthropic's relay uses — which it shares with claude.ai, so an open tab looks the same as an agent channel"})]}),e.jsx("ul",{className:"bw-profiles",children:s.map(t=>e.jsxs("li",{children:[e.jsx("span",{className:`bw-prof-dot${t.running?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-prof-name",children:t.name}),e.jsx("span",{className:"bw-prof-state",children:t.running?"running":"idle"}),t.running&&t.relay.state==="live"&&e.jsx("span",{className:"bw-relay",children:"connected to Anthropic"})]},t.key))}),a.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"bw-why bw-rest-head",onClick:()=>u(t=>!t),"aria-expanded":d,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:d?"▾":"▸"})," ",a.length," not watched"]}),d&&e.jsxs("p",{className:"bw-rest",children:[a.map(t=>t.name).join(", ")," — not installed, or installed and never opened, so there is no history to read."]})]})]})}function pe({snap:s,quiet:a,setQuiet:h,save:c,access:d,setAccess:u}){return e.jsxs("div",{className:"bw-settings",children:[e.jsxs("label",{title:"A program opening a page counts as a finding only if nobody had touched the browser for this long. Shorter catches more and reports more of your own work; longer is quieter.",children:[e.jsx("span",{children:"Nobody browsing for"}),e.jsxs("select",{value:a??s.settings.quietMinutes,onChange:t=>{h(Number(t.target.value)),c({quietMinutes:Number(t.target.value)})},children:[e.jsx("option",{value:1,children:"1 min"}),e.jsx("option",{value:5,children:"5 min"}),e.jsx("option",{value:15,children:"15 min"}),e.jsx("option",{value:30,children:"30 min"}),e.jsx("option",{value:60,children:"60 min"})]})]}),e.jsxs("label",{children:[e.jsx("span",{children:"When it finds one"}),e.jsx("select",{value:s.settings.reaction,onChange:t=>void c({reaction:t.target.value}),disabled:!s.settings.enabled,title:s.settings.enabled?"What to do besides writing it down.":"Turn watching on to arm a reaction.",children:(s.reactions??["notify"]).map(t=>e.jsx("option",{value:t,children:t==="notify"?"notify me":t==="close-tab"?"close the tab":"quit the browser"},t))})]}),e.jsxs("div",{className:"bw-settings-note",children:[e.jsxs("p",{children:["Chrome marks a navigation that came from an extension or a command rather than from a click. These are the ones that happened while nobody had touched the browser for the time above. ",e.jsx("strong",{children:"Usually that is your own agent doing what you asked."})]}),e.jsxs("button",{className:"bw-why",onClick:()=>u(t=>!t),"aria-expanded":d,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:d?"▾":"▸"})," What Browser Watch can access"]}),d&&e.jsxs("dl",{className:"bw-access",children:[e.jsx("dt",{children:"Reads"}),e.jsxs("dd",{children:["A copy of each browser's own history database — the live file is locked while the browser holds it. Only rows newer than the moment this deck started:"," ",new Date(s.coverage.startedMs).toLocaleString(),"."]}),e.jsx("dt",{children:"Keeps"}),e.jsxs("dd",{children:["Only while the switch is on, and only the episodes it flagged — never your ordinary browsing. In ",e.jsx("code",{className:"bw-path",children:s.coverage.logPath}),typeof s.coverage.logBytes=="number"?` (${Z(s.coverage.logBytes)})`:"",", with every address written in full so you can check it yourself."]}),e.jsx("dt",{children:"Sends"}),e.jsx("dd",{children:"Nothing. No part of this reads or writes over the network; the deck serves on 127.0.0.1 and this panel talks only to it."}),e.jsx("dt",{children:"Never reads"}),e.jsx("dd",{children:"Anything from before this deck started, cookies, saved passwords, page contents, or any browser profile with no history file."})]})]})]})}function H(s,a){return a?{kind:"saving",word:"Saving"}:s.settings.enabled?{kind:"on",word:"Watching"}:{kind:"off",word:"Paused"}}function ge({snap:s,saving:a,save:h,why:c,setWhy:d}){return e.jsxs("footer",{className:"bw-status",children:[e.jsx("span",{className:`bw-mode-dot${s.settings.enabled?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-mode-word",children:H(s,a).word},H(s,a).kind),e.jsx("span",{className:"bw-status-text",children:s.coverage.archived===0?s.settings.enabled?"No episodes recorded yet":"No episodes on disk":`${s.coverage.archived} ${s.coverage.archived===1?"episode":"episodes"} on disk`}),e.jsx("button",{className:`glyph-btn bw-gear${c?" on":""}`,onClick:()=>d(u=>!u),"aria-expanded":c,"aria-label":"Settings",title:"Settings",children:e.jsxs("svg",{width:"13",height:"13",viewBox:"0 0 14 14",fill:"none",stroke:"currentColor",strokeWidth:"1.3","aria-hidden":!0,children:[e.jsx("circle",{cx:"7",cy:"7",r:"2.1"}),e.jsx("path",{d:"M7 1.2v1.6M7 11.2v1.6M1.2 7h1.6M11.2 7h1.6M2.9 2.9l1.1 1.1M10 10l1.1 1.1M11.1 2.9L10 4M4 10l-1.1 1.1"})]})}),e.jsxs("label",{className:"bw-switch",htmlFor:"bw-enabled",children:[e.jsx("span",{className:"bw-switch-label",id:"bw-enabled-label",children:"Watch browser activity"}),e.jsx("button",{id:"bw-enabled",type:"button",role:"switch","aria-checked":s.settings.enabled,"aria-labelledby":"bw-enabled-label",className:"switch",onClick:()=>void h({enabled:!s.settings.enabled}),title:s.settings.enabled?"On — every episode it finds is written down, so the list outlives the browsing history being cleared":"Off — showing only what this deck has seen since it started. Anything an earlier run archived is hidden until you switch back on, and nothing new is kept",children:e.jsx("span",{className:"switch-knob"})})]})]})}function fe({relay:s}){const[a,h]=r.useState(!1),[c,d]=r.useState(!1),u=s.killswitch.blocked?"unblock":"block",t=s.command[u],n=r.useCallback(async()=>{await ee(t.command)&&(d(!0),window.setTimeout(()=>d(!1),1600))},[t.command]),l=s.profiles.filter(i=>{var w;return(w=i.report)==null?void 0:w.present}),o=s.profiles.filter(i=>i.report===null);return e.jsxs("section",{className:"bw-sec",children:[e.jsx("h4",{className:"bw-sec-head",children:"Remote control"}),e.jsx("p",{className:`bw-rc-verdict ${s.verdict}`,role:"status",children:s.verdict==="exposed"?"Claude in Chrome is live here, so any Claude Code session signed in to your Anthropic account can drive this browser — from any machine, with no prompt on this one.":s.verdict==="protected"?`The relay is black-holed in ${s.hostsPath}, so no new connection to it can be made from this machine.`:"Claude in Chrome is not installed and enabled in any profile here, so there is nothing for the relay to drive."}),s.killswitch.foreign.length>0&&e.jsxs("p",{className:"bw-rc-foreign",children:["Another entry for this host is already in ",s.hostsPath,", and ccdeck will not touch it:",s.killswitch.foreign.map(i=>e.jsx("code",{className:"bw-path",children:i},i))]}),e.jsxs("button",{className:"bw-why",onClick:()=>h(i=>!i),"aria-expanded":a,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:a?"▾":"▸"})," ",s.verdict==="protected"?"How to undo the block":"What it can reach, and how to stop it"]}),a&&e.jsxs("div",{className:"bw-rc-body",children:[l.length>0&&e.jsx("dl",{className:"bw-key",children:l.map(i=>e.jsxs(r.Fragment,{children:[e.jsxs("dt",{children:[i.name,i.profile==="Default"?"":` · ${i.profile}`]}),e.jsxs("dd",{children:[i.report.enabled?"enabled":"installed but switched off",i.report.allUrls?", can act on every site":", limited to the sites it was granted",i.report.sensitiveApis.length>0&&e.jsxs(e.Fragment,{children:[" — ",i.report.sensitiveApis.join(", ")]})]})]},`${i.browser}/${i.profile}`))}),o.length>0&&e.jsxs("p",{className:"bw-rc-note",children:[o.map(i=>i.name).join(", ")," — its settings file could not be read, so nothing is claimed about it either way."]}),!s.hostsRead&&e.jsxs("p",{className:"bw-rc-note",children:[s.hostsPath," could not be read, so whether the relay is already blocked is unknown."]}),l.some(i=>i.report.allUrls)&&e.jsxs("p",{className:"bw-rc-note",children:["Narrowing the sites in ",e.jsx("code",{className:"bw-path",children:"chrome://extensions"})," does not change this: the grant is held at the browser level and the per-site list is enforced inside the extension, by the extension."]}),e.jsxs("div",{className:"bw-rc-cmd",children:[e.jsx("code",{children:t.command}),e.jsx("button",{className:"btn bw-rc-copy",onClick:n,children:c?"Copied":"Copy"})]}),e.jsx("p",{className:"bw-rc-note",children:t.note})]})]})}function xe(s){const[a,h]=r.useState(null),[c,d]=r.useState(null),[u,t]=r.useState(null),[n,l]=r.useState(!1),o=r.useRef(!1),[i,w]=r.useState(null),[b,p]=r.useState(!1),y=r.useRef(!1),j=r.useCallback(async g=>{var v,$;if(!(g&&!se(o.current))){g&&(o.current=!0,l(!0));try{const N=await fetch(`/api/browser-watch${g?"?refresh=1":""}`);if(!N.ok)throw new Error(`the deck answered ${N.status}`);const k=await N.json();h(k),y.current||w(((v=k==null?void 0:k.settings)==null?void 0:v.quietMinutes)??15),s((($=k==null?void 0:k.settings)==null?void 0:$.enabled)===!0),d(null)}catch(N){d(N instanceof Error?N.message:String(N))}finally{g&&(o.current=!1,l(!1))}}},[s]);r.useEffect(()=>{j(!1)},[j]),r.useEffect(()=>{const g=setInterval(()=>{j(!1)},1e4);return()=>clearInterval(g)},[j]);const P=r.useCallback(async g=>{try{await fetch("/api/browser-watch/dismiss",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({host:g.host,startMs:g.startMs})})}finally{await j(!1)}},[j]),S=r.useCallback(async g=>{y.current=!0,p(!0);try{const v=await fetch("/api/browser-watch",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(g)});if(!v.ok)throw new Error(`the deck answered ${v.status}`);await j(!1),t(null)}catch(v){t(v instanceof Error?v.message:String(v))}finally{y.current=!1,p(!1)}},[j]);return{snap:a,error:c,writeError:u,setWriteError:t,busy:n,quiet:i,setQuiet:w,saving:b,load:j,dismiss:P,save:S}}function ye({onClose:s,onSeen:a,onWatching:h,palette:c}){const d=te(s),u=ne(s),{snap:t,error:n,writeError:l,setWriteError:o,busy:i,quiet:w,setQuiet:b,saving:p,load:y,dismiss:j,save:P}=xe(h),[S,g]=r.useState(!1),[v,$]=r.useState(!1),N=r.useRef(null),k=r.useRef(a);k.current=a,r.useEffect(()=>()=>k.current(Date.now()),[]);const A=ae(t==null?void 0:t.browsers),R=((t==null?void 0:t.browsers)??[]).filter(m=>!(m.installed&&m.profiles>0)),C=t?ie(t):null,W=A.reduce((m,x)=>m+x.profiles,0);return e.jsx("div",{className:"modal-backdrop",...u,role:"presentation",children:e.jsxs("div",{ref:d,className:"modal bw-modal",onClick:m=>m.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":"bw-title",children:[e.jsxs("header",{className:"modal-head",children:[e.jsxs("div",{className:"modal-title",children:[e.jsx("span",{className:"modal-tool-name",id:"bw-title",children:"Browser watch"}),e.jsx("span",{className:"modal-tool-id",children:t?`${W} ${W===1?"profile":"profiles"} · local only`:"local only"})]}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{ref:N,className:"glyph-btn",onClick:()=>void y(!0),...re(i),"aria-label":"Re-read every profile now",title:"Re-read every profile now",children:i?"…":"↻"}),e.jsx("button",{className:"glyph-btn",onClick:s,"aria-label":"Close (Esc)",title:"Close (Esc)",children:"×"})]})]}),e.jsxs("div",{className:"modal-body bw-body",children:[!t&&!n&&e.jsxs("div",{className:"bw-loading",children:[e.jsx("span",{className:"bw-loading-bar","aria-hidden":!0}),e.jsx("p",{children:"Reading each browser's history. The first look copies the file, which takes a moment."})]}),n&&e.jsx("div",{className:"bw-state",children:e.jsxs("div",{className:"bw-row err",children:[e.jsx("span",{className:"bw-dot","aria-hidden":!0}),e.jsx("span",{className:"bw-row-label",children:"Could not read"}),e.jsx("span",{className:"bw-row-detail",children:n})]})}),t&&C&&e.jsx("p",{className:`bw-trouble ${C.kind}`,role:"status",children:C.text}),t&&e.jsxs("div",{className:"bw-work",children:[e.jsxs("aside",{className:"bw-side",children:[e.jsx(me,{snap:t,watching:A,palette:c}),e.jsx(be,{watching:A,rest:R}),t.relay&&e.jsx(fe,{relay:t.relay})]}),e.jsxs("div",{className:"bw-main",children:[e.jsx(de,{snap:t,dismiss:j,refreshRef:N}),e.jsx(oe,{snap:t})]})]}),t&&S&&e.jsx(pe,{snap:t,quiet:w,setQuiet:b,save:P,access:v,setAccess:$}),l&&e.jsx("div",{className:"bw-state bw-write-err",role:"alert",children:e.jsxs("div",{className:"bw-row err",children:[e.jsx("span",{className:"bw-dot","aria-hidden":!0}),e.jsx("span",{className:"bw-row-label",children:"Could not save that setting"}),e.jsxs("span",{className:"bw-row-detail",children:[l," — the switches below show what is stored, not what you pressed."]}),e.jsx("button",{className:"glyph-btn bw-write-err-x",onClick:()=>o(null),"aria-label":"Dismiss",title:"Dismiss",children:"×"})]})}),t&&e.jsx(ge,{snap:t,saving:p,save:P,why:S,setWhy:g})]})]})})}export{ye as default};
|