Compare commits

...

37 Commits

Author SHA1 Message Date
Daniel Legt cf313cdaf3
Merge pull request #4 from manjarisri/feature
Modified Dockerfile and reduced image size
2024-01-24 11:07:02 +02:00
Manjari Srivastav 28b319c3c5
Add files via upload
optimised Dockerfile
2024-01-24 12:29:59 +05:30
Manjari Srivastav 30aa7f7bde
Delete Dockerfile 2024-01-24 12:28:22 +05:30
Manjari Srivastav 4aaa187156
Delete drive-health directory 2024-01-24 12:27:49 +05:30
Manjari Srivastav 3ba65b93c0
Add files via upload 2024-01-24 12:22:35 +05:30
Daniel Legt de08a7f970 Fixed major bug
* Resolved the hwmon not found problem
2024-01-22 12:39:31 +02:00
Daniel Legt d6588742d3 Updated Information
* Updated `build.sh`
* Updated `README.md`
2024-01-22 12:24:27 +02:00
Daniel Legt 44d4237bec Fixed Static folder push 2024-01-22 02:34:27 +02:00
Daniel Legt f2c84fb6b2 Fixed Dockerfile 2024-01-22 02:23:43 +02:00
Daniel Legt 4f50819f92 Production docker-compose.yml updates 2024-01-22 02:12:18 +02:00
Daniel Legt 6117157598 DockerFile link to repository
* Fixed issue
2024-01-22 02:01:39 +02:00
Daniel Legt 79e44bd88a + Production docker-compose.yml 2024-01-22 01:59:38 +02:00
Daniel Legt c556e3827b Updated deploy 2024-01-22 01:58:43 +02:00
Daniel Legt 63c92ac272 Docker Implementation 2024-01-22 01:49:13 +02:00
Daniel Legt 10bb300087 * Updated README 2024-01-22 00:47:09 +02:00
Daniel Legt 6d1bb15ea6 Updated Readme 2024-01-22 00:45:12 +02:00
Daniel Legt f039369ec1 * Updated Readme 2024-01-22 00:36:57 +02:00
Daniel Legt b4574eb73d + License
* Migrated to github
2024-01-22 00:36:05 +02:00
Daniel Legt cdbab95930 * Readme Update 2024-01-22 00:28:24 +02:00
Daniel Legt 1d970aa6ba Rootless Run
+ Debug Tools
* ReadME
* Optimized config, now it's cached
* ReWrote and optimized logic.go
* rewrote and optimized
+ Styling improvements
2024-01-22 00:28:07 +02:00
Daniel Legt d7e856aca2 Fixed Bugs
* Fixed gin release mode
- Removed debug logs
2024-01-21 22:56:53 +02:00
Daniel Legt 39e16ce408 * Ginmode force 2024-01-21 22:50:50 +02:00
Daniel Legt 40e02abe87 * Fixed Styling 2024-01-21 22:44:50 +02:00
Daniel Legt c8fa24f11c Date filtering
+ Date filtering
+ Input styling
* Visual changes
+ Updated api calls
2024-01-21 22:40:32 +02:00
Daniel Legt 2776ad8e52 * Fixed config defaults 2024-01-21 21:43:24 +02:00
Daniel Legt f3905bb822 Cleanup Service
* Updated .env
+ Implemented automatical removal of old logs
2024-01-21 21:42:58 +02:00
Daniel Legt 92baa56a1c Re-Added NVME 2024-01-21 19:14:34 +02:00
Daniel Legt 4c877e7162 Styling and fixes 2024-01-21 19:12:40 +02:00
Daniel Legt cbe252fe94 Implemented Authentication
+ Basic Authentication
2024-01-21 18:40:40 +02:00
Daniel Legt 07dec16aa4 Better .env 2024-01-21 18:27:31 +02:00
Daniel Legt 545eed44cd Implemented frontend
+ Graph Generation as images for frontend performance
+ A new decent style
2024-01-21 18:25:23 +02:00
Daniel Legt eb94ce4552 Fixed database extension 2024-01-21 15:27:42 +02:00
Daniel Legt c0f1ed6879 Started moving to a sqlite3 implementation 2024-01-20 18:35:50 +02:00
Daniel Legt c4aef27eda Temp Workaround 2024-01-20 02:15:02 +02:00
Daniel Legt 8f8da162e9 Removed junk 2024-01-20 02:14:20 +02:00
Daniel Legt 5cc58c7d53 Build Script 2024-01-20 02:11:59 +02:00
Daniel Legt 21f24899a2 * Custom listen 2024-01-20 02:09:10 +02:00
29 changed files with 1445 additions and 417 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
/**
!main.go
!/lib
!/templates
!/static
!/go.mod
!/go.sum

View File

@ -1,3 +1,28 @@
#
# All time references are in seconds
#
#
############################################################
# The frequency at which to fetch the temperature of ALL disks and add it to the database
DISK_FETCH_FREQUENCY=5
MEMORY_DUMP_FREQUENCY=60
MAX_HISTORY_AGE=2592000
# How ofthen should the program clean the database of old logs
CLEANUP_SERVICE_FREQUENCY=3600
# The maximum age of logs in seconds
# 1 Day = 86400
# 1 Week = 604800
# 1 Month ~= 2592000
# Recommended 1 week
MAX_HISTORY_AGE=2592000
# The ip:port to listen to for the application
LISTEN=":8080"
# Basic Security, these are required to view the data
IDENTITY_USERNAME=admin
IDENTITY_PASSWORD=admin
# Enable/Disable debug features
DEBUG_MODE=false

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
snapshots.dat
.env
dist
data.db
data.sqlite
dev_data

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}"
}
]
}

65
Dockerfile Normal file
View File

@ -0,0 +1,65 @@
# === Build Stage ===
FROM debian:bullseye-slim AS builder
ENV IS_DOCKER TRUE
LABEL org.opencontainers.image.source https://github.com/JustKato/drive-health
# Install build dependencies and runtime dependencies
RUN apt-get update && apt-get install -y \
gcc \
musl-dev \
libsqlite3-dev \
libsqlite3-0 \
wget \
&& rm -rf /var/lib/apt/lists/*
# Manually install Go 1.21
ENV GOLANG_VERSION 1.21.0
RUN wget https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz -O go.tgz \
&& tar -C /usr/local -xzf go.tgz \
&& rm go.tgz
ENV PATH /usr/local/go/bin:$PATH
# Set the environment variable for Go
ENV GOPATH=/go
ENV PATH=$GOPATH/bin:$PATH
ENV GO111MODULE=on
# Create the directory and set it as the working directory
WORKDIR /app
# Copy the Go files and download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source from the current directory to the Working Directory inside the container
COPY . .
# Build the Go app
RUN go build -o drive-health
# Cleanup build dependencies to reduce image size
RUN apt-get purge -y gcc musl-dev libsqlite3-dev wget \
&& apt-get autoremove -y \
&& apt-get clean
# === Final Stage ===
FROM debian:bullseye-slim AS final
# Set the environment variable
ENV IS_DOCKER TRUE
# Create the directory and set it as the working directory
WORKDIR /app
# Copy only the necessary files from the builder stage
COPY --from=builder /app/drive-health .
# Expose the necessary port
EXPOSE 8080
# Volume for external data
VOLUME [ "/data" ]
# Command to run the executable
CMD ["./drive-health"]

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright [2024] [Daniel Legt]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,11 +1,98 @@
## About
## 📖 About
Drive Health is a program written in golang to help with tracking and monitoring of your hardware's temperature.
## Disclaimer
This tool had been conceived with the purpose of installing it on different servers I own with different configurations to help keep track of the temperature of hard-disks, ssds, nvme drives, etc...
This software is only meant to run on linux, it's something I threw together all at once in one sitting without really thinking about structure or even caring about anything but making it work.
### Features
- Disk Listing
- Temperature Graphing
- Disk activity logging
- [API](./lib/web/api.go)
It's actually meant to run on my own server just so I can track the temperature of my harddrives in a live environment, I just want this service to run, record the status of the harddrives, and be able via my VPN to access it in a private manner, it's not meant to be dockerized, it's not meant to be ran in a VM, it's meant to be ran either as a `service` or in a [screen](https://git.savannah.gnu.org/cgit/screen.git) in the background.
![UI Example](./media/design_v1.webp)
It's not really meant to run all the time or anything like that, I just wanted to write this disclaimer, feel free to use the code any way you want to :)
## ❗ Disclaimer
I'm not exactly a linux hardware wizard, so I honestly have no clue about a lot of things and I myself can tell there's a lot to improve upon and that there's a lot of other things missing that are a little bit more obscure, I personally don't currently own any m.2 sata drives to test the code on, or many of the other drive types, I have only tested on HDD, SSD and NVMe drives, any issues opened would help me so much!
## How to use
## ❗ Requirements
1. A linux machine, this will NOT work on macOS or on Windows, it's meant to be ran on servers as a service with which administrators can privately connect to for temperature logging.
2. Please make sure you have the [**drivetemp kernel drive**](https://docs.kernel.org/hwmon/drivetemp.html) you can check this by running `sudo modprobe drivetemp`.
The program depends on this to be able to log the temperature of your devices.
## 📖 How to use
1. Follow the `Deployment` section instrcutions to launch the program
2. Once the program has launched, access it in your browser
3. Enter the administrative username and password for the simple HTTP Auth
4. You now have access to the application, you can monitor your disk's temperature over a period of time.
## 🐦 Deployment
To deploy the application you have multiple choices, the preffered method should be one which runs the binary directly and not containerization, the `docker` image is taking up a wopping `1Gb+` because I have to include sqlite3-dev and musl-dev dependencies, which sucks, so I whole heartedly recommend just installing this on your system as a binary either with `SystemD` or whichever service manager you are using.
Download binaries from [the releases page](https://github.com/JustKato/drive-health/releases)
### 🐋 Docker
In the project there's a `docker-compose.prod.yml` which you can deploy on your server, you will notice that there's also a "dev" version, this version simply has a `build` instead of `image` property, so feel free to use either.
Please do take notice that I have just fed the `environment file` directly to the service via docker-compose, and I recommend you do the same but please feel free to pass in `environment` variables straight to the process as well.
[Docker Compose File](./docker-compose.prod.yml)
```yaml
version: "3.8"
services:
drive-health:
# Latest image pull, mention the specific version here please.
image: ghcr.io/justkato/drive-health:latest
# Restart in case of crashing
restart: unless-stopped
# Load environment variables from .env file
env_file:
- .env
# Mount the volume to the local drive
volumes:
- ./data:/data
# Setup application ports
ports:
- 5003:8080
```
### 💾 SystemD
When running with SystemD or any other service manager, please make sure you have a `.env` inside the `WorkingDirectory` of your runner, in the below example I will simply put my env in `/home/daniel/services/drive-health/.env`
```ini
[Unit]
Description=Drive Health Service
After=network.target
[Service]
Type=simple
User=daniel # Your user here
WorkingDirectory=/home/daniel/services/drive-health # The path to the service's directory
ExecStart=/home/daniel/services/drive-health/drive-health # The path to the binary
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
## ❔ FAQ
### How does it work?
Currently the program does not depend on any go library for hardware detection as I couldn't find anything that would not require root access while giving me the possibility to interrogate the temperature of the drives.
I chose not to depend on `lsblk` either, so how does the program work?
The program currently looks in `/sys/block` and then tries to make sense of the devices, I have had limited testing with my hardware specs, any issues being open in regards to different kinds of hardware would be highly appreciated
### Why not just run as root?
I really, REALLY, **REALLY** want to avoid asking people to run **ANY** program I write as root and even try and prevent that from happening since that's how things can go bad, especially because I am running actions over hardware devices.
## Support & Contribution
For support, bug reports, or feature requests, please open an issue on the [GitHub repository](https://github.com/JustKato/drive-health/issues). Contributions are welcome! Fork the repository, make your changes, and submit a pull request.
## License
This project is licensed under the [Apache License 2.0](./LICENSE).

50
build.sh Executable file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -o pipefail
set -u
# Function to display messages in color
echo_color() {
color=$1
text=$2
case $color in
"green") echo -e "\033[0;32m$text\033[0m" ;;
"yellow") echo -e "\033[0;33m$text\033[0m" ;;
"red") echo -e "\033[0;31m$text\033[0m" ;;
*) echo "$text" ;;
esac
}
# Getting GIT_VERSION from the most recent tag or commit hash
GIT_VERSION=$(git describe --tags --always)
if [ -z "$GIT_VERSION" ]; then
echo_color red "Error: Unable to determine GIT_VERSION."
exit 1
fi
APP_NAME="drive-health"
DIST_DIR="${DIST_DIR:-dist}"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# make sure we are in the source dir
cd $SCRIPT_DIR;
# Create the dist directory if it doesn't exist
mkdir -p $DIST_DIR
# Build the application
echo_color yellow "[🦝] Building the application..."
GOOS=linux CGO_ENABLED=1 GOARCH=amd64 go build -o $DIST_DIR/$APP_NAME
# Copying additional resources...
cp -r static templates $DIST_DIR/
echo_color yellow "[🦝] Compilation and packaging completed, archiving..."
cd $DIST_DIR/
zip "drive-health_$GIT_VERSION.zip" -r .
# TODO: Add reliable method of cleaning up the compiled files optionally
cd $SCRIPT_DIR;

65
deploy.sh Executable file
View File

@ -0,0 +1,65 @@
#!/bin/bash
# Function to display messages in color
echo_color() {
color=$1
text=$2
case $color in
"green") echo -e "\033[0;32m$text\033[0m" ;;
"yellow") echo -e "\033[0;33m$text\033[0m" ;;
"red") echo -e "\033[0;31m$text\033[0m" ;;
*) echo "$text" ;;
esac
}
# Getting GIT_VERSION from the most recent tag or commit hash
GIT_VERSION=$(git describe --tags --always)
if [ -z "$GIT_VERSION" ]; then
echo_color red "Error: Unable to determine GIT_VERSION."
exit 1
fi
# Run tests before proceeding
echo_color yellow "Running tests..."
if ! go test; then
echo_color red "Tests failed. Cancelling build process."
exit 1
fi
echo_color green "All tests passed successfully."
echo_color green "Starting the Docker build process with version $GIT_VERSION..."
LATEST_IMAGE_NAME="ghcr.io/justkato/drive-health:latest"
IMAGE_NAME="ghcr.io/justkato/drive-health:$GIT_VERSION"
echo_color yellow "Image to be built: $IMAGE_NAME"
# Confirmation to build
read -p "Are you sure you want to build an image? (y/N) " response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
# Building the Docker image
echo "Building Docker image: $IMAGE_NAME"
docker build --no-cache -t $IMAGE_NAME .
# Also tag this build as 'latest'
echo "Tagging image as latest: $LATEST_IMAGE_NAME"
docker tag $IMAGE_NAME $LATEST_IMAGE_NAME
else
echo_color red "Build cancelled."
exit 1
fi
# Prompt to push the image
read -p "Push image to repository? (y/N) " push_response
if [[ "$push_response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
# Pushing the image
echo "Pushing image: $IMAGE_NAME"
docker push $IMAGE_NAME
# Pushing the 'latest' image
echo "Pushing latest image: $LATEST_IMAGE_NAME"
docker push $LATEST_IMAGE_NAME
else
echo_color red "Push cancelled."
fi
echo_color green "Ending the Docker build process..."

14
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,14 @@
version: "3.8"
services:
drive-health:
# Build the current image
build: .
# Read straight from the .env file, or use the environment path
env_file:
- .env
volumes:
- ./dev_data:/data
# Setup application ports
ports:
- 8080:8080

17
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,17 @@
version: "3.8"
services:
drive-health:
# Latest image pull, mention the specific version here please.
image: ghcr.io/justkato/drive-health:latest
# Restart in case of crashing
restart: unless-stopped
# Load environment variables from .env file
env_file:
- .env
# Mount the volume to the local drive
volumes:
- ./data:/data
# Setup application ports
ports:
- 5003:8080

19
go.mod
View File

@ -1,24 +1,34 @@
module tea.chunkbyte.com/kato/drive-health
module github.com/JustKato/drive-health
go 1.21.6
require (
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3 // indirect
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
github.com/wcharczuk/go-chart/v2 v2.1.1
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/blend/go-sdk v1.20220411.3 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.17.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
@ -26,6 +36,7 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/image v0.11.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect

63
go.sum
View File

@ -1,5 +1,5 @@
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3 h1:kAF2MWFD8tyDqD74OQizymjj2cnZAURwSzBrEslCDnI=
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3/go.mod h1:llkexGSe52bW0OjNva0kvIqGZxfSnVfpKHrnKBI2+pU=
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
github.com/blend/go-sdk v1.20220411.3/go.mod h1:7lnH8fTi6U4i1fArEXRyOIY2E1X4MALg09qsQqY1+ak=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
@ -12,6 +12,7 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
@ -19,6 +20,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@ -27,9 +30,15 @@ github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -42,6 +51,8 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -49,6 +60,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@ -59,32 +71,71 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,29 +1,46 @@
package config
import (
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)
type DHConfig struct {
DiskFetchFrequency int `json:"diskFetchFrequency"`
MemoryDumpFrequency int `json:"memoryDumpFrequency"`
MaxHistoryAge int `json:"maxHistoryAge"`
CleanupServiceFrequency int `json:"cleanupServiceFrequency"`
DiskFetchFrequency int `json:"diskFetchFrequency"`
MaxHistoryAge int `json:"maxHistoryAge"`
DatabaseFilePath string `json:"databaseFilePath"`
Listen string `json:"listen"`
IdentityUsername string `json:"identityUsername"`
IdentityPassword string `json:"identityPassword"`
IsDocker bool `json:isDocker`
DebugMode bool `json:"debugMode"`
}
func GetConfiguration() DHConfig {
// Load .env file if it exists
if err := godotenv.Load(); err != nil {
log.Println("No .env file found")
var config *DHConfig = nil
func GetConfiguration() *DHConfig {
if config != nil {
return config
}
config := DHConfig{
DiskFetchFrequency: 5, // default value
MemoryDumpFrequency: 60, // default value
MaxHistoryAge: 2592000, // default value
config = &DHConfig{
DiskFetchFrequency: 5,
CleanupServiceFrequency: 3600,
MaxHistoryAge: 2592000,
DatabaseFilePath: "./data.sqlite",
IdentityUsername: "admin",
IdentityPassword: "admin",
IsDocker: false,
Listen: ":8080",
}
if val, exists := os.LookupEnv("DISK_FETCH_FREQUENCY"); exists {
@ -32,9 +49,9 @@ func GetConfiguration() DHConfig {
}
}
if val, exists := os.LookupEnv("MEMORY_DUMP_FREQUENCY"); exists {
if val, exists := os.LookupEnv("CLEANUP_SERVICE_FREQUENCY"); exists {
if intValue, err := strconv.Atoi(val); err == nil {
config.MemoryDumpFrequency = intValue
config.CleanupServiceFrequency = intValue
}
}
@ -44,5 +61,35 @@ func GetConfiguration() DHConfig {
}
}
if val, exists := os.LookupEnv("LISTEN"); exists {
config.Listen = val
}
if val, exists := os.LookupEnv("DATABASE_FILE_PATH"); exists {
config.DatabaseFilePath = val
}
if val, exists := os.LookupEnv("IDENTITY_USERNAME"); exists {
config.IdentityUsername = val
}
if val, exists := os.LookupEnv("IDENTITY_PASSWORD"); exists {
config.IdentityPassword = val
}
if val, exists := os.LookupEnv("DEBUG_MODE"); exists {
if isDebug, err := strconv.ParseBool(val); err == nil {
config.DebugMode = isDebug
}
}
if val, exists := os.LookupEnv("IS_DOCKER"); exists {
if isDocker, err := strconv.ParseBool(val); err == nil {
config.IsDocker = isDocker
config.DatabaseFilePath = "/data/data.sqlite"
}
}
return config
}

View File

@ -1,62 +1,198 @@
package hardware
import (
"bufio"
"bytes"
"fmt"
"os/exec"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/JustKato/drive-health/lib/config"
"gorm.io/gorm"
)
func GetSystemHardDrives() ([]*HardDrive, error) {
func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time) ([]*HardDrive, error) {
var systemHardDrives []*HardDrive
// Execute the lsblk command to get detailed block device information
cmd := exec.Command("lsblk", "-d", "-o", "NAME,TRAN,SIZE,MODEL,SERIAL,TYPE")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
// List all block devices
devices, err := os.ReadDir("/sys/block/")
if err != nil {
fmt.Println("Failed to execute command:", err)
return nil, err
return nil, fmt.Errorf("failed to list block devices: %w", err)
}
var hardDrives []*HardDrive
for _, device := range devices {
deviceName := device.Name()
// Scan the output line by line
scanner := bufio.NewScanner(&out)
for scanner.Scan() {
line := scanner.Text()
// Skip the header line
if strings.Contains(line, "NAME") {
// Skip non-physical devices (like loop and ram devices)
// TODO: Read more about this, there might be some other devices we should or should not skip
if strings.HasPrefix(deviceName, "loop") || strings.HasPrefix(deviceName, "ram") {
continue
}
// Split the line into columns
cols := strings.Fields(line)
if len(cols) < 6 {
continue
}
// Read device details
model, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/model", deviceName))
serial, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/serial", deviceName))
sizeBytes, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/size", deviceName))
// Filter out nvme drives (M.2)
if cols[1] != "nvme" {
hd := &HardDrive{
Name: cols[0],
Transport: cols[1],
Size: cols[2],
Model: cols[3],
Serial: cols[4],
Type: cols[5],
Temperature: 0,
size := convertSizeToString(sizeBytes)
transport := getTransportType(deviceName)
// TODO: Maybe find a better way?
if size == "0 Bytes" {
// This looks like an invalid device, skip it.
if config.GetConfiguration().DebugMode {
fmt.Printf("[🟨] Igoring device:[/dev/%s], reported size of 0\n", deviceName)
}
hardDrives = append(hardDrives, hd)
continue
}
hwid, err := getHardwareID(deviceName)
if err != nil {
if config.GetConfiguration().DebugMode {
fmt.Printf("[🟨] No unique identifier found for device:[/dev/%s] unique identifier\n", deviceName)
}
continue
}
hd := &HardDrive{
Name: deviceName,
Transport: transport,
Model: strings.TrimSpace(string(model)),
Serial: strings.TrimSpace(string(serial)),
Size: size,
Type: getDriveType(deviceName),
HWID: hwid,
}
systemHardDrives = append(systemHardDrives, hd)
}
var updatedHardDrives []*HardDrive
for _, sysHDD := range systemHardDrives {
var existingHD HardDrive
q := db.Where("hw_id = ?", sysHDD.HWID)
if newerThan != nil && olderThan != nil {
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
}
result := q.First(&existingHD)
if result.Error == gorm.ErrRecordNotFound {
// Hard drive not found, create new
db.Create(&sysHDD)
updatedHardDrives = append(updatedHardDrives, sysHDD)
} else {
// Hard drive found, update existing
existingHD.Name = sysHDD.Name
existingHD.Transport = sysHDD.Transport
existingHD.Size = sysHDD.Size
existingHD.Model = sysHDD.Model
existingHD.Type = sysHDD.Type
db.Save(&existingHD)
updatedHardDrives = append(updatedHardDrives, &existingHD)
}
}
// Handle error
if err := scanner.Err(); err != nil {
return nil, err
return updatedHardDrives, nil
}
func getTransportType(deviceName string) string {
transportLink, err := filepath.EvalSymlinks(fmt.Sprintf("/sys/block/%s/device", deviceName))
if err != nil {
return "Unknown"
}
return hardDrives, nil
if strings.Contains(transportLink, "/usb/") {
return "USB"
} else if strings.Contains(transportLink, "/ata") {
return "SATA"
} else if strings.Contains(transportLink, "/nvme/") {
return "NVMe"
}
return "Other"
}
func convertSizeToString(sizeBytes []byte) string {
// Convert the size from a byte slice to a string, then to an integer
sizeStr := strings.TrimSpace(string(sizeBytes))
sizeSectors, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return "Unknown"
}
// Convert from 512-byte sectors to bytes
sizeInBytes := sizeSectors * 512
// Define size units
const (
_ = iota // ignore first value by assigning to blank identifier
KB float64 = 1 << (10 * iota)
MB
GB
TB
)
var size float64 = float64(sizeInBytes)
var unit string
// Determine the unit to use
switch {
case size >= TB:
size /= TB
unit = "TB"
case size >= GB:
size /= GB
unit = "GB"
case size >= MB:
size /= MB
unit = "MB"
case size >= KB:
size /= KB
unit = "KB"
default:
unit = "Bytes"
}
// Return the formatted size
return fmt.Sprintf("%.2f %s", size, unit)
}
// Look throug /sys/block/device/ and try and find the unique identifier of the device.
func getHardwareID(deviceName string) (string, error) {
// Define potential ID file paths
idFilePaths := []string{
"/sys/block/" + deviceName + "/device/wwid",
"/sys/block/" + deviceName + "/device/wwn",
"/sys/block/" + deviceName + "/device/serial",
}
// Try to read each file and return the first successful read
for _, path := range idFilePaths {
if idBytes, err := os.ReadFile(path); err == nil {
return strings.TrimSpace(string(idBytes)), nil
}
}
// Return an empty string if no ID is found
return "", fmt.Errorf("could not find unique identifier for %s", deviceName)
}
// Figure out what kind of device this is by reading if it's rotational or not
func getDriveType(deviceName string) string {
// Check if the drive is rotational (HDD)
if isRotational, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/queue/rotational", deviceName)); string(isRotational) == "1\n" {
return "HDD"
}
// Check if the drive is NVMe
if strings.HasPrefix(deviceName, "nvme") {
return "NVMe"
}
// Default to SSD for non-rotational and non-NVMe drives
return "SSD"
}

94
lib/hardware/models.go Normal file
View File

@ -0,0 +1,94 @@
package hardware
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
type HardDrive struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Transport string
Size string
Model string
Serial string
Type string
HWID string
Temperatures []HardDriveTemperature `gorm:"foreignKey:HardDriveID"`
}
type HardDriveTemperature struct {
gorm.Model
HardDriveID uint
TimeStamp time.Time
Temperature int
}
// A snapshot in time of the current state of the harddrives
type HardwareSnapshot struct {
TimeStamp time.Time
HDD []*HardDrive
}
type Snapshots struct {
List []*HardwareSnapshot
}
func (h *HardDrive) GetTemperature() int {
possiblePaths := []string{
"/sys/block/" + h.Name + "/device/hwmon/",
"/sys/block/" + h.Name + "/device/",
"/sys/block/" + h.Name + "/device/generic/device/",
}
for _, path := range possiblePaths {
// Try HDD/SSD path
temp, found := h.getTemperatureFromPath(path)
if found {
return temp
}
}
fmt.Printf("[🛑] Failed to get temperature for %s\n", h.Name)
return -1
}
func (h *HardDrive) getTemperatureFromPath(basePath string) (int, bool) {
hwmonDirs, err := os.ReadDir(basePath)
if err != nil {
return 0, false
}
for _, dir := range hwmonDirs {
if strings.HasPrefix(dir.Name(), "hwmon") {
tempPath := filepath.Join(basePath, dir.Name(), "temp1_input")
if _, err := os.Stat(tempPath); err == nil {
tempBytes, err := os.ReadFile(tempPath)
if err != nil {
continue
}
tempStr := strings.TrimSpace(string(tempBytes))
temperature, err := strconv.Atoi(tempStr)
if err != nil {
continue
}
// Convert millidegree Celsius to degree Celsius
return temperature / 1000, true
}
}
}
return 0, false
}

View File

@ -1,46 +0,0 @@
package hardware
import (
"fmt"
"github.com/anatol/smart.go"
)
type HardDrive struct {
Name string
Transport string
Size string
Model string
Serial string
Type string
Temperature int
}
// Fetch the temperature of the device, optinally update the reference object
func (h *HardDrive) GetTemperature(updateTemp bool) int {
// Fetch the device by name
disk, err := smart.Open("/dev/" + h.Name)
if err != nil {
fmt.Printf("Failed to open device %s: %s\n", h.Name, err)
return -1
}
defer disk.Close()
// Fetch SMART data
smartInfo, err := disk.ReadGenericAttributes()
if err != nil {
fmt.Printf("Failed to get SMART data for %s: %s\n", h.Name, err)
return -1
}
// Parse the temperature
temperature := int(smartInfo.Temperature)
// Optionally update the reference object's temperature
if updateTemp {
h.Temperature = temperature
}
// Return the found value
return temperature
}

View File

@ -1,128 +1,186 @@
package svc
import (
"encoding/gob"
"bytes"
"fmt"
"io"
"os"
"time"
"tea.chunkbyte.com/kato/drive-health/lib/config"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
"github.com/JustKato/drive-health/lib/config"
"github.com/JustKato/drive-health/lib/hardware"
"github.com/wcharczuk/go-chart/v2"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// The path to where the snapshot database is located
const SNAPSHOT_LIST_PATH = "./snapshots.dat"
var db *gorm.DB
// A simple in-memory buffer for the history of snapshots
var snapShotBuffer []*HardwareSnapshot
// Initialize the database connection
func InitDB() {
var err error
dbPath := config.GetConfiguration().DatabaseFilePath
// A snapshot in time of the current state of the harddrives
type HardwareSnapshot struct {
TimeStamp time.Time
HDD []*hardware.HardDrive
}
type Snapshots struct {
List []*HardwareSnapshot
}
// The function itterates through all hard disks and takes a snapshot of their state,
// returns a struct which contains metadata as well as the harddrives themselves.
func TakeHardwareSnapshot() (*HardwareSnapshot, error) {
drives, err := hardware.GetSystemHardDrives()
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return nil, err
// This should basically never happen, unless the path to the database
// is inaccessible, doesn't exist or there's no permission to it, which
// should and will crash the program
panic("failed to connect database")
}
snapShot := &HardwareSnapshot{
TimeStamp: time.Now(),
HDD: []*hardware.HardDrive{},
// Migrate the schema
db.AutoMigrate(&hardware.HardDrive{}, &hardware.HardDriveTemperature{})
}
// Fetch the open database pointer
func GetDatabaseRef() *gorm.DB {
return db
}
// Log the temperature of the disks
func LogDriveTemps() error {
drives, err := hardware.GetSystemHardDrives(db, nil, nil)
if err != nil {
return err
}
for _, hdd := range drives {
hdd.GetTemperature(true)
snapShot.HDD = append(snapShot.HDD, hdd)
}
// Append to the in-memory listing
snapShotBuffer = append(snapShotBuffer, snapShot)
// Return the snapshot just in case there is any need to modify it,
// any modification to it will also affect the current buffer from memory.
return snapShot, nil
}
// The function wil check if the `.dat` file is present, if it is then it will load it into memory
func UpdateHardwareSnapshotsFromFile() {
file, err := os.Open(SNAPSHOT_LIST_PATH)
if err != nil {
if os.IsNotExist(err) {
return // File does not exist, no snapshots to load
}
panic(err) // Handle error according to your error handling policy
}
defer file.Close()
decoder := gob.NewDecoder(file)
var snapshots Snapshots
if err := decoder.Decode(&snapshots); err != nil {
if err == io.EOF {
return // End of file reached
}
panic(err) // Handle error according to your error handling policy
}
snapShotBuffer = snapshots.List
fmt.Printf("Loaded %v snapshots from .dat", len(snapShotBuffer))
}
// Get the list of snapshots that have been buffered in memory
func GetHardwareSnapshot() []*HardwareSnapshot {
return snapShotBuffer
}
// Dump the current snapshot history from memory to file
func SaveSnapshotsToFile() error {
file, err := os.Create(SNAPSHOT_LIST_PATH)
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
snapshots := Snapshots{List: snapShotBuffer}
if err := encoder.Encode(snapshots); err != nil {
return err
temp := hdd.GetTemperature()
db.Create(&hardware.HardDriveTemperature{
HardDriveID: hdd.ID,
TimeStamp: time.Now(),
Temperature: temp,
})
}
return nil
}
func RunService() {
// Run the logging service, this will periodically log the temperature of the disks with the LogDriveTemps function
func RunLoggerService() {
fmt.Println("[🦝] Initializing Temperature Logging Service...")
tickTime := time.Duration(config.GetConfiguration().DiskFetchFrequency) * time.Second
// Snapshot taking routine
go func() {
waitTime := time.Duration(config.GetConfiguration().DiskFetchFrequency) * time.Second
for {
time.Sleep(waitTime)
_, err := TakeHardwareSnapshot()
time.Sleep(tickTime)
err := LogDriveTemps()
if err != nil {
fmt.Printf("Hardware Fetch Error: %s", err)
}
}
}()
// Periodic saving routine
go func() {
for {
waitTime := time.Duration(config.GetConfiguration().MemoryDumpFrequency) * time.Second
time.Sleep(waitTime)
err := SaveSnapshotsToFile()
if err != nil {
fmt.Printf("Memory Dump Error: %s", err)
fmt.Printf("[🛑] Temperature logging failed: %s\n", err)
}
}
}()
}
// Generate a PNG based upon a HDD id and a date range
func GetDiskGraphImage(hddID int, newerThan *time.Time, olderThan *time.Time) (*bytes.Buffer, error) {
var hdd hardware.HardDrive
// Fetch by a combination of fields
q := db.Where("id = ?", hddID)
if newerThan == nil || olderThan == nil {
q = q.Preload("Temperatures")
} else {
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
}
// Query for the instance
result := q.First(&hdd)
if result.Error != nil {
return nil, result.Error
}
// Prepare slices for X (time) and Y (temperature) values
var xValues []time.Time
var yValues []float64
for _, temp := range hdd.Temperatures {
xValues = append(xValues, temp.TimeStamp)
yValues = append(yValues, float64(temp.Temperature))
}
// Allocate a buffer for the graph image
graphImageBuffer := bytes.NewBuffer([]byte{})
// TODO: Graph dark theme
// Generate the chart
graph := chart.Chart{
Title: fmt.Sprintf("%s:%s[%s]", hdd.Name, hdd.Serial, hdd.Size),
TitleStyle: chart.Style{
FontSize: 14,
},
// TODO: Implement customizable sizing
Width: 1280,
Background: chart.Style{
Padding: chart.Box{
Top: 20, Right: 20, Bottom: 20, Left: 20,
},
},
XAxis: chart.XAxis{
Name: "Time",
ValueFormatter: func(v interface{}) string {
if ts, isValidTime := v.(float64); isValidTime {
t := time.Unix(int64(ts/1e9), 0)
return t.Format("Jan 2 2006, 15:04")
}
return ""
},
Style: chart.Style{},
GridMajorStyle: chart.Style{
StrokeColor: chart.ColorAlternateGray,
StrokeWidth: 0.5,
},
GridMinorStyle: chart.Style{
StrokeColor: chart.ColorAlternateGray.WithAlpha(64),
StrokeWidth: 0.25,
},
},
YAxis: chart.YAxis{
Name: "Temperature (C)",
Style: chart.Style{},
GridMajorStyle: chart.Style{
StrokeColor: chart.ColorAlternateGray,
StrokeWidth: 0.5,
},
GridMinorStyle: chart.Style{
StrokeColor: chart.ColorAlternateGray.WithAlpha(64),
StrokeWidth: 0.25,
},
},
Series: []chart.Series{
chart.TimeSeries{
Name: "Temperature",
XValues: xValues,
YValues: yValues,
Style: chart.Style{
StrokeColor: chart.ColorCyan,
StrokeWidth: 2.0,
},
},
},
}
// Add a legend to the chart
graph.Elements = []chart.Renderable{
chart.Legend(&graph, chart.Style{
Padding: chart.Box{
Top: 5, Right: 5, Bottom: 5, Left: 5,
},
FontSize: 10,
}),
}
// Render the chart into the byte buffer
err := graph.Render(chart.PNG, graphImageBuffer)
if err != nil {
return nil, err
}
return graphImageBuffer, nil
}

View File

@ -0,0 +1,45 @@
package svc
import (
"fmt"
"time"
"github.com/JustKato/drive-health/lib/config"
"github.com/JustKato/drive-health/lib/hardware"
)
// Delete all thermal entries that are older than X amount of seconds
func CleanupOldData() error {
cfg := config.GetConfiguration()
beforeDate := time.Now().Add(-1 * time.Duration(cfg.MaxHistoryAge) * time.Second)
deleteResult := db.Where("time_stamp < ?", beforeDate).Delete(&hardware.HardDriveTemperature{})
if deleteResult.Error != nil {
fmt.Printf("[🛑] Error during cleanup: %s\n", deleteResult.Error)
return db.Error
}
if deleteResult.RowsAffected > 0 {
fmt.Printf("[🛑] Cleaned up %v entries before %s\n", deleteResult.RowsAffected, beforeDate)
}
return nil
}
func RunCleanupService() {
fmt.Println("[🦝] Initializing Log Cleanup Service...")
tickTime := time.Duration(config.GetConfiguration().CleanupServiceFrequency) * time.Second
// Snapshot taking routine
go func() {
for {
time.Sleep(tickTime)
err := CleanupOldData()
if err != nil {
fmt.Printf("🛑 Cleanup process failed: %s\n", err)
}
}
}()
}

View File

@ -1,28 +1,88 @@
package web
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/JustKato/drive-health/lib/hardware"
"github.com/JustKato/drive-health/lib/svc"
"github.com/gin-gonic/gin"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
"tea.chunkbyte.com/kato/drive-health/lib/svc"
)
func setupApi(r *gin.Engine) {
api := r.Group("/v1/api")
api := r.Group("/api/v1")
// Fetch the chart image for the disk's temperature
api.GET("/disks/:diskid/chart", func(ctx *gin.Context) {
diskIDString := ctx.Param("diskid")
diskId, err := strconv.Atoi(diskIDString)
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"error": err.Error(),
"message": "Invalid Disk ID",
})
return
}
var olderThan, newerThan *time.Time
if ot := ctx.Query("older"); ot != "" {
if otInt, err := strconv.ParseInt(ot, 10, 64); err == nil {
otTime := time.UnixMilli(otInt)
olderThan = &otTime
}
}
if nt := ctx.Query("newer"); nt != "" {
if ntInt, err := strconv.ParseInt(nt, 10, 64); err == nil {
ntTime := time.UnixMilli(ntInt)
newerThan = &ntTime
}
}
graphData, err := svc.GetDiskGraphImage(diskId, newerThan, olderThan)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"error": err.Error(),
"message": "Graph generation issue",
})
return
}
// Set the content type header
ctx.Writer.Header().Set("Content-Type", "image/png")
// Write the image data to the response
ctx.Writer.WriteHeader(http.StatusOK)
_, err = graphData.WriteTo(ctx.Writer)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"error": err.Error(),
"message": "Write error",
})
return
}
})
// Get a list of all the disks
api.GET("/disks", func(ctx *gin.Context) {
olderThan := time.Now().Add(time.Minute * time.Duration(10) * -1)
newerThan := time.Now()
// Fetch the disk list
disks, err := hardware.GetSystemHardDrives()
disks, err := hardware.GetSystemHardDrives(svc.GetDatabaseRef(), &olderThan, &newerThan)
if err != nil {
ctx.Error(err)
}
if ctx.Request.URL.Query().Get("temp") != "" {
for _, d := range disks {
temp := d.GetTemperature(true)
fmt.Printf("Disk Temp: %v", temp)
d.GetTemperature()
}
}
@ -32,10 +92,4 @@ func setupApi(r *gin.Engine) {
})
})
api.GET("/snapshots", func(ctx *gin.Context) {
snapshots := svc.GetHardwareSnapshot()
ctx.JSON(http.StatusOK, snapshots)
})
}

View File

@ -0,0 +1,11 @@
package web
import "github.com/gin-gonic/gin"
func BasicAuthMiddleware(username, password string) gin.HandlerFunc {
authorized := gin.Accounts{
username: password,
}
return gin.BasicAuth(authorized)
}

View File

@ -1,10 +1,14 @@
package web
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/JustKato/drive-health/lib/hardware"
"github.com/JustKato/drive-health/lib/svc"
"github.com/gin-gonic/gin"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
)
func setupFrontend(r *gin.Engine) {
@ -12,20 +16,51 @@ func setupFrontend(r *gin.Engine) {
r.Static("/static", "./static")
// Set up a route for the root URL
r.GET("/", func(c *gin.Context) {
hardDrives, err := hardware.GetSystemHardDrives()
r.GET("/", func(ctx *gin.Context) {
hardDrives, err := hardware.GetSystemHardDrives(svc.GetDatabaseRef(), nil, nil)
if err != nil {
c.AbortWithStatus(500)
ctx.AbortWithStatus(500)
}
for _, hdd := range hardDrives {
hdd.GetTemperature(true)
hdd.GetTemperature()
}
var olderThan, newerThan *time.Time
if ot := ctx.Query("older"); ot != "" {
fmt.Printf("ot = %s\n", ot)
if otInt, err := strconv.ParseInt(ot, 10, 64); err == nil {
otTime := time.UnixMilli(otInt)
olderThan = &otTime
}
}
if nt := ctx.Query("newer"); nt != "" {
fmt.Printf("nt = %s\n", nt)
if ntInt, err := strconv.ParseInt(nt, 10, 64); err == nil {
ntTime := time.UnixMilli(ntInt)
newerThan = &ntTime
}
}
if olderThan == nil {
genTime := time.Now().Add(time.Hour * -1)
olderThan = &genTime
}
if newerThan == nil {
genTime := time.Now()
newerThan = &genTime
}
// Render the HTML template
c.HTML(http.StatusOK, "index.html", gin.H{
ctx.HTML(http.StatusOK, "index.html", gin.H{
"drives": hardDrives,
"older": olderThan.UnixMilli(),
"newer": newerThan.UnixMilli(),
})
})
}

View File

@ -1,13 +1,23 @@
package web
import (
"github.com/JustKato/drive-health/lib/config"
"github.com/gin-gonic/gin"
)
func SetupRouter() *gin.Engine {
cfg := config.GetConfiguration()
if !cfg.DebugMode {
// Set gin to release
gin.SetMode(gin.ReleaseMode)
}
// Initialize the Gin engine
r := gin.Default()
r.Use(BasicAuthMiddleware(cfg.IdentityUsername, cfg.IdentityPassword))
// Setup Health Pings
setupHealth(r)
// Setup Api

30
main.go
View File

@ -9,30 +9,40 @@ import (
"syscall"
"time"
"tea.chunkbyte.com/kato/drive-health/lib/svc"
"tea.chunkbyte.com/kato/drive-health/lib/web"
"github.com/JustKato/drive-health/lib/config"
"github.com/JustKato/drive-health/lib/svc"
"github.com/JustKato/drive-health/lib/web"
"github.com/joho/godotenv"
)
func main() {
// Load existing snapshots from file
svc.UpdateHardwareSnapshotsFromFile()
// Load .env file if it exists
if err := godotenv.Load(); err != nil {
log.Println("[🟨] No .env file found")
}
// Init the database
svc.InitDB()
cfg := config.GetConfiguration()
router := web.SetupRouter()
srv := &http.Server{
Addr: ":8080",
Addr: cfg.Listen,
Handler: router,
}
// Run the server in a goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
log.Fatalf("[🛑] listening failed: %s\n", err)
}
}()
// Run the hardware service
svc.RunService()
svc.RunLoggerService()
// Run the cleanup service
svc.RunCleanupService()
// Setting up signal capturing
quit := make(chan os.Signal, 1)
@ -40,14 +50,14 @@ func main() {
// Block until a signal is received
<-quit
log.Println("Shutting down server...")
log.Println("[🦝] Shutting down server...")
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
log.Fatal("[🛑] Server forced to shutdown:", err)
}
log.Println("Server exiting")
log.Println("[🦝] Server exiting")
}

BIN
media/design_v1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
media/old-look.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1,134 +1,55 @@
function stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xFF;
color += ('00' + value.toString(16)).substr(-2);
}
return color;
/**
* @typedef {number}
*/
var initialInputOlder = 0; // miliseconds Unix TimeStamp
/**
* @typedef {number}
*/
var initialInputNewer = 0; // miliseconds Unix TimeStamp
/**
* @typedef {HTMLInputElement}
*/
var olderThanInputElement;
/**
* @typedef {HTMLInputElement}
*/
var newerThanInputElement;
document.addEventListener(`DOMContentLoaded`, initializePage)
function initializePage() {
// Update the page's time filter
initialInputOlder = Number(document.getElementById(`inp-older`).textContent.trim())
initialInputNewer = Number(document.getElementById(`inp-newer`).textContent.trim())
// Bind the date elements
olderThanInputElement = document.getElementById(`olderThan`);
newerThanInputElement = document.getElementById(`newerThan`);
olderThanInputElement.value = convertTimestampToDateTimeLocal(initialInputOlder);
newerThanInputElement.value = convertTimestampToDateTimeLocal(initialInputNewer);
}
document.addEventListener('DOMContentLoaded', function() {
const diskTableBody = document.getElementById('disk-table-body');
const ctx = document.getElementById('temperatureChart').getContext('2d');
let temperatureChart = new Chart(ctx, {
type: 'line',
data: {
datasets: []
},
options: {
scales: {
x: {
type: 'time',
time: {
unit: 'second',
displayFormats: {
second: 'HH:mm:ss'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Temperature (°C)'
}
}
}
}
});
// Handle one of the date elements having their value changed.
function applyDateInterval() {
const olderTimeStamp = new Date(olderThanInputElement.value).getTime()
const newerTimeStamp = new Date(newerThanInputElement.value).getTime()
function fetchAndUpdateDisks() {
fetch('/v1/api/disks?temp=true')
.then(response => response.json())
.then(data => {
updateDiskTable(data.disks);
})
.catch(error => console.error('Error fetching disk data:', error));
}
window.location.href = `/?older=${olderTimeStamp}&newer=${newerTimeStamp}`;
}
function updateDiskTable(disks) {
let tableHTML = '';
disks.forEach(disk => {
tableHTML += `
<tr>
<td>${disk.Name}</td>
<td>${disk.Transport}</td>
<td>${disk.Size}</td>
<td>${disk.Model}</td>
<td>${disk.Serial}</td>
<td>${disk.Type}</td>
<td>${disk.Temperature}</td>
</tr>
`;
});
diskTableBody.innerHTML = tableHTML;
}
function fetchAndUpdateTemperatureChart() {
fetch('/v1/api/snapshots')
.then(response => response.json())
.then(snapshots => {
updateTemperatureChart(snapshots);
})
.catch(error => console.error('Error fetching temperature data:', error));
}
function updateTemperatureChart(snapshots) {
// Clear existing datasets
temperatureChart.data.datasets = [];
snapshots.forEach(snapshot => {
const time = new Date(snapshot.TimeStamp);
snapshot.HDD.forEach(disk => {
let dataset = temperatureChart.data.datasets.find(d => d.label === disk.Name);
if (!dataset) {
dataset = {
label: disk.Name,
data: [],
fill: false,
borderColor: stringToColor(disk.Name),
borderWidth: 1
};
temperatureChart.data.datasets.push(dataset);
}
dataset.data.push({
x: time,
y: disk.Temperature
});
});
});
temperatureChart.update();
}
// Chart.js zoom and pan configuration
temperatureChart.options.plugins.zoom = {
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
},
pan: {
enabled: true,
mode: 'x',
}
};
fetchAndUpdateDisks();
fetchAndUpdateTemperatureChart();
setInterval(fetchAndUpdateDisks, 5000);
setInterval(fetchAndUpdateTemperatureChart, 5000);
});
/**
* Converts a Unix timestamp to a standard datetime string
* @param {number} timestamp - The Unix timestamp in milliseconds.
* @returns {string} - A normal string with Y-m-d H:i:s format
*/
function convertTimestampToDateTimeLocal(timestamp) {
const date = new Date(timestamp);
const offset = date.getTimezoneOffset() * 60000; // offset in milliseconds
const localDate = new Date(date.getTime() - offset);
return localDate.toISOString().slice(0, 19);
}

