add vnstat-dashboard-kshitiz-b
5
vnstat-dashboard-kshitiz-b/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
dist
|
||||
build
|
||||
*.log
|
42
vnstat-dashboard-kshitiz-b/Dockerfile
Normal 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"]
|
32
vnstat-dashboard-kshitiz-b/README.md
Normal 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)
|
781
vnstat-dashboard-kshitiz-b/backend/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
vnstat-dashboard-kshitiz-b/backend/package.json
Normal 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"
|
||||
}
|
||||
}
|
36
vnstat-dashboard-kshitiz-b/backend/server.js
Normal 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}`));
|
10
vnstat-dashboard-kshitiz-b/docker-compose.yml
Normal 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
|
23
vnstat-dashboard-kshitiz-b/frontend/.gitignore
vendored
Normal 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*
|
70
vnstat-dashboard-kshitiz-b/frontend/README.md
Normal 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)
|
16567
vnstat-dashboard-kshitiz-b/frontend/package-lock.json
generated
Normal file
43
vnstat-dashboard-kshitiz-b/frontend/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
BIN
vnstat-dashboard-kshitiz-b/frontend/public/Logo.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
vnstat-dashboard-kshitiz-b/frontend/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
vnstat-dashboard-kshitiz-b/frontend/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 541 B |
BIN
vnstat-dashboard-kshitiz-b/frontend/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
vnstat-dashboard-kshitiz-b/frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
43
vnstat-dashboard-kshitiz-b/frontend/public/index.html
Normal 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>
|
BIN
vnstat-dashboard-kshitiz-b/frontend/public/logo192.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
vnstat-dashboard-kshitiz-b/frontend/public/logo512.png
Normal file
After Width: | Height: | Size: 255 KiB |
25
vnstat-dashboard-kshitiz-b/frontend/public/manifest.json
Normal 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"
|
||||
}
|
3
vnstat-dashboard-kshitiz-b/frontend/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
713
vnstat-dashboard-kshitiz-b/frontend/src/App.css
Normal 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;
|
||||
}
|
||||
}
|
442
vnstat-dashboard-kshitiz-b/frontend/src/App.js
Normal 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;
|
8
vnstat-dashboard-kshitiz-b/frontend/src/App.test.js
Normal 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();
|
||||
});
|
@@ -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 };
|
13
vnstat-dashboard-kshitiz-b/frontend/src/index.css
Normal 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;
|
||||
}
|
17
vnstat-dashboard-kshitiz-b/frontend/src/index.js
Normal 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();
|
1
vnstat-dashboard-kshitiz-b/frontend/src/logo.svg
Normal 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 |
13
vnstat-dashboard-kshitiz-b/frontend/src/reportWebVitals.js
Normal 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;
|
5
vnstat-dashboard-kshitiz-b/frontend/src/setupTests.js
Normal 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';
|
BIN
vnstat-dashboard-kshitiz-b/preview/daily.png
Normal file
After Width: | Height: | Size: 204 KiB |
BIN
vnstat-dashboard-kshitiz-b/preview/home.png
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
vnstat-dashboard-kshitiz-b/preview/hourly.png
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
vnstat-dashboard-kshitiz-b/preview/logo.png
Normal file
After Width: | Height: | Size: 237 KiB |
BIN
vnstat-dashboard-kshitiz-b/preview/monthly.png
Normal file
After Width: | Height: | Size: 173 KiB |
BIN
vnstat-dashboard-kshitiz-b/preview/yearly.png
Normal file
After Width: | Height: | Size: 130 KiB |