feat: stat cards, new logging

This commit is contained in:
Derock 2023-11-11 21:30:46 -05:00
parent a7fb919cda
commit d5caddcb22
No known key found for this signature in database
17 changed files with 727 additions and 418 deletions

95
logs/combined.log Normal file
View file

@ -0,0 +1,95 @@
2023-11-12T01:48:57.462Z info: Not running database migrations, use drizzle-kit push to migrate
2023-11-12T01:48:57.463Z info: 🚀 Hostforge v1.0.0 ready!
2023-11-12T01:48:57.469Z info: Server listening on port 3000
2023-11-12T01:49:11.893Z warn: SIGTERM received, shutting down...
2023-11-12T01:49:17.625Z info: Not running database migrations, use drizzle-kit push to migrate
2023-11-12T01:49:17.627Z info: 🚀 Hostforge v1.0.0 ready!
2023-11-12T01:49:17.635Z info: Server listening on port 3000
2023-11-12T01:50:31.777Z warn: SIGTERM received, shutting down...
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
{"level":"info","message":"Server listening on port 3000"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge"}
{"level":"info","message":"Server listening on localhost:3000"}
{"level":"info","message":"Version: 0.1.0"}
{"level":"info","message":"Environment: development"}
{"level":"info","message":"Build commit: unknown"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge"}
{"level":"info","message":" │ Server listening on localhost:3000"}
{"level":"info","message":" │ Version: 0.1.0"}
{"level":"info","message":" │ Environment: development"}
{"level":"info","message":" ╰ Build commit: unknown"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge"}
{"level":"info","message":"│ Server listening on localhost:3000"}
{"level":"info","message":"│ Version: 0.1.0"}
{"level":"info","message":"│ Environment: development"}
{"level":"info","message":"╰ Build commit: unknown"}
{"level":"warn","message":"SIGTERM received, shutting down..."}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge"}
{"level":"info","message":"│ Server listening on localhost:3000"}
{"level":"info","message":"│ Version: 0.1.0"}
{"level":"info","message":"│ Environment: development"}
{"level":"info","message":"╰ Build commit: unknown"}
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
{"level":"info","message":"🚀 Hostforge"}
{"level":"info","message":"│ Server listening on 0.0.0.0:3000"}
{"level":"info","message":"│ Version: 0.1.0"}
{"level":"info","message":"│ Environment: development"}
{"level":"info","message":"╰ Build commit: unknown"}

0
logs/error.log Normal file
View file

View file

@ -1,8 +1,4 @@
/** @type {import("next").NextConfig} */
const config = {
experimental: {
instrumentationHook: true,
},
};
const config = {};
export default config;

View file

@ -30,8 +30,7 @@
"argon2": "^0.31.2",
"better-sqlite3": "^9.0.0",
"bufferutil": "^4.0.8",
"bunyan": "^1.8.15",
"bunyan-format": "^0.2.1",
"chalk": "^5.3.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
@ -43,7 +42,6 @@
"next-themes": "^0.2.1",
"node-os-utils": "^1.3.7",
"react": "18.2.0",
"react-animated-numbers": "^0.16.0",
"react-dom": "18.2.0",
"react-icons": "^4.11.0",
"recharts": "^2.9.3",
@ -53,13 +51,12 @@
"tailwindcss-animate": "^1.0.7",
"ts-permissions": "^1.0.0",
"ua-parser-js": "^1.0.37",
"winston": "^3.11.0",
"ws": "^8.14.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.7",
"@types/bunyan": "^1.8.11",
"@types/bunyan-format": "^0.2.8",
"@types/eslint": "^8.44.7",
"@types/node": "^20.9.0",
"@types/node-os-utils": "^1.3.4",
@ -80,6 +77,7 @@
"tailwindcss": "^3.3.5",
"tscpaths": "^0.0.9",
"tsup": "^7.2.0",
"typed-emitter": "^2.1.0",
"typescript": "^5.2.2"
},
"ct3aMetadata": {

View file

@ -50,12 +50,9 @@ dependencies:
bufferutil:
specifier: ^4.0.8
version: 4.0.8
bunyan:
specifier: ^1.8.15
version: 1.8.15
bunyan-format:
specifier: ^0.2.1
version: 0.2.1
chalk:
specifier: ^5.3.0
version: 5.3.0
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@ -89,9 +86,6 @@ dependencies:
react:
specifier: 18.2.0
version: 18.2.0
react-animated-numbers:
specifier: ^0.16.0
version: 0.16.0(react-dom@18.2.0)(react@18.2.0)
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
@ -119,6 +113,9 @@ dependencies:
ua-parser-js:
specifier: ^1.0.37
version: 1.0.37
winston:
specifier: ^3.11.0
version: 3.11.0
ws:
specifier: ^8.14.2
version: 8.14.2(bufferutil@4.0.8)
@ -130,12 +127,6 @@ devDependencies:
'@types/better-sqlite3':
specifier: ^7.6.7
version: 7.6.7
'@types/bunyan':
specifier: ^1.8.11
version: 1.8.11
'@types/bunyan-format':
specifier: ^0.2.8
version: 0.2.8
'@types/eslint':
specifier: ^8.44.7
version: 8.44.7
@ -196,6 +187,9 @@ devDependencies:
tsup:
specifier: ^7.2.0
version: 7.2.0(postcss@8.4.31)(typescript@5.2.2)
typed-emitter:
specifier: ^2.1.0
version: 2.1.0
typescript:
specifier: ^5.2.2
version: 5.2.2
@ -413,6 +407,19 @@ packages:
dependencies:
regenerator-runtime: 0.14.0
/@colors/colors@1.6.0:
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
dev: false
/@dabh/diagnostics@2.0.3:
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
dependencies:
colorspace: 1.1.4
enabled: 2.0.0
kuler: 2.0.0
dev: false
/@drizzle-team/studio@0.0.5:
resolution: {integrity: sha512-ps5qF0tMxWRVu+V5gvCRrQNqlY92aTnIKdq27gm9LZMSdaKYZt6AVvSK1dlUMzs6Rt0Jm80b+eWct6xShBKhIw==}
dev: true
@ -1144,54 +1151,6 @@ packages:
react: 18.2.0
dev: false
/@react-spring/animated@9.7.3(react@18.2.0):
resolution: {integrity: sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@react-spring/shared': 9.7.3(react@18.2.0)
'@react-spring/types': 9.7.3
react: 18.2.0
dev: false
/@react-spring/core@9.7.3(react@18.2.0):
resolution: {integrity: sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@react-spring/animated': 9.7.3(react@18.2.0)
'@react-spring/shared': 9.7.3(react@18.2.0)
'@react-spring/types': 9.7.3
react: 18.2.0
dev: false
/@react-spring/shared@9.7.3(react@18.2.0):
resolution: {integrity: sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@react-spring/types': 9.7.3
react: 18.2.0
dev: false
/@react-spring/types@9.7.3:
resolution: {integrity: sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==}
dev: false
/@react-spring/web@9.7.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@react-spring/animated': 9.7.3(react@18.2.0)
'@react-spring/core': 9.7.3(react@18.2.0)
'@react-spring/shared': 9.7.3(react@18.2.0)
'@react-spring/types': 9.7.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@rushstack/eslint-patch@1.5.1:
resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==}
dev: true
@ -1331,18 +1290,6 @@ packages:
dependencies:
'@types/node': 20.9.0
/@types/bunyan-format@0.2.8:
resolution: {integrity: sha512-Wd5TIv2RroJ0VmfvkhnnlEZgm4E1D8b9akSuVtlSQVIVqH9jFpoE6Nkj4Q7f7TTKp6wtlvex3FALshTCjmfi+A==}
dependencies:
'@types/node': 20.9.0
dev: true
/@types/bunyan@1.8.11:
resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==}
dependencies:
'@types/node': 20.9.0
dev: true
/@types/cross-spawn@6.0.3:
resolution: {integrity: sha512-BDAkU7WHHRHnvBf5z89lcvACsvkz/n7Tv+HyD/uW76O29HoH1Tk/W6iQrepaZVbisvlEek4ygwT8IW7ow9XLAA==}
dependencies:
@ -1478,6 +1425,10 @@ packages:
resolution: {integrity: sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==}
dev: true
/@types/triple-beam@1.3.5:
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
dev: false
/@types/ua-parser-js@0.7.39:
resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==}
dev: true
@ -1717,14 +1668,6 @@ packages:
dependencies:
color-convert: 2.0.1
/ansicolors@0.2.1:
resolution: {integrity: sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w==}
dev: false
/ansistyles@0.1.3:
resolution: {integrity: sha512-6QWEyvMgIXX0eO972y7YPBLSBsq7UWKFAoNNTLGaOJ9bstcEL9sCbcjf96dVfNDdUsRoGOK82vWFJlKApXds7g==}
dev: false
/any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@ -2127,25 +2070,6 @@ packages:
load-tsconfig: 0.2.5
dev: true
/bunyan-format@0.2.1:
resolution: {integrity: sha512-xQs2LwWskjQdv7bVkMNwvMi7HnvDQoX4587H90nDGQGPPwHrmxsihBOIYHMVwjLMMOokITKPyFcbFneblvMEjQ==}
dependencies:
ansicolors: 0.2.1
ansistyles: 0.1.3
xtend: 2.1.2
dev: false
/bunyan@1.8.15:
resolution: {integrity: sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==}
engines: {'0': node >=0.10.0}
hasBin: true
optionalDependencies:
dtrace-provider: 0.8.8
moment: 2.29.4
mv: 2.1.1
safe-json-stringify: 1.2.0
dev: false
/busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@ -2219,7 +2143,6 @@ packages:
/chalk@5.3.0:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: true
/checkpoint-client@1.1.27:
resolution: {integrity: sha512-xstymfUalJOv6ZvTtmkwP4ORJN36ikT4PvrIoLe3wstbYf87XIXCcZrSmbFQOjyB0v1qbBnCsAscDpfdZlCkFA==}
@ -2357,11 +2280,32 @@ packages:
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
dev: false
/color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
dev: false
/color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
dependencies:
color-convert: 1.9.3
color-string: 1.9.1
dev: false
/colorspace@1.1.4:
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
dependencies:
color: 3.2.1
text-hex: 1.0.0
dev: false
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@ -2857,15 +2801,6 @@ packages:
pg: 8.11.3
dev: false
/dtrace-provider@0.8.8:
resolution: {integrity: sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==}
engines: {node: '>=0.10'}
requiresBuild: true
dependencies:
nan: 2.18.0
dev: false
optional: true
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
@ -2884,6 +2819,10 @@ packages:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
dev: true
/enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
dev: false
/end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
@ -3520,6 +3459,10 @@ packages:
dependencies:
reusify: 1.0.4
/fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
dev: false
/file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@ -3591,6 +3534,10 @@ packages:
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
dev: true
/fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
dev: false
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
@ -3757,18 +3704,6 @@ packages:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
dev: false
/glob@6.0.4:
resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==}
requiresBuild: true
dependencies:
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
dev: false
optional: true
/glob@7.1.6:
resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
dependencies:
@ -4114,6 +4049,10 @@ packages:
/is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
/is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: false
/is-async-function@2.0.0:
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
engines: {node: '>= 0.4'}
@ -4562,6 +4501,10 @@ packages:
engines: {node: '>= 8'}
dev: false
/kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
dev: false
/language-subtag-registry@0.3.22:
resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==}
dev: true
@ -4697,6 +4640,18 @@ packages:
wrap-ansi: 6.2.0
dev: false
/logform@2.6.0:
resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==}
engines: {node: '>= 12.0.0'}
dependencies:
'@colors/colors': 1.6.0
'@types/triple-beam': 1.3.5
fecha: 4.2.3
ms: 2.1.3
safe-stable-stringify: 2.4.3
triple-beam: 1.4.1
dev: false
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@ -4895,27 +4850,12 @@ packages:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
dev: false
/mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
requiresBuild: true
dependencies:
minimist: 1.2.8
dev: false
optional: true
/mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
dev: false
/moment@2.29.4:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
requiresBuild: true
dev: false
optional: true
/mongodb-connection-string-url@2.6.0:
resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==}
dependencies:
@ -5010,17 +4950,6 @@ packages:
- supports-color
dev: false
/mv@2.1.1:
resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==}
engines: {node: '>=0.8.0'}
requiresBuild: true
dependencies:
mkdirp: 0.5.6
ncp: 2.0.0
rimraf: 2.4.5
dev: false
optional: true
/mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
dependencies:
@ -5028,12 +4957,6 @@ packages:
object-assign: 4.1.1
thenify-all: 1.6.0
/nan@2.18.0:
resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==}
requiresBuild: true
dev: false
optional: true
/nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -5070,13 +4993,6 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
/ncp@2.0.0:
resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==}
hasBin: true
requiresBuild: true
dev: false
optional: true
/new-github-issue-url@0.2.1:
resolution: {integrity: sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==}
engines: {node: '>=10'}
@ -5292,10 +5208,6 @@ packages:
/object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
/object-keys@0.4.0:
resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==}
dev: false
/object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@ -5371,6 +5283,12 @@ packages:
dependencies:
wrappy: 1.0.2
/one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
dependencies:
fn.name: 1.1.0
dev: false
/onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
@ -5901,18 +5819,6 @@ packages:
strip-json-comments: 2.0.1
dev: false
/react-animated-numbers@0.16.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-MUoOsf8fLzwyUL9l6NEMma+29QtfbeYmt8x2LLt4IeLHQWJQfGa4WIUXB/VDVBXEhg74BhCRytdyvhHR3IiHsw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@react-spring/web': 9.7.3(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-intersection-observer: 8.34.0(react@18.2.0)
dev: false
/react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
@ -5931,14 +5837,6 @@ packages:
react: 18.2.0
dev: false
/react-intersection-observer@8.34.0(react@18.2.0):
resolution: {integrity: sha512-TYKh52Zc0Uptp5/b4N91XydfSGKubEhgZRtcg1rhTKABXijc4Sdr1uTp5lJ8TN27jwUsdXxjHXtHa0kPj704sw==}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0
dependencies:
react: 18.2.0
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -6222,15 +6120,6 @@ packages:
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
dev: false
/rimraf@2.4.5:
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
hasBin: true
requiresBuild: true
dependencies:
glob: 6.0.4
dev: false
optional: true
/rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
hasBin: true
@ -6250,6 +6139,14 @@ packages:
dependencies:
queue-microtask: 1.2.3
/rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
requiresBuild: true
dependencies:
tslib: 2.6.2
dev: true
optional: true
/safe-array-concat@1.0.1:
resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==}
engines: {node: '>=0.4'}
@ -6267,12 +6164,6 @@ packages:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/safe-json-stringify@1.2.0:
resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==}
requiresBuild: true
dev: false
optional: true
/safe-regex-test@1.0.0:
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
dependencies:
@ -6286,6 +6177,11 @@ packages:
ret: 0.1.15
dev: true
/safe-stable-stringify@2.4.3:
resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
engines: {node: '>=10'}
dev: false
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
@ -6394,6 +6290,12 @@ packages:
simple-concat: 1.0.1
dev: false
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
dependencies:
is-arrayish: 0.3.2
dev: false
/sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@ -6565,6 +6467,10 @@ packages:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
dev: false
/stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
dev: false
/static-extend@0.1.2:
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
engines: {node: '>=0.10.0'}
@ -6887,6 +6793,10 @@ packages:
supports-hyperlinks: 2.3.0
dev: false
/text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
dev: false
/text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
@ -6973,6 +6883,11 @@ packages:
hasBin: true
dev: true
/triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
dev: false
/ts-api-utils@1.0.3(typescript@5.2.2):
resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
engines: {node: '>=16.13.0'}
@ -7014,7 +6929,6 @@ packages:
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
/tsup@7.2.0(postcss@8.4.31)(typescript@5.2.2):
resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==}
@ -7133,6 +7047,12 @@ packages:
for-each: 0.3.3
is-typed-array: 1.1.12
/typed-emitter@2.1.0:
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
optionalDependencies:
rxjs: 7.8.1
dev: true
/typescript@5.2.2:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}
@ -7372,6 +7292,32 @@ packages:
string-width: 4.2.3
dev: false
/winston-transport@4.6.0:
resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==}
engines: {node: '>= 12.0.0'}
dependencies:
logform: 2.6.0
readable-stream: 3.6.2
triple-beam: 1.4.1
dev: false
/winston@3.11.0:
resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==}
engines: {node: '>= 12.0.0'}
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.3
async: 3.2.5
is-stream: 2.0.1
logform: 2.6.0
one-time: 1.0.0
readable-stream: 3.6.2
safe-stable-stringify: 2.4.3
stack-trace: 0.0.10
triple-beam: 1.4.1
winston-transport: 4.6.0
dev: false
/wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
dev: true
@ -7403,13 +7349,6 @@ packages:
bufferutil: 4.0.8
dev: false
/xtend@2.1.2:
resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==}
engines: {node: '>=0.4'}
dependencies:
object-keys: 0.4.0
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}

