mirror of
https://github.com/JustKato/drive-health.git
synced 2026-03-04 08:29:45 +02:00
Compare commits
34 Commits
v0.0.2
...
feature/3-
| Author | SHA1 | Date | |
|---|---|---|---|
| 145362b7ef | |||
| f17d8b44a3 | |||
| de08a7f970 | |||
| d6588742d3 | |||
| 44d4237bec | |||
| f2c84fb6b2 | |||
| 4f50819f92 | |||
| 6117157598 | |||
| 79e44bd88a | |||
| c556e3827b | |||
| 63c92ac272 | |||
| 10bb300087 | |||
| 6d1bb15ea6 | |||
| f039369ec1 | |||
| b4574eb73d | |||
| cdbab95930 | |||
| 1d970aa6ba | |||
| d7e856aca2 | |||
| 39e16ce408 | |||
| 40e02abe87 | |||
| c8fa24f11c | |||
| 2776ad8e52 | |||
| f3905bb822 | |||
| 92baa56a1c | |||
| 4c877e7162 | |||
| cbe252fe94 | |||
| 07dec16aa4 | |||
| 545eed44cd | |||
| eb94ce4552 | |||
| c0f1ed6879 | |||
| c4aef27eda | |||
| 8f8da162e9 | |||
| 5cc58c7d53 | |||
| 21f24899a2 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
|
||||||
|
!main.go
|
||||||
|
!/lib
|
||||||
|
!/templates
|
||||||
|
!/static
|
||||||
|
!/go.mod
|
||||||
|
!/go.sum
|
||||||
27
.env.example
27
.env.example
@@ -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
|
DISK_FETCH_FREQUENCY=5
|
||||||
MEMORY_DUMP_FREQUENCY=60
|
|
||||||
|
# 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
|
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
4
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
snapshots.dat
|
snapshots.dat
|
||||||
.env
|
.env
|
||||||
|
dist
|
||||||
|
data.db
|
||||||
|
data.sqlite
|
||||||
|
dev_data
|
||||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal 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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Build Stage
|
||||||
|
FROM debian:bullseye-slim
|
||||||
|
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
|
||||||
|
|
||||||
|
# Expose the necessary port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Volume for external data
|
||||||
|
VOLUME [ "/data" ]
|
||||||
|
|
||||||
|
# Command to run the executable
|
||||||
|
CMD ["./drive-health"]
|
||||||
13
LICENSE
Normal file
13
LICENSE
Normal 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.
|
||||||
99
README.md
99
README.md
@@ -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.
|

