Compare commits

..

41 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
Daniel Legt 498aba835f Messy JS + config from .env 2024-01-20 02:06:27 +02:00
Daniel Legt 71d4eb6cc3 Api and Frontend
+ Snapshot get API
+ Frontend Starting
2024-01-20 01:40:55 +02:00
Daniel Legt 4352c398c1 Fixes 2024-01-20 01:29:57 +02:00
Daniel Legt 81ca5ddf28 Implemented the hardware service
+ Started working on web interface
2024-01-20 01:27:10 +02:00
30 changed files with 1785 additions and 102 deletions

8
.dockerignore Normal file
View File

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

28
.env.example Normal file
View File

@ -0,0 +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
# 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

6
.gitignore vendored Normal file
View File

@ -0,0 +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,4 +1,98 @@
## About
## 📖 About
Drive Health is a program written in golang to help with tracking and monitoring of your hardware's temperature.
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...
### Features
- Disk Listing
- Temperature Graphing
- Disk activity logging
- [API](./lib/web/api.go)
![UI Example](./media/design_v1.webp)
## ❗ 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!
## ❗ 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
## 📖 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

43
go.mod
View File

@ -1,8 +1,45 @@
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
golang.org/x/sys v0.9.0 // 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/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/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
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
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
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

145
go.sum
View File

@ -1,4 +1,141 @@
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=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
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=
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=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
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/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=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
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=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
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=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.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/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=

95
lib/config/config.go Normal file
View File

@ -0,0 +1,95 @@
package config
import (
"os"
"strconv"
)
type DHConfig struct {
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"`
}
var config *DHConfig = nil
func GetConfiguration() *DHConfig {
if config != nil {
return config
}
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 {
if intValue, err := strconv.Atoi(val); err == nil {
config.DiskFetchFrequency = intValue
}
}
if val, exists := os.LookupEnv("CLEANUP_SERVICE_FREQUENCY"); exists {
if intValue, err := strconv.Atoi(val); err == nil {
config.CleanupServiceFrequency = intValue
}
}
if val, exists := os.LookupEnv("MAX_HISTORY_AGE"); exists {
if intValue, err := strconv.Atoi(val); err == nil {
config.MaxHistoryAge = intValue
}
}
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
}

186
lib/svc/service.go Normal file
View File

@ -0,0 +1,186 @@
package svc
import (
"bytes"
"fmt"
"time"
"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"
)
var db *gorm.DB
// Initialize the database connection
func InitDB() {
var err error
dbPath := config.GetConfiguration().DatabaseFilePath
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
// 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")
}
// 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 {
temp := hdd.GetTemperature()
db.Create(&hardware.HardDriveTemperature{
HardDriveID: hdd.ID,
TimeStamp: time.Now(),
Temperature: temp,
})
}
return nil
}
// 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() {
for {
time.Sleep(tickTime)
err := LogDriveTemps()
if err != nil {
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)
}
}
}()
}

95
lib/web/api.go Normal file
View File

@ -0,0 +1,95 @@
package web
import (
"net/http"
"strconv"
"time"
"github.com/JustKato/drive-health/lib/hardware"
"github.com/JustKato/drive-health/lib/svc"
"github.com/gin-gonic/gin"
)
func setupApi(r *gin.Engine) {
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(svc.GetDatabaseRef(), &olderThan, &newerThan)
if err != nil {
ctx.Error(err)
}
if ctx.Request.URL.Query().Get("temp") != "" {
for _, d := range disks {
d.GetTemperature()
}
}
ctx.JSON(http.StatusOK, gin.H{
"message": "Disk List",
"disks": disks,
})
})
}

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)
}

66
lib/web/frontend.go Normal file
View File

@ -0,0 +1,66 @@
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"
)
func setupFrontend(r *gin.Engine) {
r.LoadHTMLGlob("templates/*")
r.Static("/static", "./static")
// Set up a route for the root URL
r.GET("/", func(ctx *gin.Context) {
hardDrives, err := hardware.GetSystemHardDrives(svc.GetDatabaseRef(), nil, nil)
if err != nil {
ctx.AbortWithStatus(500)
}
for _, hdd := range hardDrives {
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
ctx.HTML(http.StatusOK, "index.html", gin.H{
"drives": hardDrives,
"older": olderThan.UnixMilli(),
"newer": newerThan.UnixMilli(),
})
})
}

16
lib/web/health.go Normal file
View File

@ -0,0 +1,16 @@
package web
import (
"net/http"
"github.com/gin-gonic/gin"
)
func setupHealth(r *gin.Engine) {
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "Pong",
})
})
}

29
lib/web/net.go Normal file
View File

@ -0,0 +1,29 @@
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
setupApi(r)
// Setup Frontend
setupFrontend(r)
return r
}

59
main.go
View File

@ -1,20 +1,63 @@
package main
import (
"fmt"
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
"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() {
hardDrives, err := hardware.GetSystemHardDrives()
if err != nil {
panic(err)
// Load .env file if it exists
if err := godotenv.Load(); err != nil {
log.Println("[🟨] No .env file found")
}
for _, hdd := range hardDrives {
fmt.Printf("%s %s [%s]: %vC\n", hdd.Model, hdd.Serial, hdd.Size, hdd.GetTemperature(true))
// Init the database
svc.InitDB()
cfg := config.GetConfiguration()
router := web.SetupRouter()
srv := &http.Server{
Addr: cfg.Listen,
Handler: router,
}
// Run the server in a goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("[🛑] listening failed: %s\n", err)
}
}()
// Run the hardware service
svc.RunLoggerService()
// Run the cleanup service
svc.RunCleanupService()
// Setting up signal capturing
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Block until a signal is received
<-quit
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.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

55
static/main.js Normal file
View File

@ -0,0 +1,55 @@
/**
* @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);
}
// 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()
window.location.href = `/?older=${olderTimeStamp}&newer=${newerTimeStamp}`;
}
/**
* 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);
}

188
static/style.css Normal file
View File

@ -0,0 +1,188 @@
@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 {
margin: 1rem auto;
max-width: 768px;
border-radius: 8px;
border: 1px solid var(--fg1);
background-color: var(--bg1);
overflow: hidden;
}
.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%;
border-collapse: collapse;
}
table thead tr {
border-bottom: 1px solid var(--bg2);
}
.graph-image {
max-width: 100%;
}
.disk-graph-entry {
background-color: var(--bg3);
border-radius: 8px;
padding: .3rem .5rem;
}
/* Controls */
input {
padding: .25rem .5rem;
font-size: 16px;
background-color: var(--bg3);
color: var(--fg0);
border: 1px solid var(--fg1);
border-radius: 4px;
}
.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;
}

121
templates/index.html Normal file
View File

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
<title>Drive Health Dashboard</title>
</head>
{{ $older := .older }}
{{ $newer := .newer }}
<body>
<div class="container bordered">
<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>
<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>
</html>