View file

@ -1,5 +1,5 @@
/* tailwind does not have text-shadow support, so we do it ourselves */
.stat-card > p {
.stat-card {
/* -webkit-text-stroke: 1px theme("colors.card.DEFAULT"); */
text-shadow:
1px 1px 0 theme("colors.card.DEFAULT"),

View file

@ -3,21 +3,22 @@
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { ResponsiveContainer, AreaChart, Area } from "recharts";
import styles from "./StatCard.module.css";
// import AnimatedNumber from "react-animated-numbers";
import dynamic from "next/dynamic";
const AnimatedNumber = dynamic(() => import("react-animated-numbers"), {
ssr: false,
});
import { AnimatedNumber } from "~/components/AnimatedPercent";
export function StatCard<T extends Record<string, number>>(props: {
title: string;
value: number;
unit: string;
subvalue: string;
icon: React.FC<{ className: string }>;
data: T[];
dataKey: keyof T & string;
value: number;
unit?: string;
subvalue?: string;
secondaryValue?: number;
secondaryUnit?: string;
secondarySubvalue?: string;
}) {
const rechartsColorId = `color${props.dataKey}`;
const Icon = props.icon;
@ -79,21 +80,36 @@ export function StatCard<T extends Record<string, number>>(props: {
</ResponsiveContainer>
</div>
<div className={`relative z-10 ${styles["stat-card"]}`}>
<p className="stroke stroke-card text-2xl font-bold">
<AnimatedNumber
animateToNumber={128391}
includeComma
fontStyle={{
fontSize: 24,
// fontWeight: "inherit",
}}
/>
{props.unit}
</p>
<p className="stroke stroke-card text-sm text-muted-foreground">
{props.subvalue}
</p>
<div className="relative z-10">
<div className="stroke flex flex-row stroke-card text-2xl font-bold">
<div>
<AnimatedNumber number={props.value} />
{props.unit}
{props.subvalue !== undefined && (
<p
className={`stroke stroke-card text-sm text-muted-foreground ${styles["stat-card"]} font-normal`}
>
{props.subvalue}
</p>
)}
</div>
{props.secondaryValue !== undefined && (
<div className="ml-auto mr-0 text-right">
<AnimatedNumber number={props.secondaryValue} />
{props.secondaryUnit}
{props.secondarySubvalue && (
<p
className={`stroke stroke-card text-sm text-muted-foreground ${styles["stat-card"]} text-right font-normal`}
>
{props.secondarySubvalue}
</p>
)}
</div>
)}
</div>
</div>
</CardContent>
</Card>

View file

@ -3,7 +3,12 @@
import { api } from "~/trpc/react";
import { StatCard } from "./StatCard";
import { RouterOutputs } from "~/trpc/shared";
import { RiPulseFill } from "react-icons/ri";
import {
FaMicrochip,
FaMemory,
FaHardDrive,
FaEthernet,
} from "react-icons/fa6";
import { useState } from "react";
const TEST_DATA = [
@ -32,35 +37,50 @@ export function SystemStatistics(props: { initialData: StatData }) {
<div className="m-8 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
<StatCard
title="CPU Usage"
// value={`${data.cpu.usage ?? 0}%`}
value={data.cpu.usage}
unit="%"
subvalue={`of ${data.cpu.cores} CPUs`}
icon={RiPulseFill}
icon={FaMicrochip}
data={TEST_DATA}
dataKey="cpu"
/>
<StatCard
title="Memory Usage"
value={`${((data.memory.used / data.memory.total) * 100).toFixed(2)}%`}
value={data.memory.used / data.memory.total}
unit="%"
subvalue={`${data.memory.used.toFixed(2)} / ${data.memory.total.toFixed(
2,
)} GB`}
icon={RiPulseFill}
icon={FaMemory}
data={TEST_DATA}
dataKey="cpu"
/>
<StatCard
title="Disk Usage"
value={`${((data.storage.used / data.storage.total) * 100).toFixed(
2,
)}%`}
value={data.storage.used / data.storage.total}
unit="%"
subvalue={`${data.storage.used.toFixed(
2,
)} / ${data.storage.total.toFixed(2)} GB`}
icon={RiPulseFill}
icon={FaHardDrive}
data={TEST_DATA}
dataKey="cpu"
/>
<StatCard
title="Network Usage"
// TX
value={data.network.tx}
// unit="Mbps"
subvalue="Mbps / TX"
// RX
secondaryValue={data.network.rx}
// secondaryUnit="Mbps"
secondarySubvalue="RX / Mbps"
// misc
icon={FaEthernet}
data={TEST_DATA}
dataKey="cpu"
/>

View file

@ -0,0 +1,33 @@
@property --percent {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
@property --temp {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
@property --v1 {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
@property --v2 {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
.animated-percent {
transition: --percent 1.5s;
transition-timing-function: cubic-bezier(0.38, 0, 0, 1);
--temp: calc(var(--percent) * 100);
--v1: max(var(--temp) - 0.5, 0);
--v2: max((var(--temp) - var(--v1)) * 100 - 0.5, 0);
counter-reset: v1 var(--v1) v2 var(--v2);
}
.animated-percent::before {
content: counter(v1) "." counter(v2, decimal-leading-zero);
}

View file

@ -0,0 +1,27 @@
"use client";
import { useEffect, useRef } from "react";
import { cn } from "~/utils/utils";
import styles from "./AnimatedPercent.module.css";
export type AnimatedNumberProps = {
number: number;
className?: string;
};
export function AnimatedNumber(props: AnimatedNumberProps) {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
divRef.current?.style.setProperty("--percent", props.number.toString());
}, [props.number]);
return (
<div className={cn("relative inline-block", props.className)}>
<div className="absolute inset-0 text-transparent">
{props.number.toFixed(2)}
</div>
<div className={styles["animated-percent"]} ref={divRef} />
</div>
);
}

View file

@ -29,7 +29,7 @@ export const env = createEnv({
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
NEXT_PUBLIC_BUILD_COMMIT_SHA: z.string().default("unknown"),
},
/**
@ -43,6 +43,7 @@ export const env = createEnv({
SESSION_SECRET: process.env.SESSION_SECRET,
HOSTNAME: process.env.HOSTNAME,
PORT: process.env.PORT,
NEXT_PUBLIC_BUILD_COMMIT_SHA: process.env.NEXT_PUBLIC_BUILD_COMMIT_SHA,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially

View file

@ -1,39 +0,0 @@
// import pacakge from "../package.json";
import { env } from "~/env";
// const { version } = pacakge;
const version = "1.0.0";
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const logger = (await import("./server/utils/logger")).default;
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
const { db } = await import("./server/db");
const { mkdir, stat } = await import("fs/promises");
const path = await import("path");
// check if database folder exists
try {
const dir = path.dirname(env.DATABASE_PATH);
await stat(dir);
} catch (e) {
await mkdir(path.dirname(env.DATABASE_PATH), { recursive: true });
logger.debug(
`Created database folder ${path.dirname(env.DATABASE_PATH)}`,
);
}
if (env.NODE_ENV === "production") {
logger.child({ module: "database" }).info("⚙️ Migrating database");
migrate(db, { migrationsFolder: "./migrations" });
logger.child({ module: "database" }).info("✅ Database migrated");
} else {
logger
.child({ module: "database" })
.info(
"Not running database migrations, use drizzle-kit push to migrate",
);
}
logger.info(`🚀 Hostforge v${version} ready!`);
}
}

View file

@ -1,52 +1,20 @@
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
import os from "os";
import osu from "node-os-utils";
import { observable } from "@trpc/server/observable";
async function fetchSystemInfo() {
const [cpuUsage, storage, memory] = await Promise.all([
osu.cpu.usage(),
osu.drive.info("/"),
osu.mem.info(),
]);
return {
cpu: {
usage: cpuUsage,
cores: os.cpus().length,
},
storage: {
used: parseInt(storage.usedGb),
total: parseInt(storage.totalGb),
},
memory: {
used: memory.usedMemMb / 1024,
total: memory.totalMemMb / 1024,
},
};
}
import { BasicServerStats, stats } from "~/server/modules/stats";
export const systemRouter = createTRPCRouter({
currentStats: authenticatedProcedure.query(async ({ ctx }) => {
return fetchSystemInfo();
return stats.getCurrentStats();
}),
liveStats: authenticatedProcedure.subscription(async ({ ctx }) => {
return observable<Awaited<ReturnType<typeof fetchSystemInfo>>>(
(observer) => {
console.log("subscription got");
fetchSystemInfo().then(observer.next.bind(observer));
return observable<BasicServerStats>((observer) => {
const update = observer.next.bind(observer);
const interval = setInterval(async () => {
observer.next(await fetchSystemInfo());
}, 1000);
return () => {
clearInterval(interval);
};
},
);
stats.events.on("onUpdate", update);
return () => {
stats.events.off("onUpdate", update);
};
});
}),
});

View file

@ -56,8 +56,6 @@ export const createTRPCContext = async (opts: {
req: NextRequest;
resHeaders: Headers;
}) => {
console.log("contexting ", opts.req.url);
// disable caching
opts.resHeaders.set("Cache-Control", "no-store");

View file

@ -0,0 +1,225 @@
import EventEmitter from "events";
import TypedEmitter from "typed-emitter";
import osu from "node-os-utils";
import os from "os";
import baseLogger from "../../utils/logger";
export type BasicServerStats = {
collectedAt: Date;
cpu: {
/**
* The CPU usage, each core = 100%.
* In decimal form, so 0.5 = 50%.
*/
usage: number;
/**
* The number of cores the CPU has.
*/
cores: number;
};
storage: {
/**
* The amount of storage used in GB.
*/
used: number;
/**
* The total amount of storage in GB.
*/
total: number;
};
memory: {
/**
* The amount of memory used in GB.
*/
used: number;
/**
* The total amount of memory in GB.
*/
total: number;
};
network: {
/**
* The tx (upload) speed in bytes per second.
*/
tx: number;
/**
* The rx (download) speed in bytes per second.
*/
rx: number;
};
};
type StatEvents = {
/**
* Whenever new stats are collected
* @param stats The new stats.
* @returns
*/
onUpdate: (stats: BasicServerStats) => void;
/**
* Built-in Node.JS event, emitted whenever a new listener is added.
*/
newListener: (
event: string | symbol,
listener: (...args: any[]) => void,
) => void;
/**
* Built-in Node.JS event, emitted whenever a listener is removed.
*/
removeListener: (
event: string | symbol,
listener: (...args: any[]) => void,
) => void;
};
/**
* Manages the stats for the current server.
*/
export class StatManager {
private logger = baseLogger.child({ module: "StatManager" });
/**
* The current stats for the server.
*/
private currentStats: BasicServerStats = {
collectedAt: new Date(0),
cpu: {
usage: 0,
cores: 0,
},
storage: {
used: 0,
total: 0,
},
memory: {
used: 0,
total: 0,
},
network: {
tx: 0,
rx: 0,
},
};
/**
* The event emitter for the stat manager.
*/
public readonly events = new EventEmitter() as TypedEmitter<StatEvents>;
/**
* When live stats are needed, this is the interval that is used.
*/
private liveInterval: NodeJS.Timeout | null = null;
constructor() {
this.update();
// collect stats every hour
setInterval(
async () => {
await this.update();
await this.updateDatabase();
},
60 * 60 * 1000,
);
// whenever a new listener is added, start the live interval
this.events.on("newListener", (event) => {
if (event === "onUpdate") {
this.liveInterval ??= setInterval(async () => {
await this.update();
}, 3 * 1000);
}
});
// unregister the event when the listener is removed
this.events.on("removeListener", (event) => {
if (event === "onUpdate" && this.events.listenerCount("onUpdate") === 0) {
if (this.liveInterval === null) return;
clearInterval(this.liveInterval);
this.liveInterval = null;
}
});
}
/**
* Gets the current stats for the server.
*/
async getCurrentStats() {
// return the current stats if they were collected within the last 5 minutes
if (Date.now() - this.currentStats.collectedAt.getTime() < 5 * 60 * 1000) {
return this.currentStats;
}
// otherwise, update the stats and return them
return this.update();
}
/**
* Updates the stats for the current server and pushes them to the database.
*/
async update() {
const [cpuUsage, storage, memory, network] = await Promise.all([
osu.cpu.usage(),
osu.drive.info("/"),
osu.mem.info(),
osu.netstat.inOut(),
]);
if (typeof network === "string") {
this.logger.warn(
"Failed to get network stats, got string instead of object: ",
network,
);
}
this.currentStats = {
collectedAt: new Date(),
cpu: {
usage: cpuUsage / 100,
cores: os.cpus().length,
},
storage: {
used: parseInt(storage.usedGb),
total: parseInt(storage.totalGb),
},
memory: {
used: memory.usedMemMb / 1024,
total: memory.totalMemMb / 1024,
},
network: {
tx: typeof network === "string" ? -1 : network.total.outputMb,
rx: typeof network === "string" ? -1 : network.total.inputMb,
},
};
this.events.emit("onUpdate", this.currentStats);
return this.currentStats;
}
/**
* Updates the database with the current stats.
*/
async updateDatabase() {}
}
export const stats = new StatManager();

View file

@ -1,88 +1,102 @@
// import { config } from "dotenv";
// config();
import "dotenv/config";
import next from "next";
import { env } from "~/env";
import { createServer } from "http";
import logger from "./utils/logger";
// import ws from "ws";
import { WebSocketServer } from "ws";
import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { appRouter } from "./api/root";
import { createTRPCContext } from "./api/trpc";
import { incomingRequestToNextRequest } from "./utils/serverUtils";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { db } from "./db";
import { mkdir, stat } from "fs/promises";
import path from "path";
import { version } from "../../package.json";
async function startApp() {
// initialize the next app
const app = next({
dev: env.NODE_ENV !== "production",
hostname: env.HOSTNAME,
port: env.PORT,
// dir: path.join(__dirname, "../.."),
customServer: true,
isNodeDebugging: true,
});
await app.prepare();
// get the handles
const getHandler = app.getRequestHandler();
const upgradeHandler = app.getUpgradeHandler();
// create the http server
const server = createServer((req, res) => {
console.log("req", req.url);
getHandler(req, res).catch((error) => {
logger.error(error);
res.statusCode = 500;
res.end("Internal Server Error");
});
});
// create the websocket server
const wss = new WebSocketServer({ noServer: true });
const trpcHandler = applyWSSHandler({
wss,
router: appRouter,
createContext: ({ req }) => {
console.log("createContext", req.url);
return createTRPCContext({
req: incomingRequestToNextRequest(req),
resHeaders: new Headers(),
});
},
});
process.on("SIGTERM", () => {
logger.warn("SIGTERM received, shutting down...");
trpcHandler.broadcastReconnectNotification();
server.close(() => {
process.exit(0);
});
});
// handle the upgrade
server.on("upgrade", (req, socket, head) => {
console.log("upgrade", req.url);
// send trpc requests to the trpc server
if (req.url?.startsWith("/api/trpc")) {
console.log("🚚 passing upgrade to tRPC");
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
} else {
console.log("🆙 ws for next.js recieved");
void upgradeHandler(req, socket, head);
}
});
// start the server
server.listen(env.PORT, () => {
logger.info(`Server listening on port ${env.PORT}`);
});
// check if database folder exists
try {
const dir = path.dirname(env.DATABASE_PATH);
await stat(dir);
} catch (e) {
await mkdir(path.dirname(env.DATABASE_PATH), { recursive: true });
logger.debug(`Created database folder ${path.dirname(env.DATABASE_PATH)}`);
}
void startApp();
// migrate the database
if (env.NODE_ENV === "production") {
logger.child({ module: "database" }).info("⚙️ Migrating database");
migrate(db, { migrationsFolder: "./migrations" });
logger.child({ module: "database" }).info("✅ Database migrated");
} else {
logger
.child({ module: "database" })
.info("Not running database migrations, use drizzle-kit push to migrate");
}
// initialize the next app
const app = next({
dev: env.NODE_ENV !== "production",
hostname: env.HOSTNAME,
port: env.PORT,
// dir: path.join(__dirname, "../.."),
customServer: true,
isNodeDebugging: true,
});
await app.prepare();
// get the handles
const getHandler = app.getRequestHandler();
const upgradeHandler = app.getUpgradeHandler();
// create the http server
const server = createServer((req, res) => {
getHandler(req, res).catch((error) => {
logger.error(error);
res.statusCode = 500;
res.end("Internal Server Error");
});
});
// create the websocket server
const wss = new WebSocketServer({ noServer: true });
const trpcHandler = applyWSSHandler({
wss,
router: appRouter,
createContext: ({ req }) => {
return createTRPCContext({
req: incomingRequestToNextRequest(req),
resHeaders: new Headers(),
});
},
});
process.on("SIGTERM", () => {
logger.warn("SIGTERM received, shutting down...");
trpcHandler.broadcastReconnectNotification();
server.close(() => {
process.exit(0);
});
});
// handle the upgrade
server.on("upgrade", (req, socket, head) => {
// send trpc requests to the trpc server
if (req.url?.startsWith("/api/trpc")) {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
} else {
void upgradeHandler(req, socket, head);
}
});
// start the server
server.listen(env.PORT, env.HOSTNAME, () => {
logger.info(`🚀 Hostforge`);
logger.info(`│ Server listening on ${env.HOSTNAME}:${env.PORT}`);
logger.info(`│ Version: ${version}`);
logger.info(`│ Environment: ${env.NODE_ENV}`);
logger.info(`╰ Build commit: ${env.NEXT_PUBLIC_BUILD_COMMIT_SHA}`);
});

View file

@ -1,10 +1,28 @@
import bunyan from "bunyan";
import bunyanFormat from "bunyan-format";
import { createLogger, format, transports } from "winston";
import chalk from "chalk";
const formatOut = bunyanFormat({ outputMode: "short" });
const logger = bunyan.createLogger({
name: "hostforge",
streams: [{ stream: formatOut }],
const logger = createLogger({
transports: [
new transports.Console({
// HH:MM:SS.mmm level hostforge.<module>: message
format: format.combine(
format.colorize(),
format.timestamp({
format() {
return chalk.gray(new Date().toISOString().split("T")[1]);
},
}),
format.printf(({ level, message, timestamp, module }) => {
return `${timestamp} ${level} ${chalk.cyan(
(module ?? "main") + ":",
)} ${message}`;
}),
),
}),
new transports.File({ filename: "logs/error.log", level: "error" }),
new transports.File({ filename: "logs/combined.log" }),
],
});
export default logger;