This commit is contained in:
BhagwanSingh 2025-11-10 12:27:22 +05:30
commit bb8b654e93
15 changed files with 457 additions and 198 deletions

View File

@ -5,10 +5,10 @@
<!-- <link rel="icon" type="image/svg+xml" href="" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VA Tracker</title>
<meta
<!-- <meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';"
/>
/> -->
</head>
<body>
<div id="root"></div>

182
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "va-tracker",
"version": "0.0.0",
"dependencies": {
"@reduxjs/toolkit": "^2.10.1",
"@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.2",
"chalk": "^5.6.2",
@ -17,6 +18,7 @@
"os-utils": "^0.0.14",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.5",
"recharts": "^2.12.7",
"sharp": "^0.34.4",
@ -79,6 +81,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz",
"integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.2",
@ -2068,6 +2071,32 @@
"node": ">=18"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz",
"integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^10.2.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
@ -2272,6 +2301,18 @@
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -2747,13 +2788,14 @@
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"dev": true
"devOptional": true
},
"node_modules/@types/react": {
"version": "18.3.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
"dev": true,
"devOptional": true,
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@ -2777,6 +2819,12 @@
"@types/node": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@types/verror": {
"version": "1.10.10",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz",
@ -2983,6 +3031,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -3171,7 +3220,6 @@
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.4",
@ -3190,7 +3238,6 @@
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"peer": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
@ -3212,7 +3259,6 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -3227,15 +3273,13 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/archiver-utils/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@ -3479,6 +3523,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
@ -4049,7 +4094,6 @@
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
@ -4231,7 +4275,6 @@
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
},
@ -4244,7 +4287,6 @@
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
@ -4656,6 +4698,7 @@
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.5.tgz",
"integrity": "sha512-ocnZV44ZqInoSFaY54fF7BlCtw+WtbrjyPrkBhaB+Ztn7GPKjmFgRbIKytifJ8h9Cib8jdFRMgjCUtkU45Y6DA==",
"dev": true,
"peer": true,
"dependencies": {
"app-builder-lib": "25.0.5",
"builder-util": "25.0.3",
@ -4879,7 +4922,6 @@
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.5.tgz",
"integrity": "sha512-N2U7LGSdt4hmEhjEeIV2XJbjj2YIrTL6enfsGKfOhGTpL6GEejUmT3gjdKUqKBS5+NBx0GWhnEwD3MpO2P6Nfg==",
"dev": true,
"peer": true,
"dependencies": {
"app-builder-lib": "25.0.5",
"archiver": "^5.3.1",
@ -4892,7 +4934,6 @@
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@ -4907,7 +4948,6 @@
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"peer": true,
"dependencies": {
"universalify": "^2.0.0"
},
@ -4920,7 +4960,6 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"peer": true,
"engines": {
"node": ">= 10.0.0"
}
@ -5202,15 +5241,6 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"devOptional": true
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -5633,8 +5663,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/fs-extra": {
"version": "8.1.0",
@ -6540,6 +6569,16 @@
}
]
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@ -6887,8 +6926,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/isbinaryfile": {
"version": "5.0.2",
@ -7149,7 +7187,6 @@
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true,
"peer": true,
"dependencies": {
"readable-stream": "^2.0.5"
},
@ -7162,7 +7199,6 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -7177,15 +7213,13 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@ -7463,36 +7497,31 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/log-symbols": {
"version": "4.1.0",
@ -8132,7 +8161,6 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -8757,8 +8785,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/progress": {
"version": "2.0.3",
@ -8839,6 +8866,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -8850,6 +8878,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@ -8863,6 +8892,30 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@ -9001,7 +9054,6 @@
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
}
@ -9011,7 +9063,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"peer": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
@ -9049,6 +9100,22 @@
"decimal.js-light": "^2.4.1"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@ -9107,6 +9174,12 @@
"url": "https://github.com/sponsors/jet2jet"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -9967,7 +10040,6 @@
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"peer": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@ -10337,6 +10409,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/utf8-byte-length": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
@ -10399,6 +10480,7 @@
"version": "5.4.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@ -10831,7 +10913,6 @@
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2",
@ -10846,7 +10927,6 @@
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
"peer": true,
"dependencies": {
"glob": "^7.2.3",
"graceful-fs": "^4.2.0",

View File

@ -18,6 +18,7 @@
"test:unit": "vitest src"
},
"dependencies": {
"@reduxjs/toolkit": "^2.10.1",
"@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.2",
"chalk": "^5.6.2",
@ -27,6 +28,7 @@
"os-utils": "^0.0.14",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.5",
"recharts": "^2.12.7",
"sharp": "^0.34.4",

View File

@ -1,54 +1,55 @@
// electron/ipc/tokenHandler.ts
import { ipcMain } from 'electron';
import { getToken, saveToken, clearToken } from '../utils/tokenHelper.js';
import { getActiveApp, handleLogin, takeScreenshot } from '../utils/utilityFunctions.js';
import api from '../api/axiosClient.js';
import { ipcMain, BrowserWindow } from "electron";
import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js";
import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling } from "../utils/utilityFunctions.js";
import api from "../api/axiosClient.js";
import { getMainWindow } from '../main.js';
import { exec } from 'child_process';
ipcMain.handle('get-token', async (_, TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(TOKEN_KEY);
});
export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
console.log("⚙️ Registering IPC Handlers...");
ipcMain.handle('save-token', async (_, TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN", token: string) => {
await saveToken(TOKEN_KEY, token);
ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(key);
});
ipcMain.handle("save-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN", token: string) => {
await saveToken(key, token);
return true;
});
});
ipcMain.handle('clear-token', async (_, TOKEN_KEY: string) => {
await clearToken(TOKEN_KEY);
ipcMain.handle("clear-token", async (_, key: string) => {
await clearToken(key);
return true;
});
});
ipcMain.handle("upload-screenshot", async () => {
await takeScreenshot();
return { success: true };
});
ipcMain.handle('upload-screenshot', async () => {
takeScreenshot()
})
// Handle login request from renderer
ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => {
ipcMain.handle("login-request", async (_, credentials: { email: string; password: string }) => {
return handleLogin(credentials);
});
});
ipcMain.handle('logout-request', async () => {
ipcMain.handle("logout-request", async () => {
try {
console.log("Handle Logout listened")
console.log("Token", await getToken("ACCESS_TOKEN"))
const res = await api.get("/logout");
if (res && ("success" in res) && res.success) {
return { success: true, message: "Logout Successfully" }
}
return { success: false, message: "" }
} catch (error) {
return { success: false, message: "" }
console.log("Handle Logout listened");
console.log("Token", await getToken("ACCESS_TOKEN"));
const res = await api.get("/logout");
if (res && "success" in res && res.success) {
return { success: true, message: "Logout Successfully" };
}
return { success: false, message: "" };
} catch {
return { success: false, message: "Logout failed" };
}
});
});
ipcMain.handle('get-active-app', () => {
return getActiveApp();
});
ipcMain.handle("get-active-app", async () => {
return await getActiveApp();
});
ipcMain.handle('toggle-close-btn', (event, shouldEnable: boolean) => {
ipcMain.handle('toggle-close-btn', (event, shouldEnable: boolean) => {
const mainWindow = getMainWindow()
console.log("toggle-close-btn Called", mainWindow)
if (mainWindow && !mainWindow.isDestroyed()) {
@ -59,26 +60,11 @@ ipcMain.handle('toggle-close-btn', (event, shouldEnable: boolean) => {
ipcMain.handle('resize-window', (event, width: number, height: number) => {
const mainWindow = getMainWindow()
console.log("toggle-close-btn Called", mainWindow)
if (mainWindow && !mainWindow.isDestroyed()) {
// if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.setSize(width, height);
}
});
// }
})
ipcMain.handle('get-running-apps', async () => {
return new Promise((resolve, reject) => {
exec('tasklist', (err, stdout) => {
if (err) return reject(err);
const lines = stdout.split('\n').slice(3); // skip headers
const apps = lines
.map(line => line.trim().split(/\s+/))
.filter(parts => parts.length > 1)
.map(parts => ({
name: parts[0],
pid: parts[1],
}));
resolve(apps);
});
});
});
// ✅ Start background polling when main window is ready
};
// startWindowPolling(mainWindow);

View File

@ -4,6 +4,7 @@ import { getStaticData, pollResources } from './resourceManager.js';
import { getPreloadPath, getUIPath } from './pathResolver.js';
import "./ipc/ipcHandler.js";
import { registerIPCHandlers } from './ipc/ipcHandler.js';
let mainWindow:any = null;
@ -14,7 +15,7 @@ app.on('ready', async () => {
width: 500, // fixed width
height: 600, // fixed height
resizable: false, // prevents resizing
resizable: true, // prevents resizing
fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar)
minimizable: false, // disables minimize button
maximizable: false, // prevents maximize
@ -41,6 +42,10 @@ app.on('ready', async () => {
}
// pollResources(mainWindow);
// 🔹 Register all IPC handlers (includes polling)
registerIPCHandlers(mainWindow);
pollResources(mainWindow);
// IPC handlers
// ipcMainHandle('getStaticData', () => getStaticData());

View File

@ -10,7 +10,8 @@ contextBridge.exposeInMainWorld('electron', {
getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'),
toggleCloseBtn: (shouldEnable: boolean) => ipcRenderer.invoke('toggle-close-btn', shouldEnable),
resizeWindow: (width: number, height: number) => ipcRenderer.invoke('resize-window', width, height),
getRunningApps: () => ipcRenderer.invoke('get-running-apps'),
onWindowUpdate: (callback) =>
ipcRenderer.on("window:updated", (_, data) => callback(data)),
} satisfies Window['electron']);
// function ipcInvoke<Key extends keyof EventPayloadMapping>(

View File

@ -1,4 +1,4 @@
import { desktopCapturer } from "electron";
import { BrowserWindow, desktopCapturer } from "electron";
import * as fs from "fs";
import path from "path";
import axios from 'axios';
@ -95,7 +95,23 @@ export const handleLogin = async (credentials: { email: string, password: string
}
}
// Get active window info with timestamp
export const getActiveApp = async () => {
const result = await activeWindow();
return result;
const result = await activeWindow();
const dataWithTime = { ...result, timestamp: new Date().toISOString() };
// console.log("🪟 Active Window with Time:", dataWithTime);
return dataWithTime;
};
// Periodically send active window updates
export const startWindowPolling = (win: BrowserWindow) => {
console.log("⏱️ Started active window polling (every 5s)");
setInterval(async () => {
try {
const app = await getActiveApp();
if (app) win.webContents.send("window:updated", app);
} catch (err) {
console.error("❌ Polling error:", err);
}
}, 5000);
};

View File

@ -0,0 +1,51 @@
// src/hooks/useActiveAppTracker.ts
import { useEffect, useRef } from "react";
import { useDispatch } from "react-redux";
import { setActiveWindow, WindowInfo } from "../store/windowSlice";
export const useActiveAppTracker = (
isTracking: boolean,
projectId: string,
projectName: string
) => {
const dispatch = useDispatch();
const lastActiveRef = useRef<WindowInfo | null>(null);
useEffect(() => {
if (!isTracking || !projectId) {
console.log(" Tracking not active or project not selected");
return;
}
console.log(` Tracking started for project: ${projectName}`);
const fetchActiveApp = async () => {
try {
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
if (!details || !details.id) return;
const prev = lastActiveRef.current;
const hasChanged =
!prev ||
prev.title !== details.title ||
prev.owner.processId !== details.owner.processId;
if (hasChanged) {
console.log("🪟 Window changed:", details.title);
dispatch(setActiveWindow({ window: details, projectId, projectName }));
lastActiveRef.current = details;
}
} catch (error) {
console.error("❌ Failed to fetch active app:", error);
}
};
fetchActiveApp();
const intervalId = setInterval(fetchActiveApp, 5000);
return () => {
console.log("🛑 Stopping active window tracker");
clearInterval(intervalId);
};
}, [isTracking, dispatch, projectId, projectName]);
};

View File

@ -22,7 +22,11 @@ import {
ShieldBan,
ChevronDown,
Play,
ChevronUp,
} from "lucide-react";
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
import { useSelector } from "react-redux";
import { RootState } from "../store";
interface User {
email: string;
@ -43,7 +47,7 @@ interface ProjectTrackingData {
isPaused: boolean;
pauseStartTime: Date | null;
totalPausedTime: number;
accumulatedTime: number; // ✅ NEW: Stores time from previous sessions
accumulatedTime: number; // NEW: Stores time from previous sessions
}
interface TrackingState {
@ -67,11 +71,17 @@ interface DashboardLayoutProps {
}
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const [isSidebarOpen, setSidebarOpen] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const navigate = useNavigate();
const location = useLocation();
const { windowLogs } = useSelector((state: RootState) => state.window);
console.log("🧾 windowLogs in component:", windowLogs);
// Tracking states
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isTracking, setIsTracking] = useState(false);
@ -81,6 +91,8 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const [endTime, setEndTime] = useState<Date | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
useActiveAppTracker(isTracking, selectedProject?.id, selectedProject?.projectName);
const [projectsData, setProjectsData] = useState<Project[]>([
{ id: 1, projectName: "CRM Redesign", clientName: "Salesforce" },
{ id: 2, projectName: "AI Assistant", clientName: "OpenAI" },
@ -105,21 +117,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
};
useEffect(() => {
setInterval(async () => {
const details = await window.electron.getActiveAppDetails()
console.log(details)
}, 15000)
}, [])
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
const closeSidebarOnMobile = () => {
if (isMobile) setSidebarOpen(false);
};
const menuItems = [
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
{ label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
@ -129,7 +126,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
];
// ✅ Sync global states with selected project tracking data
// Sync global states with selected project tracking data
useEffect(() => {
if (selectedProject) {
const projectData = projectTrackingMap.get(selectedProject.id);
@ -143,7 +140,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}
}, [selectedProject, projectTrackingMap]);
// ✅ Timer effect - calculates elapsed time based on timestamps + accumulated time
// Timer effect - calculates elapsed time based on timestamps + accumulated time
useEffect(() => {
if (isTracking && !isPaused && selectedProject) {
timerRef.current = setInterval(() => {
@ -156,9 +153,9 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const startTimestamp = current.startTime.getTime();
const currentSessionTime = Math.floor((now - startTimestamp) / 1000);
const totalPausedTime = current.totalPausedTime || 0;
// ✅ FIX: Add accumulated time from previous sessions
const calculatedElapsed =
// FIX: Add accumulated time from previous sessions
const calculatedElapsed =
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
current.elapsedTime = calculatedElapsed;
@ -185,7 +182,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
};
}, [isTracking, isPaused, selectedProject]);
// ✅ Start tracking
// Start tracking
const handleStartTracking = (project: Project) => {
if (timerRef.current) {
clearInterval(timerRef.current);
@ -193,7 +190,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}
const now = new Date();
window.electron.toggleCloseBtn(false)
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(project.id) || {
@ -207,7 +204,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
accumulatedTime: 0,
};
// ✅ Keep accumulated time from previous sessions
// Keep accumulated time from previous sessions
current.startTime = now;
current.isTracking = true;
current.isPaused = false;
@ -225,7 +222,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
});
};
// ✅ Pause/Resume tracking with pause duration tracking
// Pause/Resume tracking with pause duration tracking
const handlePauseResume = () => {
if (!selectedProject) return;
@ -263,7 +260,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
});
};
// ✅ Stop tracking - PRESERVES elapsed time
// Stop tracking - PRESERVES elapsed time
const handleStopTracking = () => {
if (!selectedProject) return;
@ -273,13 +270,13 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}
const now = new Date();
window.electron.toggleCloseBtn(true)
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
if (!current) return updated;
// ✅ FIX: Store current elapsed time as accumulated time
// FIX: Store current elapsed time as accumulated time
current.accumulatedTime = current.elapsedTime;
current.isTracking = false;
current.isPaused = false;
@ -291,7 +288,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
setIsTracking(false);
setIsPaused(false);
setEndTime(now);
setElapsedTime(current.elapsedTime); // ✅ Keep the elapsed time
setElapsedTime(current.elapsedTime); // Keep the elapsed time
return updated;
});
@ -308,7 +305,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
};
}, []);
// ✅ Handle project selection
// Handle project selection
const handleProjectSelect = (id: number) => {
if (isTracking) return;
@ -355,6 +352,8 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
return `${hrs}h ${mins}m ${secs}s`;
};
const toggleWindowSize = useRef(true); // true -> Open, false -> Close
return (
<>
{/* Header */}
@ -413,22 +412,20 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
</span>
{/* Dropdown */}
<div className="dropdown dropdown-end">
<div
tabIndex={0}
role="button"
className="btn btn-sm btn-outline btn-primary p-1.5 rounded-lg"
>
<ChevronDown />
</div>
<ul
tabIndex={0}
className="dropdown-content menu p-2 shadow-lg bg-base-100 rounded-box w-40 mt-2"
>
<li><a>View Logs</a></li>
<li><a>Settings</a></li>
</ul>
</div>
<button
onClick={() => {
if (toggleWindowSize.current) {
window.electron.resizeWindow(500, 85)
toggleWindowSize.current = false;
} else {
window.electron.resizeWindow(500, 600)
toggleWindowSize.current = true;
}
}}
className="btn btn-xs btn-outline btn-primary ">
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
</button>
</div>
</header>
@ -445,10 +442,9 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
to={item.path}
end={item.exact}
className={({ isActive }) =>
`flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${
isActive
? "bg-primary text-primary-content"
: "hover:bg-base-300 text-base-content"
`flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${isActive
? "bg-primary text-primary-content"
: "hover:bg-base-300 text-base-content"
}`
}
>

View File

@ -2,9 +2,11 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { Provider } from "react-redux";
import { store } from "./store";
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<App />
</React.StrictMode>,
</Provider>
)

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useRef } from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
import { useOutletContext } from "react-router-dom";
@ -91,6 +91,9 @@ const Dashboard: React.FC = () => {
? projectTrackingMap.get(selectedProject.id)
: null;
return (
<main className="flex-1 overflow-y-auto no-scrollbar">
{/* Header */}
@ -104,35 +107,6 @@ const Dashboard: React.FC = () => {
>
Screenshot Test
</button>
<button
onClick={() => {
window.electron.toggleCloseBtn(true)
}}
className="btn btn-xs btn-outline mt-2">
Enable
</button>
<button
onClick={() => {
window.electron.toggleCloseBtn(false)
}}
className="btn btn-xs btn-outline mt-2">
Disable
</button>
<button
onClick={() => {
window.electron.resizeWindow(500, 100)
}}
className="btn btn-xs btn-outline mt-2">
Resize Toggle
</button>
<button
onClick={async () => {
const res = await window.electron.getRunningApps()
console.log(res, "Line 131")
}}
className="btn btn-xs btn-outline mt-2">
Get Running Apps
</button>
</div>
{/* Widgets */}

View File

@ -1,5 +1,7 @@
import React from "react";
import { CalendarDays, Clock, FileText, CheckCircle, XCircle } from "lucide-react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
interface TimesheetEntry {
id: number;
@ -22,6 +24,10 @@ const Timesheets: React.FC = () => {
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
const { windowLogs } = useSelector((state: RootState) => state.window);
console.log("🧾 Timesheet in component:", windowLogs);
return (
<div className="flex flex-col gap-2 max-h-screen">
{/* Header */}

12
src/ui/store/index.tsx Normal file
View File

@ -0,0 +1,12 @@
// src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import windowReducer from "./windowSlice";
export const store = configureStore({
reducer: {
window: windowReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@ -0,0 +1,128 @@
// src/store/windowSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface OwnerInfo {
processId: number;
path: string;
name: string;
}
export interface WindowInfo {
platform: string;
id: number;
title: string;
owner: OwnerInfo;
bounds: Record<string, number>;
contentBounds: Record<string, number>;
memoryUsage: number;
timestamp: string;
}
export interface ProjectWindowLog {
projectId: string;
projectName: string;
appName: string;
windowTitle: string;
platform: string;
duration: number; // in seconds
startTime: string;
endTime: string;
memoryUsage: number;
}
interface WindowState {
activeWindow: WindowInfo | null;
activeSince: number | null;
activeProjectId: string | null;
activeProjectName: string | null;
windowLogs: ProjectWindowLog[];
}
const initialState: WindowState = {
activeWindow: null,
activeSince: null,
activeProjectId: null,
activeProjectName: null,
windowLogs: [],
};
interface SetActiveWindowPayload {
window: WindowInfo;
projectId: string;
projectName: string;
}
const windowSlice = createSlice({
name: "window",
initialState,
reducers: {
// ✅ Start tracking window for a project
setActiveWindow: (state, action: PayloadAction<SetActiveWindowPayload>) => {
const { window, projectId, projectName } = action.payload;
const now = Date.now();
// 🕒 Log previous window before switching
if (state.activeWindow && state.activeSince && state.activeProjectId) {
const duration = Math.floor((now - state.activeSince) / 1000);
const logEntry: ProjectWindowLog = {
projectId: state.activeProjectId,
projectName: state.activeProjectName || "Untitled",
appName: state.activeWindow.owner.name,
windowTitle: state.activeWindow.title,
platform: state.activeWindow.platform,
duration,
startTime: new Date(state.activeSince).toISOString(),
endTime: new Date(now).toISOString(),
memoryUsage: state.activeWindow.memoryUsage,
};
state.windowLogs.push(logEntry);
console.log("🧾 Log saved:", logEntry);
}
// 🔁 Update new active window + project context
state.activeWindow = window;
state.activeSince = now;
state.activeProjectId = projectId;
state.activeProjectName = projectName;
},
// ✅ Stop tracking (finalize)
finalizeActiveWindow: (state) => {
if (state.activeWindow && state.activeSince && state.activeProjectId) {
const now = Date.now();
const duration = Math.floor((now - state.activeSince) / 1000);
const logEntry: ProjectWindowLog = {
projectId: state.activeProjectId,
projectName: state.activeProjectName || "Untitled",
appName: state.activeWindow.owner.name,
windowTitle: state.activeWindow.title,
platform: state.activeWindow.platform,
duration,
startTime: new Date(state.activeSince).toISOString(),
endTime: new Date(now).toISOString(),
memoryUsage: state.activeWindow.memoryUsage,
};
state.windowLogs.push(logEntry);
console.log("✅ Final log saved:", logEntry);
}
state.activeWindow = null;
state.activeSince = null;
state.activeProjectId = null;
state.activeProjectName = null;
},
clearWindowLogs: (state) => {
state.windowLogs = [];
},
},
});
export const { setActiveWindow, finalizeActiveWindow, clearWindowLogs } =
windowSlice.actions;
export default windowSlice.reducer;

2
types.d.ts vendored
View File

@ -32,7 +32,7 @@ interface Window {
getActiveAppDetails: () => {};
toggleCloseBtn: (shouldEnable: boolean) => {},
resizeWindow: (width: number, height: number) => {},
getRunningApps: () => Promise<{name: string, pid:string}[]>,
onWindowUpdate: (callback: (data: any) => void) => void;
};
}