|
||||||
|
|
||||||
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
50
build.sh
Executable 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
65
deploy.sh
Executable 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
14
docker-compose.dev.yml
Normal 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
17
docker-compose.prod.yml
Normal 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
19
go.mod
@@ -1,24 +1,34 @@
|
|||||||
module tea.chunkbyte.com/kato/drive-health
|
module github.com/JustKato/drive-health
|
||||||
|
|
||||||
go 1.21.6
|
go 1.21.6
|
||||||
|
|
||||||
require (
|
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/bytedance/sonic v1.10.2 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // 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/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.17.0 // indirect
|
github.com/go-playground/validator/v10 v10.17.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // 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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 // 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
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
golang.org/x/arch v0.7.0 // indirect
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
golang.org/x/crypto v0.18.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/net v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
|||||||
63
go.sum
63
go.sum
@@ -1,5 +1,5 @@
|
|||||||
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3 h1:kAF2MWFD8tyDqD74OQizymjj2cnZAURwSzBrEslCDnI=
|
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
|
||||||
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3/go.mod h1:llkexGSe52bW0OjNva0kvIqGZxfSnVfpKHrnKBI2+pU=
|
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.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.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
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 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
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.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/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 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
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-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 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
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 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/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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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/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/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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
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 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/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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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/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 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
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/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.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.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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2/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/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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
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 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
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.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 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
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 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
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 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
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/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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
@@ -1,29 +1,46 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DHConfig struct {
|
type DHConfig struct {
|
||||||
DiskFetchFrequency int `json:"diskFetchFrequency"`
|
CleanupServiceFrequency int `json:"cleanupServiceFrequency"`
|
||||||
MemoryDumpFrequency int `json:"memoryDumpFrequency"`
|
DiskFetchFrequency int `json:"diskFetchFrequency"`
|
||||||
MaxHistoryAge int `json:"maxHistoryAge"`
|
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 {
|
var config *DHConfig = nil
|
||||||
// Load .env file if it exists
|
|
||||||
if err := godotenv.Load(); err != nil {
|
func GetConfiguration() *DHConfig {
|
||||||
log.Println("No .env file found")
|
|
||||||
|
if config != nil {
|
||||||
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
config := DHConfig{
|
config = &DHConfig{
|
||||||
DiskFetchFrequency: 5, // default value
|
DiskFetchFrequency: 5,
|
||||||
MemoryDumpFrequency: 60, // default value
|
CleanupServiceFrequency: 3600,
|
||||||
MaxHistoryAge: 2592000, // default value
|
MaxHistoryAge: 2592000,
|
||||||
|
DatabaseFilePath: "./data.sqlite",
|
||||||
|
IdentityUsername: "admin",
|
||||||
|
IdentityPassword: "admin",
|
||||||
|
|
||||||
|
IsDocker: false,
|
||||||
|
|
||||||
|
Listen: ":8080",
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, exists := os.LookupEnv("DISK_FETCH_FREQUENCY"); exists {
|
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 {
|
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
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,198 @@
|
|||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"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
|
// List all block devices
|
||||||
cmd := exec.Command("lsblk", "-d", "-o", "NAME,TRAN,SIZE,MODEL,SERIAL,TYPE")
|
devices, err := os.ReadDir("/sys/block/")
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to execute command:", err)
|
return nil, fmt.Errorf("failed to list block devices: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hardDrives []*HardDrive
|
for _, device := range devices {
|
||||||
|
deviceName := device.Name()
|
||||||
|
|
||||||
// Scan the output line by line
|
// Skip non-physical devices (like loop and ram devices)
|
||||||
scanner := bufio.NewScanner(&out)
|
// TODO: Read more about this, there might be some other devices we should or should not skip
|
||||||
for scanner.Scan() {
|
if strings.HasPrefix(deviceName, "loop") || strings.HasPrefix(deviceName, "ram") {
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
// Skip the header line
|
|
||||||
if strings.Contains(line, "NAME") {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the line into columns
|
// Read device details
|
||||||
cols := strings.Fields(line)
|
model, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/model", deviceName))
|
||||||
if len(cols) < 6 {
|
serial, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/serial", deviceName))
|
||||||
continue
|
sizeBytes, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/size", deviceName))
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out nvme drives (M.2)
|
size := convertSizeToString(sizeBytes)
|
||||||
if cols[1] != "nvme" {
|
transport := getTransportType(deviceName)
|
||||||
hd := &HardDrive{
|
|
||||||
Name: cols[0],
|
// TODO: Maybe find a better way?
|
||||||
Transport: cols[1],
|
if size == "0 Bytes" {
|
||||||
Size: cols[2],
|
// This looks like an invalid device, skip it.
|
||||||
Model: cols[3],
|
if config.GetConfiguration().DebugMode {
|
||||||
Serial: cols[4],
|
fmt.Printf("[🟨] Igoring device:[/dev/%s], reported size of 0\n", deviceName)
|
||||||
Type: cols[5],
|
|
||||||
Temperature: 0,
|
|
||||||
}
|
}
|
||||||
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
|
return updatedHardDrives, nil
|
||||||
if err := scanner.Err(); err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
|
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
94
lib/hardware/models.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,128 +1,186 @@
|
|||||||
package svc
|
package svc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/config"
|
"github.com/JustKato/drive-health/lib/config"
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
|
"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
|
var db *gorm.DB
|
||||||
const SNAPSHOT_LIST_PATH = "./snapshots.dat"
|
|
||||||
|
|
||||||
// A simple in-memory buffer for the history of snapshots
|
// Initialize the database connection
|
||||||
var snapShotBuffer []*HardwareSnapshot
|
func InitDB() {
|
||||||
|
var err error
|
||||||
|
dbPath := config.GetConfiguration().DatabaseFilePath
|
||||||
|
|
||||||
// A snapshot in time of the current state of the harddrives
|
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||||
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()
|
|
||||||
if err != nil {
|
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{
|
// Migrate the schema
|
||||||
TimeStamp: time.Now(),
|
db.AutoMigrate(&hardware.HardDrive{}, &hardware.HardDriveTemperature{})
|
||||||
HDD: []*hardware.HardDrive{},
|
}
|
||||||
|
|
||||||
|
// 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 {
|
for _, hdd := range drives {
|
||||||
hdd.GetTemperature(true)
|
temp := hdd.GetTemperature()
|
||||||
snapShot.HDD = append(snapShot.HDD, hdd)
|
db.Create(&hardware.HardDriveTemperature{
|
||||||
}
|
HardDriveID: hdd.ID,
|
||||||
|
TimeStamp: time.Now(),
|
||||||
// Append to the in-memory listing
|
Temperature: temp,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
// Snapshot taking routine
|
||||||
go func() {
|
go func() {
|
||||||
waitTime := time.Duration(config.GetConfiguration().DiskFetchFrequency) * time.Second
|
|
||||||
for {
|
for {
|
||||||
time.Sleep(waitTime)
|
time.Sleep(tickTime)
|
||||||
_, err := TakeHardwareSnapshot()
|
err := LogDriveTemps()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Hardware Fetch Error: %s", err)
|
fmt.Printf("[🛑] Temperature logging failed: %s\n", 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
45
lib/svc/service_cleanup.go
Normal file
45
lib/svc/service_cleanup.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,28 +1,88 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/JustKato/drive-health/lib/hardware"
|
||||||
|
"github.com/JustKato/drive-health/lib/svc"
|
||||||
"github.com/gin-gonic/gin"
|
"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) {
|
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) {
|
api.GET("/disks", func(ctx *gin.Context) {
|
||||||
|
|
||||||
|
olderThan := time.Now().Add(time.Minute * time.Duration(10) * -1)
|
||||||
|
newerThan := time.Now()
|
||||||
|
|
||||||
// Fetch the disk list
|
// Fetch the disk list
|
||||||
disks, err := hardware.GetSystemHardDrives()
|
disks, err := hardware.GetSystemHardDrives(svc.GetDatabaseRef(), &olderThan, &newerThan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Request.URL.Query().Get("temp") != "" {
|
if ctx.Request.URL.Query().Get("temp") != "" {
|
||||||
for _, d := range disks {
|
for _, d := range disks {
|
||||||
temp := d.GetTemperature(true)
|
d.GetTemperature()
|
||||||
fmt.Printf("Disk Temp: %v", temp)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,10 +92,4 @@ func setupApi(r *gin.Engine) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
api.GET("/snapshots", func(ctx *gin.Context) {
|
|
||||||
snapshots := svc.GetHardwareSnapshot()
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, snapshots)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
lib/web/auth_middleware.go
Normal file
11
lib/web/auth_middleware.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -1,31 +1,81 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/JustKato/drive-health/lib/hardware"
|
||||||
|
"github.com/JustKato/drive-health/lib/svc"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupFrontend(r *gin.Engine) {
|
func setupFrontend(r *gin.Engine) {
|
||||||
r.LoadHTMLGlob("templates/*")
|
r.LoadHTMLGlob("templates/*")
|
||||||
r.Static("/static", "./static")
|
r.Static("/static", "./static")
|
||||||
|
|
||||||
// Set up a route for the root URL
|
r.GET("/disk/:id", func(ctx *gin.Context) {
|
||||||
r.GET("/", func(c *gin.Context) {
|
id := ctx.Param("id")
|
||||||
|
var hdd hardware.HardDrive
|
||||||
hardDrives, err := hardware.GetSystemHardDrives()
|
tx := svc.GetDatabaseRef().Where("id = ?", id).Preload("Temperatures").First(&hdd)
|
||||||
if err != nil {
|
if tx.Error != nil {
|
||||||
c.AbortWithStatus(500)
|
ctx.AbortWithError(500, tx.Error)
|
||||||
}
|
return
|
||||||
|
|
||||||
for _, hdd := range hardDrives {
|
|
||||||
hdd.GetTemperature(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the HTML template
|
// Render the HTML template
|
||||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
ctx.HTML(http.StatusOK, "drive.html", gin.H{
|
||||||
|
"hdd": hdd,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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,
|
"drives": hardDrives,
|
||||||
|
"older": olderThan.UnixMilli(),
|
||||||
|
"newer": newerThan.UnixMilli(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/JustKato/drive-health/lib/config"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRouter() *gin.Engine {
|
func SetupRouter() *gin.Engine {
|
||||||
|
cfg := config.GetConfiguration()
|
||||||
|
|
||||||
|
if !cfg.DebugMode {
|
||||||
|
// Set gin to release
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the Gin engine
|
// Initialize the Gin engine
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.Use(BasicAuthMiddleware(cfg.IdentityUsername, cfg.IdentityPassword))
|
||||||
|
|
||||||
// Setup Health Pings
|
// Setup Health Pings
|
||||||
setupHealth(r)
|
setupHealth(r)
|
||||||
// Setup Api
|
// Setup Api
|
||||||
|
|||||||
30
main.go
30
main.go
@@ -9,30 +9,40 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/svc"
|
"github.com/JustKato/drive-health/lib/config"
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/web"
|
"github.com/JustKato/drive-health/lib/svc"
|
||||||
|
"github.com/JustKato/drive-health/lib/web"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load existing snapshots from file
|
// Load .env file if it exists
|
||||||
svc.UpdateHardwareSnapshotsFromFile()
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Println("[🟨] No .env file found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the database
|
||||||
|
svc.InitDB()
|
||||||
|
cfg := config.GetConfiguration()
|
||||||
|
|
||||||
router := web.SetupRouter()
|
router := web.SetupRouter()
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: ":8080",
|
Addr: cfg.Listen,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the server in a goroutine
|
// Run the server in a goroutine
|
||||||
go func() {
|
go func() {
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
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
|
// Run the hardware service
|
||||||
svc.RunService()
|
svc.RunLoggerService()
|
||||||
|
// Run the cleanup service
|
||||||
|
svc.RunCleanupService()
|
||||||
|
|
||||||
// Setting up signal capturing
|
// Setting up signal capturing
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
@@ -40,14 +50,14 @@ func main() {
|
|||||||
|
|
||||||
// Block until a signal is received
|
// Block until a signal is received
|
||||||
<-quit
|
<-quit
|
||||||
log.Println("Shutting down server...")
|
log.Println("[🦝] Shutting down server...")
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
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
BIN
media/design_v1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
BIN
media/old-look.png
Normal file
BIN
media/old-look.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
181
static/main.js
181
static/main.js
@@ -1,134 +1,55 @@
|
|||||||
function stringToColor(str) {
|
/**
|
||||||
let hash = 0;
|
* @typedef {number}
|
||||||
for (let i = 0; i < str.length; i++) {
|
*/
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
var initialInputOlder = 0; // miliseconds Unix TimeStamp
|
||||||
}
|
/**
|
||||||
let color = '#';
|
* @typedef {number}
|
||||||
for (let i = 0; i < 3; i++) {
|
*/
|
||||||
const value = (hash >> (i * 8)) & 0xFF;
|
var initialInputNewer = 0; // miliseconds Unix TimeStamp
|
||||||
color += ('00' + value.toString(16)).substr(-2);
|
|
||||||
}
|
|
||||||
return color;
|
/**
|
||||||
|
* @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() {
|
// Handle one of the date elements having their value changed.
|
||||||
const diskTableBody = document.getElementById('disk-table-body');
|
function applyDateInterval() {
|
||||||
const ctx = document.getElementById('temperatureChart').getContext('2d');
|
const olderTimeStamp = new Date(olderThanInputElement.value).getTime()
|
||||||
let temperatureChart = new Chart(ctx, {
|
const newerTimeStamp = new Date(newerThanInputElement.value).getTime()
|
||||||
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)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function fetchAndUpdateDisks() {
|
window.location.href = `/?older=${olderTimeStamp}&newer=${newerTimeStamp}`;
|
||||||
fetch('/v1/api/disks?temp=true')
|
}
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
updateDiskTable(data.disks);
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error fetching disk data:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDiskTable(disks) {
|
/**
|
||||||
let tableHTML = '';
|
* Converts a Unix timestamp to a standard datetime string
|
||||||
disks.forEach(disk => {
|
* @param {number} timestamp - The Unix timestamp in milliseconds.
|
||||||
tableHTML += `
|
* @returns {string} - A normal string with Y-m-d H:i:s format
|
||||||
<tr>
|
*/
|
||||||
<td>${disk.Name}</td>
|
function convertTimestampToDateTimeLocal(timestamp) {
|
||||||
<td>${disk.Transport}</td>
|
const date = new Date(timestamp);
|
||||||
<td>${disk.Size}</td>
|
const offset = date.getTimezoneOffset() * 60000; // offset in milliseconds
|
||||||
<td>${disk.Model}</td>
|
const localDate = new Date(date.getTime() - offset);
|
||||||
<td>${disk.Serial}</td>
|
return localDate.toISOString().slice(0, 19);
|
||||||
<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);
|
|
||||||
});
|
|
||||||
|
|||||||
227
static/style.css
227
static/style.css
@@ -1,45 +1,228 @@
|
|||||||
body {
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Roboto:wght@100;300;400&display=swap');
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #333;
|
:root {
|
||||||
color: #fff;
|
--bg0: #202327;
|
||||||
margin: 0;
|
--bg1: #282d33;
|
||||||
|
--bg2: #31373f;
|
||||||
|
--bg3: #3e4248;
|
||||||
|
--bg4: #1a1c1f;
|
||||||
|
|
||||||
|
--fg0: #bbc0ca;
|
||||||
|
--fg1: #434c56;
|
||||||
|
|
||||||
|
--acc1: #2aa3f4;
|
||||||
|
--acc1H: #0089e5;
|
||||||
|
--acc1BG0: #2aa3f450;
|
||||||
|
--acc1BG1: #2aa3f430;
|
||||||
|
|
||||||
|
--acc2: #2af488;
|
||||||
|
--acc2H: #2af488;
|
||||||
|
--acc2BG0: #2af48850;
|
||||||
|
--acc2BG1: #2af48830;
|
||||||
|
|
||||||
|
--acc3: #f4e02a;
|
||||||
|
--acc3H: #f4e02a;
|
||||||
|
--acc3BG0: #f4e02a50;
|
||||||
|
--acc3BG1: #f4e02a30;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color: var(--fg0);
|
||||||
|
font-family: "Noto Sans Mono", "Roboto", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
background-color: var(--bg0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 80%;
|
margin: 1rem auto;
|
||||||
margin: auto;
|
max-width: 768px;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--fg1);
|
||||||
|
|
||||||
|
background-color: var(--bg1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.container-titlebar {
|
||||||
text-align: center;
|
width: 100%;
|
||||||
padding: 20px 0;
|
|
||||||
|
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 {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 20px;
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
table, th, td {
|
table thead tr {
|
||||||
border: 1px solid #ddd;
|
border-bottom: 1px solid var(--bg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
.graph-image {
|
||||||
text-align: left;
|
max-width: 100%;
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
.disk-graph-entry {
|
||||||
background-color: #555;
|
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 {
|
.btn {
|
||||||
background-color: #555;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
color: var(--acc1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:hover {
|
||||||
|
color: var(--acc1H);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-button {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
mask-image: url("/static/view-svgrepo-com.svg");
|
||||||
|
background: var(--fg0);
|
||||||
|
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-button:hover {
|
||||||
|
mask-image: url("/static/view-alt-svgrepo-com.svg");
|
||||||
|
background-color: var(--acc1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
}
|
}
|
||||||
5
static/view-alt-svgrepo-com.svg
Normal file
5
static/view-alt-svgrepo-com.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 4H17.2C18.9913 4 19.887 4 20.4435 4.5565C21 5.11299 21 6.00866 21 7.8V8M17 20H17.2C18.9913 20 19.887 20 20.4435 19.4435C21 18.887 21 17.9913 21 16.2V16M7 4H6.8C5.00866 4 4.11299 4 3.5565 4.5565C3 5.11299 3 6.00866 3 7.8V8M7 20H6.8C5.00866 20 4.11299 20 3.5565 19.4435C3 18.887 3 17.9913 3 16.2V16" stroke="#33363F" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8149 12C18.8149 11.4637 18.6892 11.2462 18.4379 10.8112C17.5834 9.33247 15.6561 7 12 7C8.34395 7 6.41664 9.33247 5.56212 10.8112C5.31077 11.2462 5.18509 11.4637 5.18509 12C5.18509 12.5363 5.31077 12.7538 5.56212 13.1888C6.41664 14.6675 8.34395 17 12 17C15.6561 17 17.5834 14.6675 18.4379 13.1888C18.6892 12.7538 18.8149 12.5363 18.8149 12ZM12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3432 9 9.00001 10.3431 9.00001 12C9.00001 13.6569 10.3432 15 12 15Z" fill="#33363F"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
4
static/view-svgrepo-com.svg
Normal file
4
static/view-svgrepo-com.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7703 12C20.7703 11.6412 20.5762 11.4056 20.188 10.9343C18.768 9.21014 15.6357 6 12 6C8.36428 6 5.23207 9.21014 3.81198 10.9343C3.42382 11.4056 3.22974 11.6412 3.22974 12C3.22974 12.3588 3.42382 12.5944 3.81198 13.0657C5.23207 14.7899 8.36428 18 12 18C15.6357 18 18.768 14.7899 20.188 13.0657C20.5762 12.5944 20.7703 12.3588 20.7703 12ZM12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3432 9 9.00002 10.3431 9.00002 12C9.00002 13.6569 10.3432 15 12 15Z" fill="#33363F"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 767 B |
46
templates/drive.html
Normal file
46
templates/drive.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<!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>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container bordered">
|
||||||
|
|
||||||
|
<div class="container-titlebar">
|
||||||
|
<div class="pad">
|
||||||
|
<h4>{{ .hdd.Model }} <span class="grooved">{{ .hdd.Size }}</span></h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-body">
|
||||||
|
<div class="pad">
|
||||||
|
<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">
|
||||||
|
<tr>
|
||||||
|
<td>#{{ .hdd.ID }}</td>
|
||||||
|
<td> {{ .hdd.Name }}</td>
|
||||||
|
<td> {{ .hdd.Model }}</td>
|
||||||
|
<td> {{ .hdd.Serial }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -6,30 +6,119 @@
|
|||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<title>Drive Health Dashboard</title>
|
<title>Drive Health Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
{{ $older := .older }}
|
||||||
|
{{ $newer := .newer }}
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container bordered">
|
||||||
<h1>Drive Health Dashboard</h1>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<!-- ... table headers ... -->
|
|
||||||
</thead>
|
|
||||||
<tbody id="disk-table-body">
|
|
||||||
<!-- Data will be populated here by JavaScript -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<hr>
|
<div class="container-titlebar">
|
||||||
|
<div class="pad">
|
||||||
<div class="chart-container" style="position: relative; height:40vh; width:80vw; overflow-x: scroll;">
|
<h4>Available Disks</h4>
|
||||||
<canvas id="temperatureChart"></canvas>
|
</div>
|
||||||
</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>
|
||||||
|
<td>Actions</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 }}°C</td>
|
||||||
|
{{ else if gt $temp 30 }} <!-- Temperature between 31°C and 50°C -->
|
||||||
|
<td style="color: orange;">{{ $temp }}°C</td>
|
||||||
|
{{ else }} <!-- Temperature 30°C or below -->
|
||||||
|
<td style="color: lime;">{{ $temp }}°C</td>
|
||||||
|
{{ end }}
|
||||||
|
<td>
|
||||||
|
<a title="View Disk" class="info-button" href="/disk/{{ .ID }}"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{ else }}
|
||||||
|
<p>No hard drives found.</p>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@latest"></script>
|
<div class="container bordered">
|
||||||
<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-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>
|
<script src="/static/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user