mirror of
https://github.com/JustKato/drive-health.git
synced 2026-03-04 16:39:45 +02:00
Compare commits
23 Commits
v0.1.1
...
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 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
|
||||||
|
!main.go
|
||||||
|
!/lib
|
||||||
|
!/templates
|
||||||
|
!/static
|
||||||
|
!/go.mod
|
||||||
|
!/go.sum
|
||||||
16
.env.example
16
.env.example
@@ -1,7 +1,20 @@
|
|||||||
|
#
|
||||||
|
# All time references are in seconds
|
||||||
|
#
|
||||||
|
#
|
||||||
|
############################################################
|
||||||
|
|
||||||
# The frequency at which to fetch the temperature of ALL disks and add it to the database
|
# 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
|
||||||
|
|
||||||
|
# How ofthen should the program clean the database of old logs
|
||||||
|
CLEANUP_SERVICE_FREQUENCY=3600
|
||||||
|
|
||||||
# The maximum age of logs in seconds
|
# 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
|
# The ip:port to listen to for the application
|
||||||
@@ -10,3 +23,6 @@ LISTEN=":8080"
|
|||||||
# Basic Security, these are required to view the data
|
# Basic Security, these are required to view the data
|
||||||
IDENTITY_USERNAME=admin
|
IDENTITY_USERNAME=admin
|
||||||
IDENTITY_PASSWORD=admin
|
IDENTITY_PASSWORD=admin
|
||||||
|
|
||||||
|
# Enable/Disable debug features
|
||||||
|
DEBUG_MODE=false
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ snapshots.dat
|
|||||||
dist
|
dist
|
||||||
data.db
|
data.db
|
||||||
data.sqlite
|
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).
|
||||||
49
build.sh
49
build.sh
@@ -1,17 +1,50 @@
|
|||||||
#!/bin/bash
|
#!/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"
|
APP_NAME="drive-health"
|
||||||
DIST_DIR="dist"
|
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
|
# Create the dist directory if it doesn't exist
|
||||||
mkdir -p $DIST_DIR
|
mkdir -p $DIST_DIR
|
||||||
|
|
||||||
# Build the application
|
# Build the application
|
||||||
echo "Building the application..."
|
echo_color yellow "[🦝] Building the application..."
|
||||||
GOOS=linux GOARCH=amd64 go build -o $DIST_DIR/$APP_NAME
|
GOOS=linux CGO_ENABLED=1 GOARCH=amd64 go build -o $DIST_DIR/$APP_NAME
|
||||||
|
|
||||||
# Copy additional resources (like .env, static files, templates) to the dist directory
|
# Copying additional resources...
|
||||||
echo "Copying additional resources..."
|
cp -r static templates $DIST_DIR/
|
||||||
cp -r .env static templates $DIST_DIR/
|
|
||||||
|
|
||||||
echo "Compilation and packaging completed."
|
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
|
||||||
17
go.mod
17
go.mod
@@ -1,15 +1,22 @@
|
|||||||
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
|
||||||
@@ -17,7 +24,6 @@ require (
|
|||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // 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
|
||||||
@@ -28,7 +34,6 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.1 // 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/image v0.11.0 // indirect
|
||||||
@@ -37,6 +42,4 @@ require (
|
|||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
google.golang.org/protobuf v1.32.0 // indirect
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gorm.io/driver/sqlite v1.5.4 // indirect
|
|
||||||
gorm.io/gorm v1.25.5 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
17
go.sum
17
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=
|
||||||
@@ -29,7 +32,7 @@ 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/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -57,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=
|
||||||
@@ -67,6 +71,7 @@ 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=
|
||||||
@@ -102,8 +107,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
@@ -121,9 +124,11 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
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/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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=
|
||||||
|
|||||||
@@ -1,35 +1,45 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DHConfig struct {
|
type DHConfig struct {
|
||||||
|
CleanupServiceFrequency int `json:"cleanupServiceFrequency"`
|
||||||
DiskFetchFrequency int `json:"diskFetchFrequency"`
|
DiskFetchFrequency int `json:"diskFetchFrequency"`
|
||||||
MaxHistoryAge int `json:"maxHistoryAge"`
|
MaxHistoryAge int `json:"maxHistoryAge"`
|
||||||
DatabaseFilePath string
|
|
||||||
Listen string
|
DatabaseFilePath string `json:"databaseFilePath"`
|
||||||
IdentityUsername string
|
|
||||||
IdentityPassword string
|
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,
|
||||||
MaxHistoryAge: 2592000, // default value
|
CleanupServiceFrequency: 3600,
|
||||||
|
MaxHistoryAge: 2592000,
|
||||||
DatabaseFilePath: "./data.sqlite",
|
DatabaseFilePath: "./data.sqlite",
|
||||||
IdentityUsername: "admin",
|
IdentityUsername: "admin",
|
||||||
IdentityPassword: "admin",
|
IdentityPassword: "admin",
|
||||||
|
|
||||||
|
IsDocker: false,
|
||||||
|
|
||||||
Listen: ":8080",
|
Listen: ":8080",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +49,12 @@ func GetConfiguration() DHConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val, exists := os.LookupEnv("CLEANUP_SERVICE_FREQUENCY"); exists {
|
||||||
|
if intValue, err := strconv.Atoi(val); err == nil {
|
||||||
|
config.CleanupServiceFrequency = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if val, exists := os.LookupEnv("MAX_HISTORY_AGE"); exists {
|
if val, exists := os.LookupEnv("MAX_HISTORY_AGE"); exists {
|
||||||
if intValue, err := strconv.Atoi(val); err == nil {
|
if intValue, err := strconv.Atoi(val); err == nil {
|
||||||
config.MaxHistoryAge = intValue
|
config.MaxHistoryAge = intValue
|
||||||
@@ -61,5 +77,19 @@ func GetConfiguration() DHConfig {
|
|||||||
config.IdentityPassword = val
|
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,70 +1,78 @@
|
|||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/JustKato/drive-health/lib/config"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time) ([]*HardDrive, error) {
|
func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time) ([]*HardDrive, error) {
|
||||||
|
|
||||||
// Execute the lsblk command to get detailed block device information
|
|
||||||
cmd := exec.Command("lsblk", "-d", "-o", "NAME,TRAN,SIZE,MODEL,SERIAL,TYPE")
|
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to execute command:", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemHardDrives []*HardDrive
|
var systemHardDrives []*HardDrive
|
||||||
|
|
||||||
// Scan the output line by line
|
// List all block devices
|
||||||
scanner := bufio.NewScanner(&out)
|
devices, err := os.ReadDir("/sys/block/")
|
||||||
for scanner.Scan() {
|
if err != nil {
|
||||||
line := scanner.Text()
|
return nil, fmt.Errorf("failed to list block devices: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Skip the header line
|
for _, device := range devices {
|
||||||
if strings.Contains(line, "NAME") {
|
deviceName := device.Name()
|
||||||
|
|
||||||
|
// Skip non-physical devices (like loop and ram devices)
|
||||||
|
// TODO: Read more about this, there might be some other devices we should or should not skip
|
||||||
|
if strings.HasPrefix(deviceName, "loop") || strings.HasPrefix(deviceName, "ram") {
|
||||||
continue
|
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))
|
||||||
|
sizeBytes, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/size", deviceName))
|
||||||
|
|
||||||
|
size := convertSizeToString(sizeBytes)
|
||||||
|
transport := getTransportType(deviceName)
|
||||||
|
|
||||||
|
// TODO: Maybe find a better way?
|
||||||
|
if size == "0 Bytes" {
|
||||||
|
// This looks like an invalid device, skip it.
|
||||||
|
if config.GetConfiguration().DebugMode {
|
||||||
|
fmt.Printf("[🟨] Igoring device:[/dev/%s], reported size of 0\n", deviceName)
|
||||||
|
}
|
||||||
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out nvme drives (M.2)
|
|
||||||
if cols[1] != "usb" {
|
|
||||||
hd := &HardDrive{
|
hd := &HardDrive{
|
||||||
Name: cols[0],
|
Name: deviceName,
|
||||||
Transport: cols[1],
|
Transport: transport,
|
||||||
Size: cols[2],
|
Model: strings.TrimSpace(string(model)),
|
||||||
Model: cols[3],
|
Serial: strings.TrimSpace(string(serial)),
|
||||||
Serial: cols[4],
|
Size: size,
|
||||||
Type: cols[5],
|
Type: getDriveType(deviceName),
|
||||||
}
|
HWID: hwid,
|
||||||
systemHardDrives = append(systemHardDrives, hd)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle error
|
systemHardDrives = append(systemHardDrives, hd)
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedHardDrives []*HardDrive
|
var updatedHardDrives []*HardDrive
|
||||||
|
|
||||||
for _, sysHDD := range systemHardDrives {
|
for _, sysHDD := range systemHardDrives {
|
||||||
var existingHD HardDrive
|
var existingHD HardDrive
|
||||||
q := db.Where("serial = ? AND model = ? AND type = ?", sysHDD.Serial, sysHDD.Model, sysHDD.Type)
|
q := db.Where("hw_id = ?", sysHDD.HWID)
|
||||||
|
|
||||||
if newerThan != nil && olderThan != nil {
|
if newerThan != nil && olderThan != nil {
|
||||||
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
|
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
|
||||||
@@ -90,3 +98,101 @@ func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time
|
|||||||
|
|
||||||
return updatedHardDrives, nil
|
return updatedHardDrives, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTransportType(deviceName string) string {
|
||||||
|
transportLink, err := filepath.EvalSymlinks(fmt.Sprintf("/sys/block/%s/device", deviceName))
|
||||||
|
if err != nil {
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package hardware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/anatol/smart.go"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ type HardDrive struct {
|
|||||||
Model string
|
Model string
|
||||||
Serial string
|
Serial string
|
||||||
Type string
|
Type string
|
||||||
|
HWID string
|
||||||
Temperatures []HardDriveTemperature `gorm:"foreignKey:HardDriveID"`
|
Temperatures []HardDriveTemperature `gorm:"foreignKey:HardDriveID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,26 +43,52 @@ type Snapshots struct {
|
|||||||
List []*HardwareSnapshot
|
List []*HardwareSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the temperature of the device, optinally update the reference object
|
|
||||||
func (h *HardDrive) GetTemperature() int {
|
func (h *HardDrive) GetTemperature() 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
|
possiblePaths := []string{
|
||||||
smartInfo, err := disk.ReadGenericAttributes()
|
"/sys/block/" + h.Name + "/device/hwmon/",
|
||||||
if err != nil {
|
"/sys/block/" + h.Name + "/device/",
|
||||||
fmt.Printf("Failed to get SMART data for %s: %s\n", h.Name, err)
|
"/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
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the temperature
|
func (h *HardDrive) getTemperatureFromPath(basePath string) (int, bool) {
|
||||||
temperature := int(smartInfo.Temperature)
|
hwmonDirs, err := os.ReadDir(basePath)
|
||||||
|
if err != nil {
|
||||||
// Return the found value
|
return 0, false
|
||||||
return temperature
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,25 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/JustKato/drive-health/lib/config"
|
||||||
|
"github.com/JustKato/drive-health/lib/hardware"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/config"
|
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
|
|
||||||
|
// Initialize the database connection
|
||||||
func InitDB() {
|
func InitDB() {
|
||||||
var err error
|
var err error
|
||||||
dbPath := config.GetConfiguration().DatabaseFilePath
|
dbPath := config.GetConfiguration().DatabaseFilePath
|
||||||
if dbPath == "" {
|
|
||||||
dbPath = "./data.sqlite"
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// This should basically never happen, unless the path to the database
|
||||||
|
// is inaccessible, doesn't exist or there's no permission to it, which
|
||||||
|
// should and will crash the program
|
||||||
panic("failed to connect database")
|
panic("failed to connect database")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +31,12 @@ func InitDB() {
|
|||||||
db.AutoMigrate(&hardware.HardDrive{}, &hardware.HardDriveTemperature{})
|
db.AutoMigrate(&hardware.HardDrive{}, &hardware.HardDriveTemperature{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the open database pointer
|
||||||
func GetDatabaseRef() *gorm.DB {
|
func GetDatabaseRef() *gorm.DB {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the temperature of the disks
|
||||||
func LogDriveTemps() error {
|
func LogDriveTemps() error {
|
||||||
drives, err := hardware.GetSystemHardDrives(db, nil, nil)
|
drives, err := hardware.GetSystemHardDrives(db, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -52,8 +55,9 @@ func LogDriveTemps() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run the logging service, this will periodically log the temperature of the disks with the LogDriveTemps function
|
||||||
func RunLoggerService() {
|
func RunLoggerService() {
|
||||||
fmt.Println("Initializing Temperature Logging Service...")
|
fmt.Println("[🦝] Initializing Temperature Logging Service...")
|
||||||
|
|
||||||
tickTime := time.Duration(config.GetConfiguration().DiskFetchFrequency) * time.Second
|
tickTime := time.Duration(config.GetConfiguration().DiskFetchFrequency) * time.Second
|
||||||
|
|
||||||
@@ -63,18 +67,19 @@ func RunLoggerService() {
|
|||||||
time.Sleep(tickTime)
|
time.Sleep(tickTime)
|
||||||
err := LogDriveTemps()
|
err := LogDriveTemps()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("🛑 Temperature logging failed: %s\n", err)
|
fmt.Printf("[🛑] Temperature logging failed: %s\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a PNG based upon a HDD id and a date range
|
||||||
func GetDiskGraphImage(hddID int, newerThan *time.Time, olderThan *time.Time) (*bytes.Buffer, error) {
|
func GetDiskGraphImage(hddID int, newerThan *time.Time, olderThan *time.Time) (*bytes.Buffer, error) {
|
||||||
var hdd hardware.HardDrive
|
var hdd hardware.HardDrive
|
||||||
// Fetch by a combination of fields
|
// Fetch by a combination of fields
|
||||||
q := db.Where("id = ?", hddID)
|
q := db.Where("id = ?", hddID)
|
||||||
|
|
||||||
if newerThan == nil && olderThan == nil {
|
if newerThan == nil || olderThan == nil {
|
||||||
q = q.Preload("Temperatures")
|
q = q.Preload("Temperatures")
|
||||||
} else {
|
} else {
|
||||||
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
|
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -5,14 +5,15 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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("/api/v1")
|
api := r.Group("/api/v1")
|
||||||
|
|
||||||
|
// Fetch the chart image for the disk's temperature
|
||||||
api.GET("/disks/:diskid/chart", func(ctx *gin.Context) {
|
api.GET("/disks/:diskid/chart", func(ctx *gin.Context) {
|
||||||
diskIDString := ctx.Param("diskid")
|
diskIDString := ctx.Param("diskid")
|
||||||
diskId, err := strconv.Atoi(diskIDString)
|
diskId, err := strconv.Atoi(diskIDString)
|
||||||
@@ -25,7 +26,23 @@ func setupApi(r *gin.Engine) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
graphData, err := svc.GetDiskGraphImage(diskId, nil, nil)
|
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 {
|
if err != nil {
|
||||||
ctx.AbortWithStatusJSON(500, gin.H{
|
ctx.AbortWithStatusJSON(500, gin.H{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -51,6 +68,7 @@ func setupApi(r *gin.Engine) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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)
|
olderThan := time.Now().Add(time.Minute * time.Duration(10) * -1)
|
||||||
|
|||||||
@@ -1,35 +1,81 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"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 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")
|
||||||
olderThan := time.Now().Add(time.Minute * time.Duration(10) * -1)
|
var hdd hardware.HardDrive
|
||||||
newerThan := time.Now()
|
tx := svc.GetDatabaseRef().Where("id = ?", id).Preload("Temperatures").First(&hdd)
|
||||||
|
if tx.Error != nil {
|
||||||
|
ctx.AbortWithError(500, tx.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
hardDrives, err := hardware.GetSystemHardDrives(svc.GetDatabaseRef(), &olderThan, &newerThan)
|
// Render the HTML template
|
||||||
|
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 {
|
if err != nil {
|
||||||
c.AbortWithStatus(500)
|
ctx.AbortWithStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, hdd := range hardDrives {
|
for _, hdd := range hardDrives {
|
||||||
hdd.GetTemperature()
|
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
|
// Render the HTML template
|
||||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
ctx.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
"drives": hardDrives,
|
"drives": hardDrives,
|
||||||
|
"older": olderThan.UnixMilli(),
|
||||||
|
"newer": newerThan.UnixMilli(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/JustKato/drive-health/lib/config"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRouter() *gin.Engine {
|
func SetupRouter() *gin.Engine {
|
||||||
// Initialize the Gin engine
|
|
||||||
cfg := config.GetConfiguration()
|
cfg := config.GetConfiguration()
|
||||||
|
|
||||||
|
if !cfg.DebugMode {
|
||||||
|
// Set gin to release
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the Gin engine
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.Use(BasicAuthMiddleware(cfg.IdentityUsername, cfg.IdentityPassword))
|
r.Use(BasicAuthMiddleware(cfg.IdentityUsername, cfg.IdentityPassword))
|
||||||
|
|||||||
22
main.go
22
main.go
@@ -9,12 +9,18 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/config"
|
"github.com/JustKato/drive-health/lib/config"
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/svc"
|
"github.com/JustKato/drive-health/lib/svc"
|
||||||
"tea.chunkbyte.com/kato/drive-health/lib/web"
|
"github.com/JustKato/drive-health/lib/web"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Load .env file if it exists
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Println("[🟨] No .env file found")
|
||||||
|
}
|
||||||
|
|
||||||
// Init the database
|
// Init the database
|
||||||
svc.InitDB()
|
svc.InitDB()
|
||||||
cfg := config.GetConfiguration()
|
cfg := config.GetConfiguration()
|
||||||
@@ -29,12 +35,14 @@ func main() {
|
|||||||
// 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.RunLoggerService()
|
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)
|
||||||
@@ -42,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 |
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {number}
|
||||||
|
*/
|
||||||
|
var initialInputOlder = 0; // miliseconds Unix TimeStamp
|
||||||
|
/**
|
||||||
|
* @typedef {number}
|
||||||
|
*/
|
||||||
|
var initialInputNewer = 0; // miliseconds Unix TimeStamp
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {HTMLInputElement}
|
||||||
|
*/
|
||||||
|
var olderThanInputElement;
|
||||||
|
/**
|
||||||
|
* @typedef {HTMLInputElement}
|
||||||
|
*/
|
||||||
|
var newerThanInputElement;
|
||||||
|
|
||||||
|
document.addEventListener(`DOMContentLoaded`, initializePage)
|
||||||
|
|
||||||
|
function initializePage() {
|
||||||
|
|
||||||
|
// Update the page's time filter
|
||||||
|
initialInputOlder = Number(document.getElementById(`inp-older`).textContent.trim())
|
||||||
|
initialInputNewer = Number(document.getElementById(`inp-newer`).textContent.trim())
|
||||||
|
|
||||||
|
// Bind the date elements
|
||||||
|
olderThanInputElement = document.getElementById(`olderThan`);
|
||||||
|
newerThanInputElement = document.getElementById(`newerThan`);
|
||||||
|
|
||||||
|
olderThanInputElement.value = convertTimestampToDateTimeLocal(initialInputOlder);
|
||||||
|
newerThanInputElement.value = convertTimestampToDateTimeLocal(initialInputNewer);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle one of the date elements having their value changed.
|
||||||
|
function applyDateInterval() {
|
||||||
|
const olderTimeStamp = new Date(olderThanInputElement.value).getTime()
|
||||||
|
const newerTimeStamp = new Date(newerThanInputElement.value).getTime()
|
||||||
|
|
||||||
|
window.location.href = `/?older=${olderTimeStamp}&newer=${newerTimeStamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Unix timestamp to a standard datetime string
|
||||||
|
* @param {number} timestamp - The Unix timestamp in milliseconds.
|
||||||
|
* @returns {string} - A normal string with Y-m-d H:i:s format
|
||||||
|
*/
|
||||||
|
function convertTimestampToDateTimeLocal(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const offset = date.getTimezoneOffset() * 60000; // offset in milliseconds
|
||||||
|
const localDate = new Date(date.getTime() - offset);
|
||||||
|
return localDate.toISOString().slice(0, 19);
|
||||||
|
}
|
||||||
|
|||||||
151
static/style.css
151
static/style.css
@@ -5,11 +5,25 @@
|
|||||||
--bg1: #282d33;
|
--bg1: #282d33;
|
||||||
--bg2: #31373f;
|
--bg2: #31373f;
|
||||||
--bg3: #3e4248;
|
--bg3: #3e4248;
|
||||||
|
--bg4: #1a1c1f;
|
||||||
|
|
||||||
--fg0: #bbc0ca;
|
--fg0: #bbc0ca;
|
||||||
--fg1: #434c56;
|
--fg1: #434c56;
|
||||||
|
|
||||||
--acc: #bbc0ca;
|
--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 {
|
:root {
|
||||||
@@ -17,6 +31,17 @@
|
|||||||
font-family: "Noto Sans Mono", "Roboto", sans-serif;
|
font-family: "Noto Sans Mono", "Roboto", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -54,6 +79,41 @@ html, body {
|
|||||||
margin: 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 */
|
||||||
|
|
||||||
@@ -77,3 +137,92 @@ table thead tr {
|
|||||||
|
|
||||||
padding: .3rem .5rem;
|
padding: .3rem .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Controls */
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: var(--bg3);
|
||||||
|
color: var(--fg0);
|
||||||
|
|
||||||
|
border: 1px solid var(--fg1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
|
||||||
|
border: 1px solid var(--fg1);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
background-color: var(--bg4);
|
||||||
|
color: var(--fg0);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: var(--bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,6 +6,10 @@
|
|||||||
<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 bordered">
|
<div class="container bordered">
|
||||||
|
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
<td>Model</td>
|
<td>Model</td>
|
||||||
<td>Serial</td>
|
<td>Serial</td>
|
||||||
<td>Temperature</td>
|
<td>Temperature</td>
|
||||||
|
<td>Actions</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="disk-table-body">
|
<tbody id="disk-table-body">
|
||||||
@@ -45,6 +50,9 @@
|
|||||||
{{ else }} <!-- Temperature 30°C or below -->
|
{{ else }} <!-- Temperature 30°C or below -->
|
||||||
<td style="color: lime;">{{ $temp }}°C</td>
|
<td style="color: lime;">{{ $temp }}°C</td>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<td>
|
||||||
|
<a title="View Disk" class="info-button" href="/disk/{{ .ID }}"></a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -66,12 +74,40 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="container-body">
|
<div class="container-body">
|
||||||
<div class="pad">
|
<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 }}
|
{{ if len .drives }}
|
||||||
{{ range .drives }}
|
{{ range .drives }}
|
||||||
<div class="disk-graph-entry bordered" id="disk-temp-{{ .ID }}">
|
<div class="disk-graph-entry bordered" id="disk-temp-{{ .ID }}" style="position: relative;">
|
||||||
<h4>{{.Name}}:{{.Serial}} [{{.Size}}]</h4>
|
<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">
|
<a href="/api/v1/disks/{{.ID}}/chart" target="_blank">
|
||||||
<img class="graph-image" src="/api/v1/disks/{{.ID}}/chart" alt="{{ .Model }} Image">
|
<img class="graph-image" src="/api/v1/disks/{{.ID}}/chart?older={{ $older }}&newer={{ $newer }}" alt="{{ .Model }} Image">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
Reference in New Issue
Block a user