View File

@ -1,45 +1,188 @@
body {
font-family: Arial, sans-serif;
background-color: #333;
color: #fff;
margin: 0;
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Roboto:wght@100;300;400&display=swap');
:root {
--bg0: #202327;
--bg1: #282d33;
--bg2: #31373f;
--bg3: #3e4248;
--bg4: #1a1c1f;
--fg0: #bbc0ca;
--fg1: #434c56;
--acc1: #2aa3f4;
--acc1BG0: #2aa3f450;
--acc1BG1: #2aa3f430;
--acc2: #2af488;
--acc2BG0: #2af48850;
--acc2BG1: #2af48830;
--acc3: #f4e02a;
--acc3BG0: #f4e02a50;
--acc3BG1: #f4e02a30;
}
:root {
color: var(--fg0);
font-family: "Noto Sans Mono", "Roboto", sans-serif;
}
html, body {
margin: 0;
padding: 0;
width: 100vw;
overflow: auto;
background-color: var(--bg0);
}
.container {
width: 80%;
margin: auto;
margin: 1rem auto;
max-width: 768px;
border-radius: 8px;
border: 1px solid var(--fg1);
background-color: var(--bg1);
overflow: hidden;
}
h1 {
text-align: center;
padding: 20px 0;
.container-titlebar {
width: 100%;
background-color: var(--bg2);
}
.container .pad {
padding: .5rem 1rem;
}
.container-titlebar h4 {
padding: 0;
margin: 0;
}
.badge {
font-size: 12px;
font-weight: bold;
padding: .1rem .4rem;
display: inline-block;
border-radius: 3px;
color: var(--acc1);
background-color: var(--acc1BG0);
border: 1px solid var(--acc1BG1);
}
.badge[type="HDD"] {
color: var(--acc2);
background-color: var(--acc2BG0);
border: 1px solid var(--acc2BG1);
}
.badge[type="NVMe"] {
color: var(--acc3);
background-color: var(--acc3BG0);
border: 1px solid var(--acc3BG1);
}
.grooved {
background-color: var(--bg0);
border: 1px solid var(--bg1);
font-size: 12px;
padding: .1rem .3rem;
border-radius: 4px;
}
/* Table */
table {
width: 100%;
margin-top: 20px;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #ddd;
table thead tr {
border-bottom: 1px solid var(--bg2);
}
th, td {
text-align: left;
padding: 8px;
.graph-image {
max-width: 100%;
}
th {
background-color: #555;
.disk-graph-entry {
background-color: var(--bg3);
border-radius: 8px;
padding: .3rem .5rem;
}
tr:nth-child(even) {
background-color: #666;
/* Controls */
input {
padding: .25rem .5rem;
font-size: 16px;
background-color: var(--bg3);
color: var(--fg0);
border: 1px solid var(--fg1);
border-radius: 4px;
}
tr:hover {
background-color: #555;
.btn {
font-size: 16px;
padding: .5rem 1rem;
border: 1px solid var(--fg1);
border-radius: 6px;
background-color: var(--bg4);
color: var(--fg0);
cursor: pointer;
}
.btn:hover {
background-color: var(--bg3);
}
.input-grp {
display: flex;
flex-flow: column;
}
.input-grp label {
margin-bottom: .25rem;
font-weight: bold;
}
.graph-controls {
display: flex;
flex-flow: wrap;
width: 100%;
}
.graph-controls input:nth-child(0) {
margin-right: 1rem;
}
.controls-panel {
display: flex;
flex-flow: column;
padding: 1rem 0;
border-bottom: 1px solid var(--fg1);
margin-bottom: 1rem;
}

View File

@ -6,30 +6,115 @@
<link rel="stylesheet" href="/static/style.css">
<title>Drive Health Dashboard</title>
</head>
{{ $older := .older }}
{{ $newer := .newer }}
<body>
<div class="container">
<h1>Drive Health Dashboard</h1>
<table>
<thead>
<!-- ... table headers ... -->
</thead>
<tbody id="disk-table-body">
<!-- Data will be populated here by JavaScript -->
</tbody>
</table>
<div class="container bordered">
<hr>
<div class="chart-container" style="position: relative; height:40vh; width:80vw; overflow-x: scroll;">
<canvas id="temperatureChart"></canvas>
<div class="container-titlebar">
<div class="pad">
<h4>Available Disks</h4>
</div>
</div>
<div class="container-body">
<div class="pad">
{{ if len .drives }}
<table id="disks-table">
<thead>
<tr>
<td>ID</td>
<td>Name</td>
<td>Model</td>
<td>Serial</td>
<td>Temperature</td>
</tr>
</thead>
<tbody id="disk-table-body">
{{ range .drives }}
{{ $temp := .GetTemperature }}
<tr>
<td>#{{ .ID }}</td>
<td> {{ .Name }}</td>
<td> {{ .Model }}</td>
<td> {{ .Serial }}</td>
{{ if gt $temp 50 }} <!-- Temperature greater than 50°C -->
<td style="color: red;">{{ $temp }}&deg;C</td>
{{ else if gt $temp 30 }} <!-- Temperature between 31°C and 50°C -->
<td style="color: orange;">{{ $temp }}&deg;C</td>
{{ else }} <!-- Temperature 30°C or below -->
<td style="color: lime;">{{ $temp }}&deg;C</td>
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
{{ else }}
<p>No hard drives found.</p>
{{ end }}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@latest"></script>
<div class="container bordered">
<div class="container-titlebar">
<div class="pad">
<h4>Temperature Graph</h4>
</div>
</div>
<div class="container-body">
<div class="pad">
<!-- Controls -->
<div class="controls-panel">
<div class="graph-controls">
<span id="inp-older" style="display: none !important" hidden="true">{{ .older }}</span>
<span id="inp-newer" style="display: none !important" hidden="true">{{ .newer }}</span>
<div class="input-grp" style="margin-right: 1rem;">
<label for="olderThan">From Date</label>
<input id="olderThan" type="datetime-local" class="date-change-inp">
</div>
<div class="input-grp">
<label for="newerThan">To Date</label>
<input id="newerThan" type="datetime-local" class="date-change-inp">
</div>
</div>
<div class="btn-group">
<button type="button" class="btn" onclick="applyDateInterval()" style="margin-top: 1rem;">
Filter
</button>
</div>
</div>
<!-- Drives -->
{{ if len .drives }}
{{ range .drives }}
<div class="disk-graph-entry bordered" id="disk-temp-{{ .ID }}" style="position: relative;">
<div class="badge" type="{{.Type}}" style="position: absolute; top: 1rem; right: 1rem;">/dev/{{.Name}}</div>
<h4>{{.Model}}:{{.HWID}} <span class="grooved">{{.Size}}</span></h4>
<a href="/api/v1/disks/{{.ID}}/chart" target="_blank">
<img class="graph-image" src="/api/v1/disks/{{.ID}}/chart?older={{ $older }}&newer={{ $newer }}" alt="{{ .Model }} Image">
</a>
</div>
<br>
{{ end }}
{{ else }}
<p>No hard drives found.</p>
{{ end }}
</div>
</div>
</div>
<script src="/static/main.js"></script>
</body>