add vnstat-dashboard-kshitiz-b

This commit is contained in:
Stille
2025-09-18 19:34:41 +08:00
parent 2fae82109e
commit 294cf31379
36 changed files with 19095 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
node_modules
frontend/node_modules
dist
build
*.log

View File

@@ -0,0 +1,42 @@
# =====================
# STAGE 1: Build React frontend
# =====================
FROM node:18 AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build
# =====================
# STAGE 2: Set up Node backend with built frontend
# =====================
FROM node:18
WORKDIR /app
# Copy backend files
COPY backend/ ./backend
# Copy backend's package.json
COPY backend/package*.json ./backend/
WORKDIR /app/backend
RUN npm install
# Copy frontend build
COPY --from=frontend-build /app/frontend/build /app/frontend-build
# Install vnstat
RUN apt-get update && \
apt-get install -y vnstat && \
apt-get clean && rm -rf /var/lib/apt/lists/*
ENV FRONTEND_DIR=frontend-build
ENV PORT=8050
EXPOSE 8050
CMD ["node", "server.js"]

View File

@@ -0,0 +1,32 @@
# vnstat-dashboard-kshitiz-b
GitHub [stilleshan/dockerfiles](https://github.com/stilleshan/dockerfiles)
Docker [stilleshan/vnstat-dashboard-kshitiz-b](https://hub.docker.com/r/stilleshan/vnstat-dashboard-kshitiz-b)
> *docker image support for X86 and ARM*
## 简介
基于 [Kshitiz-b/vnstat-dashboard](https://github.com/Kshitiz-b/vnstat-dashboard) 网络流量监控软件前端面板项目的 docker 镜像备份.
## 部署
### docker
需要服务器已安装`vnStat`软件,详情访问 [vnStat](https://humdi.net/vnstat/) 或 [vergoh/vnstat](https://github.com/vergoh/vnstat) .
```shell
docker run -d \
--name=vnstat-dashboard \
--privileged \
--restart=always \
-p 12345:8050 \
-v /var/lib/vnstat:/var/lib/vnstat \
-e TZ=Asia/Shanghai \
stilleshan/vnstat-dashboard-kshitiz-b
```
### docker compose
下载 [docker-compose.yml](https://raw.githubusercontent.com/stilleshan/dockerfiles/main/vnstat-dashboard-kshitiz-b/docker-compose.yml) 执行以下命令启动:
```shell
docker-compose up -d
```
## 参考
参考以下原项目备份镜像:
- GitHub [Kshitiz-b/vnstat-dashboard](https://github.com/Kshitiz-b/vnstat-dashboard)

View File

@@ -0,0 +1,781 @@
{
"name": "backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "backend",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.0"
}
}

View File

@@ -0,0 +1,36 @@
const express = require('express');
const { exec } = require('child_process');
const path = require('path');
const app = express();
const ALLOWED_INTERFACES = ['eth0', 'wlan0', 'docker0', 'tailscale0'];
const FRONTEND_DIR = process.env.FRONTEND_DIR || 'frontend-build';
app.use(express.static(path.join(__dirname, '..', FRONTEND_DIR)));
app.get('/api/vnstat/:iface', (req, res) => {
const iface = req.params.iface;
if (!ALLOWED_INTERFACES.includes(iface)) {
return res.status(400).json({ error: 'Invalid interface' });
}
exec(`vnstat -i ${iface} --json`, (error, stdout, stderr) => {
if (error) {
return res.status(500).json({ error: stderr || error.message });
}
try {
const data = JSON.parse(stdout);
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Failed to parse vnstat output.' });
}
});
});
// Serve React frontend for all non-API routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '..', FRONTEND_DIR, 'index.html'));
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

View File

@@ -0,0 +1,10 @@
services:
vnstat-dashboard:
# container_name: vnstat-dashboard
image: stilleshan/vnstat-dashboard-kshitiz-b
privileged: true
ports:
- 12345:8050
volumes:
- /var/lib/vnstat:/var/lib/vnstat
restart: always

View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
{
"proxy": "http://localhost:3001",
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-scripts": "^5.0.1",
"recharts": "^3.0.2",
"web-vitals": "^5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Monitor your network interface statistics in real-time from VNStat analytics"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>VNStat Dashboard</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,713 @@
/* App.css - Enhanced VNStat Dashboard Styles */
/* Base Styles */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #030712 !important;
color: #ffffff;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.no-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
/* Container Styles */
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 1rem;
}
.github {
display: flex;
justify-content: end;
align-items: center;
gap: 2px;
}
.github-icon {
color: white;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1f2937;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Loading Animation */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
/* Gradient Text */
.bg-gradient-to-r {
background: linear-gradient(to right, #60a5fa, #a855f7);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Card Hover Effects */
.stats-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.stats-card:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
/* Button Styles */
.tab-bar {
background: #232c3b;
border-radius: 10px;
border: 1px solid #293042;
padding: 6px 10px;
display: flex;
gap: 6px;
/* tighter spacing between tabs */
justify-content: flex-start;
margin: 0 auto;
width: fit-content;
/* only as wide as content */
}
.tab-button {
background: #232c3b;
color: #e5e7eb;
border: none;
padding: 6px 16px;
/* less horizontal padding */
font-size: 0.97rem;
border-radius: 8px;
font-weight: 500;
transition: all 0.16s;
outline: none;
display: flex;
align-items: center;
gap: 8px;
box-shadow: none;
position: relative;
}
.tab-button.active {
background: #2563eb;
color: #fff;
font-weight: 600;
border: none;
box-shadow: 0 4px 24px 0 rgba(59, 130, 246, 0.10);
z-index: 2;
}
.tab-button:not(.active):hover {
background: #263045;
color: #fff;
/* no border! */
}
.tab-button:not(.active) {
background: #232c3b;
color: #e5e7eb;
border: none;
box-shadow: none;
}
/* Select Dropdown */
.interface-select {
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23ffffff' d='M2 0L0 2h4zm0 5L0 3h4z'/></svg>");
background-repeat: no-repeat;
background-position: right 0.7rem center;
background-size: 0.65rem auto;
padding-right: 2.5rem;
}
.interface-select:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* Table Styles */
.network-table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
background: #1f2937;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.network-table th {
background: #374151;
color: #d1d5db;
font-weight: 600;
font-size: 0.875rem;
padding: 1rem;
text-align: left;
border-bottom: 1px solid #4b5563;
}
.network-table td {
padding: 1rem;
border-bottom: 1px solid #374151;
color: #e5e7eb;
font-size: 0.875rem;
}
.network-table tr:hover {
background-color: #374151;
}
.network-table tr:last-child td {
border-bottom: none;
}
/* Chart Container */
.chart-container {
background: #1f2937;
border-radius: 12px;
padding: 1.5rem;
border: 1px solid #374151;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
/* Responsive Design */
@media (max-width: 768px) {
.label-text {
display: none;
}
.container {
padding: 0 0.5rem;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.tab-bar {
display: flex;
flex-direction: row;
overflow-x: auto;
white-space: nowrap;
flex-wrap: nowrap;
justify-content: flex-start;
padding-bottom: 6px;
gap: 8px;
min-width: max-content;
}
.tab-button {
flex-shrink: 0;
}
.network-table th,
.network-table td {
padding: 0.5rem;
font-size: 0.75rem;
}
.chart-container {
padding: 1rem;
}
}
@media (max-width: 640px) {
.stats-grid {
grid-template-columns: 1fr;
}
.header-title {
font-size: 2rem;
}
.section-title {
font-size: 1.25rem;
}
.network-table {
font-size: 0.75rem;
}
.network-table th,
.network-table td {
padding: 0.75rem 0.5rem;
}
}
/* Utility Classes */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
.font-medium {
font-weight: 500;
}
.text-sm {
font-size: 0.875rem;
}
.text-base {
font-size: 1rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-xl {
font-size: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
}
.text-3xl {
font-size: 1.875rem;
}
.text-4xl {
font-size: 2.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-4 {
margin-top: 1rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.mt-8 {
margin-top: 2rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.p-3 {
padding: 0.75rem;
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.p-8 {
padding: 2rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
.shadow-sm {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.shadow-xl {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.border {
border-width: 1px;
}
.border-t {
border-top-width: 1px;
}
.border-b {
border-bottom-width: 1px;
}
.flex {
display: flex;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
.hidden {
display: none;
}
.block {
display: block;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.gap-3 {
gap: 0.75rem;
}
.gap-4 {
gap: 1rem;
}
.gap-6 {
gap: 1.5rem;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.min-h-screen {
min-height: 100vh;
}
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
.transition-all {
transition: all 0.15s ease;
}
.transition-colors {
transition: color 0.15s ease, background-color 0.15s ease;
}
/* Color Classes */
.text-white {
color: #ffffff;
}
.text-gray-300 {
color: #d1d5db;
}
.text-gray-400 {
color: #9ca3af;
}
.text-gray-500 {
color: #6b7280;
}
.text-blue-400 {
color: #60a5fa;
}
.text-blue-500 {
color: #3b82f6;
}
.text-green-400 {
color: #34d399;
}
.text-purple-400 {
color: #a78bfa;
}
.text-orange-400 {
color: #fb923c;
}
.bg-gray-800 {
background-color: #1f2937;
}
.bg-gray-900 {
background-color: #111827;
}
.bg-gray-950 {
background-color: #030712;
}
.border-gray-700 {
border-color: #374151;
}
.border-gray-800 {
border-color: #1f2937;
}
.focus\:outline-none:focus {
outline: none;
}
.focus\:ring-2:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
}
.focus\:ring-blue-500:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
}
.focus\:border-blue-500:focus {
border-color: #3b82f6;
}
.hover\:bg-gray-700:hover {
background-color: #374151;
}
.hover\:text-white:hover {
color: #ffffff;
}
/* Custom Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(-10px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: slideIn 0.3s ease-out;
}
/* Chart Customizations */
.recharts-wrapper {
font-family: inherit;
}
.recharts-tooltip-wrapper {
z-index: 1000;
}
.recharts-cartesian-axis-tick-value {
fill: #9ca3af;
font-size: 12px;
}
.recharts-cartesian-grid-horizontal line,
.recharts-cartesian-grid-vertical line {
stroke: #374151;
}
/* Error States */
.error-message {
color: #ef4444;
background-color: #7f1d1d;
border: 1px solid #991b1b;
padding: 1rem;
border-radius: 0.5rem;
margin: 1rem 0;
}
.success-message {
color: #10b981;
background-color: #064e3b;
border: 1px solid #065f46;
padding: 1rem;
border-radius: 0.5rem;
margin: 1rem 0;
}
/* Focus States for Accessibility */
.tab-button:focus,
.interface-select:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
}
/* Print Styles */
@media print {
body {
background: white !important;
color: black !important;
}
.bg-gray-800,
.bg-gray-900,
.bg-gray-950 {
background: white !important;
color: black !important;
}
.text-white,
.text-gray-300,
.text-gray-400 {
color: black !important;
}
.border-gray-700,
.border-gray-800 {
border-color: #ccc !important;
}
}

View File

@@ -0,0 +1,442 @@
import React, { useState, useEffect } from 'react';
import './App.css';
import {
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid
} from 'recharts';
import { Network, Activity, Calendar, Clock, TrendingUp, Download, Upload, Server, Github } from 'lucide-react';
import { HourlyTable, DailyTable, MonthlyTable, YearlyTable } from './components/TrafficTable';
function formatDate({ year, month, day }) {
const date = new Date(year, month - 1, day);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[date.getMonth()]} ${date.getDate().toString().padStart(2, '0')}, ${date.getFullYear()}`;
}
function formatTime({ hour, minute }) {
const period = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')} ${period}`;
}
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
const k = 1024, dm = 2;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function formatMonthYear(year, month) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[month - 1]} ${year}`;
}
const TABS = [
{ id: 'Summary', label: 'Summary', icon: Activity },
{ id: 'Hourly', label: 'Hourly', icon: Clock },
{ id: 'Daily', label: 'Daily', icon: Calendar },
{ id: 'Monthly', label: 'Monthly', icon: Calendar },
{ id: 'Yearly', label: 'Yearly', icon: TrendingUp }
];
const INTERFACES = [
{ id: 'eth0', label: 'Ethernet (eth0)', icon: Network },
{ id: 'wlan0', label: 'Wifi (wlan0)', icon: Network },
{ id: 'docker0', label: 'Docker (docker0)', icon: Server },
{ id: 'tailscale0', label: 'Tailscale (tailscale0)', icon: Network }
];
function App() {
const [selected, setSelected] = useState('eth0');
const [tab, setTab] = useState('Summary');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch(`/api/vnstat/${selected}`)
.then(res => res.json())
.then(setData)
.finally(() => setLoading(false));
}, [selected]);
const ifaceInfo = data && data.interfaces ? data.interfaces[0] : null;
const traffic = ifaceInfo ? ifaceInfo.traffic : null;
const hourly = traffic && traffic.hour
? [...traffic.hour]
.filter(row => row.time && typeof row.time.hour === 'number')
.sort((a, b) => {
const dateA = new Date(a.date.year, a.date.month - 1, a.date.day, a.time.hour);
const dateB = new Date(b.date.year, b.date.month - 1, b.date.day, b.time.hour);
return dateB - dateA;
})
.slice(0, 24)
: [];
const daily = traffic && traffic.day
? [...traffic.day].sort((a, b) => {
const dateA = new Date(a.date.year, a.date.month - 1, a.date.day);
const dateB = new Date(b.date.year, b.date.month - 1, b.date.day);
return dateB - dateA;
})
: [];
const monthly = traffic && traffic.month
? [...traffic.month].sort((a, b) => {
const dateA = new Date(a.date.year, a.date.month - 1);
const dateB = new Date(b.date.year, b.date.month - 1);
return dateB - dateA;
})
: [];
const yearly = traffic && traffic.year
? [...traffic.year].sort((a, b) => b.date.year - a.date.year)
: [];
const fivemin = traffic && traffic.fiveminute
? traffic.fiveminute.slice(-10).reverse()
: [];
const getLabel = (row, type) => {
if (type === 'hourly') {
const date = new Date(row.date.year, row.date.month - 1, row.date.day, row.time.hour);
return date.toLocaleString('en-US', {
month: 'short',
day: '2-digit',
hour: '2-digit',
hour12: true
});
}
if (type === 'daily') return formatDate(row.date);
if (type === 'monthly') return formatMonthYear(row.date.year, row.date.month);
if (type === 'yearly') return `${row.date.year}`;
return '';
};
const graphData = (rows, type) => rows.map(row => ({
name: getLabel(row, type),
RX: row.rx ? row.rx : 0,
TX: row.tx ? row.tx : 0,
Total: row.rx && row.tx ? row.rx + row.tx : 0,
}));
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-800 border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-gray-300 text-sm mb-2">{label}</p>
{payload.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-sm">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-gray-300">{entry.dataKey}:</span>
<span className="text-white font-medium">{formatBytes(entry.value)}</span>
</div>
))}
</div>
);
}
return null;
};
return (
<div className="min-h-screen bg-gray-950 text-white mb-8">
<div className="container mx-auto px-4 py-8 max-w-xl w-full">
{/* Header */}
<div className="mb-8 text-center">
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
Network Traffic Dashboard
</h1>
<p className="text-gray-400">Monitor your network interface statistics in real-time</p>
</div>
<div class="github">
<a class="github-icon" href="https://github.com/Kshitiz-b/vnstat-dashboard" target="_blank" rel="noreferrer">
<Github class="h-5 w-5" />
<span>Kshitiz-b</span>
</a>
</div>
{/* Interface Selector */}
<div className="mb-8">
<label className="block text-sm font-medium text-gray-300 mb-3">
Select Network Interface
</label>
<div className="flex flex-row items-center gap-4 justify-center">
<Network className="h-5 w-5 text-gray-400" />
<select
value={selected}
onChange={e => setSelected(e.target.value)}
className="w-full max-w-xs bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 pr-10 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all appearance-none"
>
{INTERFACES.map(iface => (
<option key={iface.id} value={iface.id}>
{iface.label}
</option>
))}
</select>
</div>
</div>
{/* Tab Navigation */}
<div className="mb-8">
<div className="overflow-x-auto no-scrollbar">
<div className="tab-bar">
{TABS.map(t => {
const Icon = t.icon;
return (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`tab-button${t.id === tab ? ' active' : ''}`}
>
<Icon className="h-4 w-4" />
{t.label}
</button>
);
})}
</div>
</div>
</div>
{/* Main Content */}
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-96">
<div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<span className="text-gray-400">Loading network data...</span>
</div>
</div>
) : !ifaceInfo ? (
<div className="flex items-center justify-center h-96">
<div className="text-center">
<Server className="h-12 w-12 text-gray-500 mx-auto mb-4" />
<p className="text-gray-400">No data available for this interface</p>
</div>
</div>
) : tab === "Summary" ? (
<div className="p-8">
<div className="mb-8">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
<Activity className="h-6 w-6 text-blue-400" />
{ifaceInfo.name} Overview
</h2>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div className="flex items-center gap-3 mb-2">
<Download className="h-5 w-5 text-green-400" />
<span className="text-sm text-gray-400">Total Received</span>
</div>
<div className="text-2xl font-bold text-green-400">
{formatBytes(traffic.total.rx)}
</div>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div className="flex items-center gap-3 mb-2">
<Upload className="h-5 w-5 text-blue-400" />
<span className="text-sm text-gray-400">Total Sent</span>
</div>
<div className="text-2xl font-bold text-blue-400">
{formatBytes(traffic.total.tx)}
</div>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div className="flex items-center gap-3 mb-2">
<Calendar className="h-5 w-5 text-purple-400" />
<span className="text-sm text-gray-400">Created</span>
</div>
<div className="text-lg font-semibold text-purple-400">
{formatDate(ifaceInfo.created.date)}
</div>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div className="flex items-center gap-3 mb-2">
<Clock className="h-5 w-5 text-orange-400" />
<span className="text-sm text-gray-400">Last Updated</span>
</div>
<div className="text-lg font-semibold text-orange-400">
{formatDate(ifaceInfo.updated.date)}
</div>
<div className="text-sm text-gray-400">
{formatTime(ifaceInfo.updated.time)}
</div>
</div>
</div>
</div>
{/* Recent Traffic Table */}
<div>
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-blue-400" />
Recent Traffic (5-minute intervals)
</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-800">
<th className="text-left p-4 font-medium text-gray-300 border-b border-gray-700">Date</th>
<th className="text-left p-4 font-medium text-gray-300 border-b border-gray-700">Time</th>
<th className="text-left p-4 font-medium text-gray-300 border-b border-gray-700">
<div className="flex items-center gap-2">
<Download className="h-4 w-4 text-green-400" />
<span className="label-text">Received</span>
</div>
</th>
<th className="text-left p-4 font-medium text-gray-300 border-b border-gray-700">
<div className="flex items-center gap-2">
<Upload className="h-4 w-4 text-blue-400" />
<span className="label-text">Sent</span>
</div>
</th>
</tr>
</thead>
<tbody>
{fivemin.map((row, i) => (
<tr key={i} className="hover:bg-gray-800 transition-colors">
<td className="p-4 border-b border-gray-800 text-gray-300">
{formatDate(row.date)}
</td>
<td className="p-4 border-b border-gray-800 text-gray-300">
{formatTime(row.time)}
</td>
<td className="p-4 border-b border-gray-800 font-medium text-green-400">
{formatBytes(row.rx)}
</td>
<td className="p-4 border-b border-gray-800 font-medium text-blue-400">
{formatBytes(row.tx)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
) : (
<div className="p-8">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
<TrendingUp className="h-6 w-6 text-blue-400" />
{tab} Traffic Analysis
</h2>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<ResponsiveContainer width="100%" height={400}>
<LineChart
data={graphData(
tab === "Hourly" ? [...hourly.slice(-24)].reverse() :
tab === "Daily" ? [...daily].reverse() :
tab === "Monthly" ? [...monthly].reverse() :
tab === "Yearly" ? [...yearly].reverse() : [],
tab.toLowerCase()
)}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="name"
stroke="#9CA3AF"
fontSize={12}
tickLine={false}
/>
<YAxis
tickFormatter={formatBytes}
stroke="#9CA3AF"
fontSize={12}
tickLine={false}
width={80}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="RX"
stroke="#10B981"
strokeWidth={3}
dot={{ fill: '#10B981', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#10B981', strokeWidth: 2 }}
/>
<Line
type="monotone"
dataKey="TX"
stroke="#3B82F6"
strokeWidth={3}
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#3B82F6', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Summary Stats */}
{tab === 'Daily' && daily.length > 0 && (
<div className="mt-6 bg-gray-800 rounded-lg p-6 border border-gray-700">
<h4 className="text-lg font-semibold mb-2 text-blue-400">Today's Usage</h4>
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex flex-col bg-gray-900 rounded-md p-4 border border-gray-700 min-w-[120px] items-center">
<span className="text-sm text-gray-400 mb-1">Download:</span>
<span className="text-xl font-bold text-green-400 ml-2">{formatBytes(daily[0].rx)}</span>
</div>
<div className="flex flex-col bg-gray-900 rounded-md p-4 border border-gray-700 min-w-[120px] items-center">
<span className="text-sm text-gray-400 mb-1">Upload:</span>
<span className="text-xl font-bold text-blue-400 ml-2">{formatBytes(daily[0].tx)}</span>
</div>
</div>
</div>
)}
{tab === 'Monthly' && monthly.length > 0 && (
<div className="mt-6 bg-gray-800 rounded-lg p-6 border border-gray-700">
<h4 className="text-lg font-semibold mb-2 text-blue-400">This Month's Usage</h4>
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex flex-col bg-gray-900 rounded-md p-4 border border-gray-700 min-w-[120px] items-center">
<span className="text-sm text-gray-400">Download:</span>
<span className="text-xl font-bold text-green-400 ml-2">{formatBytes(monthly[0].rx)}</span>
</div>
<div className="flex flex-col bg-gray-900 rounded-md p-4 border border-gray-700 min-w-[120px] items-center">
<span className="text-sm text-gray-400">Upload:</span>
<span className="text-xl font-bold text-blue-400 ml-2">{formatBytes(monthly[0].tx)}</span>
</div>
</div>
</div>
)}
{tab === 'Yearly' && yearly.length > 0 && (
<div className="mt-6 bg-gray-800 rounded-lg p-6 border border-gray-700">
<h4 className="text-lg font-semibold mb-2 text-blue-400">This Year's Usage</h4>
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex flex-col bg-gray-900 rounded-md p-4 border border-gray-700 min-w-[120px] items-center">
<span className="text-sm text-gray-400">Download:</span>
<span className="text-xl font-bold text-green-400 ml-2">{formatBytes(yearly[0].rx)}</span>
</div>
<div className="flex flex-col bg-gray-900 rounded-md p-4 border border-gray-700 min-w-[120px] items-center">
<span className="text-sm text-gray-400">Upload:</span>
<span className="text-xl font-bold text-blue-400 ml-2">{formatBytes(yearly[0].tx)}</span>
</div>
</div>
</div>
)}
<div>
{tab === 'Hourly' && <HourlyTable data={hourly} />}
{tab === 'Daily' && <DailyTable data={daily} />}
{tab === 'Monthly' && <MonthlyTable data={monthly} />}
{tab === 'Yearly' && <YearlyTable data={yearly} />}
</div>
</div>
)}
</div>
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,187 @@
import React from 'react';
import { Clock, Calendar, Download, Upload, BarChart, LineChart } from 'lucide-react';
const TrafficTable = ({ title, icon, data, headers }) => (
<div>
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
{icon}
{title}
</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-800">
{headers.map((header, i) => (
<th key={i} className="text-left p-4 font-medium text-gray-300 border-b border-gray-700">
<div className="flex items-center gap-2">
{header.icon}
<span className="label-text">{header.label}</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i} className="hover:bg-gray-800 transition-colors">
{headers.map((header, j) => (
<td
key={j}
className={`p-4 border-b border-gray-800 text-gray-300 ${header.className || ''}`.trim()}
>
{header.render(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const formatBytes = (bytes) => {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
const HourlyTable = ({ data }) => {
const sortedData = [...data]
.filter((row) => row.rx || row.tx)
.sort((a, b) => {
const dateA = new Date(a.date.year, a.date.month - 1, a.date.day, a.hour || 0);
const dateB = new Date(b.date.year, b.date.month - 1, b.date.day, b.hour || 0);
return dateB - dateA; // descending
})
.slice(0, 24); // last 24 hours
return (
<TrafficTable
title="Hourly Traffic"
icon={<Clock className="h-5 w-5 text-yellow-400" />}
data={sortedData}
headers={[
{
label: 'Date',
render: (row) =>
new Date(row.date.year, row.date.month - 1, row.date.day).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: '2-digit',
}),
},
{
label: 'Time',
render: (row) =>
new Date(
row.date.year,
row.date.month - 1,
row.date.day,
row.time?.hour ?? 0,
row.time?.minute ?? 0
).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
}),
},
{
label: 'Received',
icon: <Download className="h-4 w-4 text-green-400" />,
className: 'font-medium text-green-400',
render: (row) => formatBytes(row.rx),
},
{
label: 'Sent',
icon: <Upload className="h-4 w-4 text-blue-400" />,
className: 'font-medium text-blue-400',
render: (row) => formatBytes(row.tx),
},
]}
/>
);
};
const DailyTable = ({ data }) => (
<TrafficTable
title="Daily Traffic"
icon={<Calendar className="h-5 w-5 text-green-400" />}
data={data}
headers={[
{ label: 'Date',
render: (row) =>
new Date(row.date.year, row.date.month - 1, row.date.day).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: '2-digit',
}), },
{
label: 'Received',
icon: <Download className="h-4 w-4 text-green-400" />,
className: 'font-medium text-green-400',
render: (row) => formatBytes(row.rx),
},
{
label: 'Sent',
icon: <Upload className="h-4 w-4 text-blue-400" />,
className: 'font-medium text-blue-400',
render: (row) => formatBytes(row.tx),
},
]}
/>
);
const MonthlyTable = ({ data }) => (
<TrafficTable
title="Monthly Traffic"
icon={<BarChart className="h-5 w-5 text-purple-400" />}
data={data}
headers={[
{ label: 'Month', render: (row) =>
new Date(row.date.year, row.date.month - 1).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
}), },
{
label: 'Received',
icon: <Download className="h-4 w-4 text-green-400" />,
className: 'font-medium text-green-400',
render: (row) => formatBytes(row.rx),
},
{
label: 'Sent',
icon: <Upload className="h-4 w-4 text-blue-400" />,
className: 'font-medium text-blue-400',
render: (row) => formatBytes(row.tx),
},
]}
/>
);
const YearlyTable = ({ data }) => (
<TrafficTable
title="Yearly Traffic"
icon={<LineChart className="h-5 w-5 text-orange-400" />}
data={data}
headers={[
{ label: 'Year', render: (row) => `${row.date.year}` },
{
label: 'Received',
icon: <Download className="h-4 w-4 text-green-400" />,
className: 'font-medium text-green-400',
render: (row) => formatBytes(row.rx),
},
{
label: 'Sent',
icon: <Upload className="h-4 w-4 text-blue-400" />,
className: 'font-medium text-blue-400',
render: (row) => formatBytes(row.tx),
},
]}
/>
);
export { HourlyTable, DailyTable, MonthlyTable, YearlyTable };

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB