Compare commits

..

44 Commits

Author SHA1 Message Date
packagrio-bot eefdc548b2 (v0.4.14) Automated packaging of release by Packagr 2022-06-25 22:13:15 +00:00
Jason Kulatunga fb918e2d6e Merge pull request #308 from AnalogJ/beta
pre v0.4.14 release
2022-06-25 15:03:35 -07:00
Jason Kulatunga 3d9001a5e4 when deviceType not specified in collector config, scrutiny will ignore the device. We need to make sure we correctly override the device.
fixes #255
2022-06-25 11:19:44 -07:00
Jason Kulatunga fbe7d63a24 trying to fix tests. 2022-06-20 18:01:43 -07:00
Jason Kulatunga d718b0898b trying to fix tests. 2022-06-20 17:21:27 -07:00
Jason Kulatunga 44c7211b5f temp artifacts for #304 2022-06-20 13:32:53 -07:00
Jason Kulatunga 157c93b967 provide a mechanism to specify the absolute path to the smartctl binary used by metrics collector.
- fixes #304
2022-06-20 12:09:56 -07:00
Jason Kulatunga 7babc280a0 ensure that users can filter their notifications by:
- failing attribute type (Critical vs All)
 - failure reason (Smart, Scrutiny, Both)

 fixes #300
2022-06-20 08:15:06 -07:00
Jason Kulatunga e364e480e8 update Synology Guide. 2022-06-15 07:10:36 -07:00
Jason Kulatunga bfefe7e98a Merge pull request #303 from SiM22/patch-1 2022-06-15 07:05:29 -07:00
Simon Garcia 831cca7853 Create INSTALL_COLLECTOR_SYNOLOGY_AARCH64.md
A little tutorial to get the collector running on Synology
2022-06-15 09:56:32 +01:00
Jason Kulatunga 46f3b1c02c fix using linter. 2022-06-14 22:21:00 -07:00
Jason Kulatunga 8a1ae2ffa0 Update TROUBLESHOOTING_DEVICE_COLLECTOR.md 2022-06-14 21:41:22 -07:00
packagrio-bot 145c819fc1 (v0.4.13) Automated packaging of release by Packagr 2022-06-14 14:42:54 +00:00
Jason Kulatunga a9ea231de0 Merge pull request #301 from AnalogJ/disable_seek_read_error_rates 2022-06-14 07:33:45 -07:00
Jason Kulatunga c2488af1c3 Disable Seek & Read error rate attribute analysis. Causes issues with Seagate Ironwolf drives.
Added documentation.
2022-06-14 07:32:33 -07:00
Jason Kulatunga ecf7a447a7 Disable Seek & Read error rate attribute analysis. Causes issues with Seagate Ironwolf drives.
Added documentation.
2022-06-14 07:29:23 -07:00
Jason Kulatunga f8e61af2f9 adding docs. 2022-06-13 08:55:27 -07:00
Jason Kulatunga ee61d986d8 Update docker-nightly.yaml 2022-06-12 10:13:10 -07:00
Jason Kulatunga 8fe8cec09a Update TROUBLESHOOTING_DOCKER.md 2022-06-12 10:09:25 -07:00
packagrio-bot b953456d6b (v0.4.12) Automated packaging of release by Packagr 2022-06-11 23:32:42 +00:00
Jason Kulatunga 4057699cad Merge pull request #296 from AnalogJ/beta 2022-06-11 16:22:55 -07:00
Jason Kulatunga d3e7fc6067 make sure we dont create incorrect temp data. 2022-06-11 15:57:12 -07:00
Jason Kulatunga 09a8574d83 fixing tooltip theme. 2022-06-11 15:21:20 -07:00
Jason Kulatunga 7695cc185f color the barchart data in the sparklines, so that we know when a failure/warning was detected (historically) 2022-06-11 14:43:34 -07:00
Jason Kulatunga fc7208020e remove status reason click for more details text. 2022-06-11 12:18:27 -07:00
Jason Kulatunga 75d5930835 correctly using the latest data for table. 2022-06-11 11:00:00 -07:00
Jason Kulatunga 3c9e16169e correctly using the latest data for table. 2022-06-11 09:28:37 -07:00
Jason Kulatunga 9e1076f302 using constants for Attribute status values. 2022-06-11 09:17:35 -07:00
Jason Kulatunga 75ab87e109 Update TROUBLESHOOTING_INFLUXDB.md 2022-06-11 08:13:29 -07:00
Jason Kulatunga 0b8251fce2 Merge pull request #295 from AnalogJ/expanding_row 2022-06-11 08:07:03 -07:00
Jason Kulatunga f57b71ae96 updated tooltips in details page (click for more details). 2022-06-11 08:05:52 -07:00
Jason Kulatunga ce324c3de1 moved nightly build into its own ci job.
fixes #289
2022-06-11 07:47:25 -07:00
packagrio-bot 281b56d287 (v0.4.11) Automated packaging of release by Packagr 2022-06-11 03:29:04 +00:00
Jason Kulatunga cbd23e334b Merge pull request #293 from AnalogJ/beta 2022-06-10 19:48:22 -07:00
Jason Kulatunga 7a0b9c9e0d trying to fix docker image build. 2022-06-10 08:20:13 -07:00
Jason Kulatunga 44b3d982dd trying to fix docker image build. 2022-06-10 08:19:25 -07:00
Jason Kulatunga 769f253e7d fixing the table header for failures. 2022-06-09 22:42:21 -07:00
Jason Kulatunga fbd5bb57ac update descriptions for SCSI attributes. 2022-06-09 22:31:35 -07:00
Jason Kulatunga b9eb5687cd working on expanding row content. 2022-06-09 22:13:44 -07:00
Jason Kulatunga cbd230a7e0 wip expanding row for more details for attributes.
see https://stackblitz.com/angular/eaajjobynjkl?file=src%2Fapp%2Ftable-expandable-rows-example.html

see https://material.angular.io/components/table/examples#table-expandable-rows
2022-06-09 19:39:46 -07:00
Jason Kulatunga 892e9685f3 attempting to fix https://github.com/AnalogJ/scrutiny/issues/281 by removing static flag in artifact build 2022-06-09 19:34:16 -07:00
Jason Kulatunga 7ba7b6efda Update TROUBLESHOOTING_DOCKER.md 2022-06-09 07:35:07 -07:00
Jason Kulatunga 453069deec Create TROUBLESHOOTING_DOCKER.md 2022-06-09 07:34:14 -07:00
58 changed files with 5124 additions and 4337 deletions
-2
View File
@@ -1,7 +1,5 @@
name: Docker
on:
schedule:
- cron: '36 12 * * *'
push:
branches: [ master, beta ]
# Publish semver tags as releases.
+69
View File
@@ -0,0 +1,69 @@
name: Docker - Nightly
on:
schedule:
- cron: '36 12 * * *'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
omnibus:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Generate frontend & version information"
uses: addnab/docker-run-action@v3
with:
image: node:lts
options: -v ${{ github.workspace }}:/work
run: |
cd /work
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
tags: |
type=ref,enable=true,event=branch,suffix=-omnibus-nightly
type=ref,enable=true,event=tag,suffix=-omnibus-nightly
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
context: .
file: docker/Dockerfile
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# cache-from: type=gha
# cache-to: type=gha,mode=max
+3 -3
View File
@@ -98,10 +98,10 @@ func (mc *MetricsCollector) Run() error {
func (mc *MetricsCollector) Validate() error {
mc.logger.Infoln("Verifying required tools")
_, lookErr := exec.LookPath("smartctl")
_, lookErr := exec.LookPath(mc.config.GetString("commands.metrics_smartctl_bin"))
if lookErr != nil {
return errors.DependencyMissingError("smartctl is missing")
return errors.DependencyMissingError(fmt.Sprintf("%s binary is missing", mc.config.GetString("commands.metrics_smartctl_bin")))
}
return nil
@@ -124,7 +124,7 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
}
args = append(args, fullDeviceName)
result, err := mc.shell.Command(mc.logger, "smartctl", args, "", os.Environ())
result, err := mc.shell.Command(mc.logger, mc.config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
resultBytes := []byte(result)
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
+1
View File
@@ -43,6 +43,7 @@ func (c *configuration) Init() error {
c.SetDefault("api.endpoint", "http://localhost:8080")
c.SetDefault("commands.metrics_smartctl_bin", "smartctl")
c.SetDefault("commands.metrics_scan_args", "--scan --json")
c.SetDefault("commands.metrics_info_args", "--info --json")
c.SetDefault("commands.metrics_smart_args", "--xall --json")
+29 -4
View File
@@ -29,7 +29,7 @@ type Detect struct {
func (d *Detect) SmartctlScan() ([]models.Device, error) {
//we use smartctl to detect all the drives available.
args := strings.Split(d.Config.GetString("commands.metrics_scan_args"), " ")
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, d.Config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
if err != nil {
d.Logger.Errorf("Error scanning for devices: %v", err)
return nil, err
@@ -60,7 +60,7 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
}
args = append(args, fullDeviceName)
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, d.Config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
if err != nil {
d.Logger.Errorf("Could not retrieve device information for %s: %v", device.DeviceName, err)
return err
@@ -149,10 +149,35 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
//create a new device group, and replace the one generated by smartctl --scan
overrideDeviceGroup := []models.Device{}
for _, overrideDeviceType := range overrideDevice.DeviceType {
if overrideDevice.DeviceType != nil {
for _, overrideDeviceType := range overrideDevice.DeviceType {
overrideDeviceGroup = append(overrideDeviceGroup, models.Device{
HostId: d.Config.GetString("host.id"),
DeviceType: overrideDeviceType,
DeviceName: strings.TrimPrefix(overrideDeviceFile, DevicePrefix()),
})
}
} else {
//user may have specified device in config file without device type (default to scanned device type)
//check if the device file was detected by the scanner
var deviceType string
if scannedDevice, foundScannedDevice := groupedDevices[overrideDeviceFile]; foundScannedDevice {
if len(scannedDevice) > 0 {
//take the device type from the first grouped device
deviceType = scannedDevice[0].DeviceType
} else {
deviceType = "ata"
}
} else {
//fallback to ata if no scanned device detected
deviceType = "ata"
}
overrideDeviceGroup = append(overrideDeviceGroup, models.Device{
HostId: d.Config.GetString("host.id"),
DeviceType: overrideDeviceType,
DeviceType: deviceType,
DeviceName: strings.TrimPrefix(overrideDeviceFile, DevicePrefix()),
})
}
+63
View File
@@ -19,6 +19,7 @@ func TestDetect_SmartctlScan(t *testing.T) {
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl)
@@ -47,6 +48,7 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl)
@@ -78,6 +80,7 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl)
@@ -108,6 +111,7 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
detectedDevices := models.Scan{
@@ -140,6 +144,7 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
detectedDevices := models.Scan{
@@ -170,6 +175,7 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
{
@@ -210,6 +216,7 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
detectedDevices := models.Scan{
@@ -234,3 +241,59 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "sat+megaraid", transformedDevices[0].DeviceType)
}
// test https://github.com/AnalogJ/scrutiny/issues/255#issuecomment-1164024126
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/sda",
InfoName: "/dev/sda",
Protocol: "ata",
Type: "scsi",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
//test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
}
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
detectedDevices := models.Scan{}
d := detect.Detect{
Config: fakeConfig,
}
//test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "ata", transformedDevices[0].DeviceType)
}
+3 -3
View File
@@ -6,9 +6,9 @@ WORKDIR /go/src/github.com/analogj/scrutiny
COPY . /go/src/github.com/analogj/scrutiny
RUN go mod vendor && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
go build -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go && \
go build -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
go build -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
########
FROM debian:bullseye-slim as runtime
+1 -1
View File
@@ -6,7 +6,7 @@ WORKDIR /go/src/github.com/analogj/scrutiny
COPY . /go/src/github.com/analogj/scrutiny
RUN go mod vendor && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
go build -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
########
FROM debian:bullseye-slim as runtime
+134
View File
@@ -0,0 +1,134 @@
# Install collector on Synology
## Install Entware
This will allow you to install a newer version of smartmontools on your Synology. Follow the instructions here (This is tested on DSM7) - https://github.com/Entware/Entware/wiki/Install-on-Synology-NAS
**PLEASE NOTE THAT IF YOU UPDATE DSM FIRMWARE YOU MAY BORK THE EXISTING ENTWARE INSTALLATION, FOR ANYTHING THAT MAY RELATE TO ENTWARE PLEASE VISIT THEIR REPO**
## Collector Setup
**1. Run an update**
`sudo opkg update`
**2. Run an upgrade**
`sudo opkg upgrade`
**3. Install smartmontools**
`sudo opkg install smartmontools`
*It should install v7.2-2*
`Installing smartmontools (7.2-2) to root...`
**4. We will now create the directories.**
```
mkdir -p /volume1/\@Entware/scrutiny/bin
mkdir -p /volume1/\@Entware/scrutiny/conf
```
**5. change into the bin directory**
`cd /volume1/\@Entware/scrutiny/bin`
**6. Download the collector binary for your architecture and make it executable**
`wget https://github.com/AnalogJ/scrutiny/releases/download/v0.4.12/scrutiny-collector-metrics-linux-arm64`
`chmod +x /volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64`
**7. Create a config file for the collector**
```
cd /volume1/\@Entware/scrutiny/conf
wget https://raw.githubusercontent.com/AnalogJ/scrutiny/master/example.collector.yaml
mv example.collector.yaml collector.yaml
```
**8. Lets make some changes in the [collector config file](../example.collector.yaml), these are what i uncommented/added, please tweak the device paths to your needs**
```
host:
id: 'Server_Name'
devices:
# # example for forcing device type detection for a single disk
- device: /dev/sda
type: 'sat'
- device: /dev/sdb
type: 'sat'
- device: /dev/sdc
type: 'sat'
- device: /dev/sdd
type: 'sat'
api:
endpoint: 'http://<url>:8080'
```
**9. Let's update the smartd db**
```
cd /volume1/\@Entware/scrutiny/bin/
wget https://raw.githubusercontent.com/smartmontools/smartmontools/master/smartmontools/drivedb.h
```
**10. I ran it like this but you can tweak to your liking, the most important part is the --drivedb, as this loads it into the aplication for future use**
`smartctl -d sat --all /dev/sda --drivedb=/volume1/\@Entware/scrutiny/bin/drivedb.h`
**11. Now lets create a small bash script, this will be used for the scheduled task inside Synology**
`vim /volume1/\@Entware/scrutiny/bin/run_collect.sh`
**The contents are below, copy and paste them in**
```
#!/bin/bash
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/config/collector.yaml
```
## Set up Synology to run a scheduled task.
Log in to DSM and do the following:
Goto: DSM > Control Panel > Task Scheduler
Create > Scheduled Task > User Defined Script
###### General
```
Task: Scrutiny_Collector
User: root
Enabled: yes
```
###### Schedule
```
Run on the following days: Daily
```
###### Time:
```
Frequency: <Your desired frequency>
```
###### Task Settings
**Run Command**
```
. /opt/etc/profile; /volume1/\@Entware/scrutiny/bin/run_collect.sh
```
## Troubleshooting
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
+2 -1
View File
@@ -7,10 +7,11 @@ Once a guide is created (in `docs/guides/`) it will be linked here.
- [x] [unraid](./INSTALL_UNRAID.md)
- [ ] ESXI
- [ ] Proxmox
- [ ] Synology
- [x] Synology(./INSTALL_SYNOLOGY_COLLECTOR.md)
- [ ] OMV
- [ ] Amahi
- [ ] Running in a LXC container
- [x] [PFSense](./INSTALL_UNRAID.md)
- [ ] QNAP
- [ ] RockStor
+67 -1
View File
@@ -180,6 +180,72 @@ If Scrutiny detects that an attribute corresponds with a high rate of failure us
This can cause some confusion when comparing Scrutiny's dashboard against other SMART analysis tools.
If you hover over the "failed" label beside an attribute, Scrutiny will tell you if the failure was due to SMART or Scrutiny/BackBlaze data.
### Device failed but Smart & Scrutiny passed
Device SMART results are the source of truth for Scrutiny, however we don't just take into account the current SMART results, but also historical analysis of a disk.
This means that if a device is marked as failed at any point in its history, it will continue to be stored in the database as failed until the device is removed (or status is reset -- see below).
In some cases, this historical failure may have been due to attribute analysis/thresholds that have since been relaxed:
- NVME - Numb Error Log Entries (v0.4.7)
- ATA - Power Cycle Count (v0.4.7)
- ATA - Read Error Rate (v0.4.13)
- ATA - Seek Error Rate (v0.4.13)
If you'd like to reset the status of a disk (to healthy) and allow the next run of the collector to determine the actual status, you can run the following command:
```bash
# connect to scrutiny docker container
docker exec -it scrutiny bash
# install sqlite CLI tools (inside container)
apt update && apt install -y sqlite3
# connect to the scrutiny database
sqlite3 /opt/scrutiny/config/scrutiny.db
# reset/update the devices table, unset the failure status.
UPDATE devices SET device_status = null;
# exit sqlite CLI
.exit
```
### Seagate Drives Failing
As thoroughly discussed in [#255](https://github.com/AnalogJ/scrutiny/issues/255), Seagate (Ironwolf & others) drives are almost always marked as failed by Scrutiny.
> The `Seek Error Rate` & `Read Error Rate` attribute raw values are typically very high, and the
> normalised values (Current / Worst / Threshold) are usually quite low. Despite this, the numbers in most cases are perfectly OK
>
> The anxiety arises because we intuitively expect that the normalised values should reflect a "health" score, with
> 100 being the ideal value. Similarly, we would expect that the raw values should reflect an error count, in
> which case a value of 0 would be most desirable. However, Seagate calculates and applies these attribute values
> in a counterintuitive way.
>
> http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
Some analysis has been done which shows that Seagate drives break the common SMART conventions, which also causes Scrutiny's
comparison against BackBlaze data to detect these drives as failed.
**So what's the Solution?**
After taking a look at the BackBlaze data for the relevant Attributes (`Seek Error Rate` & `Read Error Rate`), I've decided
to disable Scrutiny analysis for them. Both are non-critical, and have low-correlation with failure.
> Please note: SMART failures for these attributes will still cause the drive to be marked as failed. Only BackBlaze analysis has been disabled
If this is effecting your drives, you'll need to do the following:
1. Upgrade to v0.4.13+
2. Reset your drive status using the SQLite script in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
3. Wait for (or manually start) the collector.
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ from
other drives, please read the following:
- http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
- https://www.truenas.com/community/threads/seagate-ironwolf-smart-test-raw_read_error_rate-seek_error_rate.68634/
## Hub & Spoke model, with multiple Hosts.
@@ -201,4 +267,4 @@ Or if you're not using docker, you can pass CLI arguments to the collector durin
```bash
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
```
```
+19
View File
@@ -0,0 +1,19 @@
# Docker Images `master-omnibus` vs `latest`
> TL;DR; The `master-omnibus` and `latest` tags are almost semantically identical, as I follow a `golden master`
development process. However if you want to ensure you're only using the latest release, you can change to `latest`
The CI script used to orchestrate the docker image builds can be found here: https://github.com/AnalogJ/scrutiny/blob/master/.github/workflows/docker-build.yaml#L166-L184
In general Scrutiny follows a `golden master` development process, which means that the `master` branch is not directly updated (unless its for documentation changes),
instead development is done in a feature branch, or committed to the `beta` branch.
As development progresses, and we're satisfied that a feature is complete, and the quality is acceptable,
I merge the changes to `master` and trigger the creation of a new release -- ie, when master is updated, a new release
is almost immediately created (and tagged with `latest`)
So changing from `master-omnibus -> latest` will be the same thing for all intents and purposes.
> NOTE: Previously, there was a `automated cron build` that ran on the `master` and `beta` branches.
They used to trigger a `nightly` build, even if nothing has changed on the branch. This has a couple of benefits, but one is to
ensure that there's no broken external dependencies in our (unchanged) code. This `nightly` build no longer updates the `master-omnibus` tag.
+9
View File
@@ -54,6 +54,15 @@ time="2022-05-13T14:38:05Z" level=info msg="Successfully connected to scrutiny s
panic: a username and password is required for a setup
```
or
```
Start the scrutiny server
time="2022-06-11T10:35:04-04:00" level=info msg="Trying to connect to scrutiny sqlite db: \n"
time="2022-06-11T10:35:04-04:00" level=info msg="Successfully connected to scrutiny sqlite db: \n"
panic: failed to check influxdb setup status - parse "://:": missing protocol scheme
```
As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234),
this usually related to either:
+1
View File
@@ -73,6 +73,7 @@ devices:
# example to show how to override the smartctl command args globally
#commands:
# metrics_smartctl_bin: 'smartctl' # change to provide custom `smartctl` binary path, eg. `/usr/sbin/smartctl`
# metrics_scan_args: '--scan --json' # used to detect devices
# metrics_info_args: '--info --json' # used to determine device unique ID & register device with Scrutiny
# metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
+2
View File
@@ -73,6 +73,8 @@ log:
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
# - "script:///file/path/on/disk"
# - "https://www.example.com/path"
# filter_attributes: 'all' # options: 'all' or 'critical'
# level: 'fail' # options: 'fail', 'fail_scrutiny', 'fail_smart'
########################################################################################################################
# FEATURES COMING SOON
+3
View File
@@ -2,6 +2,7 @@ package config
import (
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/spf13/viper"
"log"
@@ -38,6 +39,8 @@ func (c *configuration) Init() error {
c.SetDefault("log.file", "")
c.SetDefault("notify.urls", []string{})
c.SetDefault("notify.filter_attributes", pkg.NotifyFilterAttributesAll)
c.SetDefault("notify.level", pkg.NotifyLevelFail)
c.SetDefault("web.influxdb.scheme", "http")
c.SetDefault("web.influxdb.host", "localhost")
+7
View File
@@ -4,6 +4,13 @@ const DeviceProtocolAta = "ATA"
const DeviceProtocolScsi = "SCSI"
const DeviceProtocolNvme = "NVMe"
const NotifyFilterAttributesAll = "all"
const NotifyFilterAttributesCritical = "critical"
const NotifyLevelFail = "fail"
const NotifyLevelFailScrutiny = "fail_scrutiny"
const NotifyLevelFailSmart = "fail_smart"
type AttributeStatus uint8
const (
+12
View File
@@ -0,0 +1,12 @@
package database
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"sort"
)
func sortSmartMeasurementsDesc(smartResults []measurements.Smart) {
sort.SliceStable(smartResults, func(i, j int) bool {
return smartResults[i].Date.After(smartResults[j].Date)
})
}
@@ -0,0 +1,30 @@
package database
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func Test_sortSmartMeasurementsDesc_LatestFirst(t *testing.T) {
//setup
timeNow := time.Now()
smartResults := []measurements.Smart{
{
Date: timeNow.AddDate(0, 0, -2),
},
{
Date: timeNow,
},
{
Date: timeNow.AddDate(0, 0, -1),
},
}
//test
sortSmartMeasurementsDesc(smartResults)
//assert
require.Equal(t, smartResults[0].Date, timeNow)
}
@@ -29,6 +29,7 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin
return deviceSmartData, sr.saveDatapoint(sr.influxWriteApi, "smart", tags, fields, deviceSmartData.Date, ctx)
}
// GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end.
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error) {
// Get SMartResults from InfluxDB
@@ -64,6 +65,9 @@ func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn
return nil, err
}
//we have to sort the smartResults again, because the `union` command will return multiple 'tables' and only sort the records in each table.
sortSmartMeasurementsDesc(smartResults)
return smartResults, nil
//if err := device.SquashHistory(); err != nil {
@@ -109,7 +109,7 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string {
|> toInt()
temp_data
|> aggregateWindow(fn: mean, every: aggWindow)
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> to(bucket: destBucket, org: destOrg)
`,
sourceBucket,
+122 -13
View File
@@ -6,7 +6,11 @@ import (
"errors"
"fmt"
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"github.com/containrrr/shoutrrr"
shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
"github.com/sirupsen/logrus"
@@ -14,28 +18,130 @@ import (
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
const NotifyFailureTypeEmailTest = "EmailTest"
const NotifyFailureTypeSmartPrefail = "SmartPreFailure"
const NotifyFailureTypeBothFailure = "SmartFailure" //SmartFailure always takes precedence when Scrutiny & Smart failed.
const NotifyFailureTypeSmartFailure = "SmartFailure"
const NotifyFailureTypeSmartErrorLog = "SmartErrorLog"
const NotifyFailureTypeSmartSelfTest = "SmartSelfTestLog"
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLevel string, notifyFilterAttributes string) bool {
// 1. check if the device is healthy
if device.DeviceStatus == pkg.DeviceStatusPassed {
return false
}
// setup constants for comparison
var requiredDeviceStatus pkg.DeviceStatus
var requiredAttrStatus pkg.AttributeStatus
if notifyLevel == pkg.NotifyLevelFail {
// either scrutiny or smart failures should trigger an email
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
} else if notifyLevel == pkg.NotifyLevelFailSmart {
//only smart failures
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
requiredAttrStatus = pkg.AttributeStatusFailedSmart
} else {
requiredDeviceStatus = pkg.DeviceStatusFailedScrutiny
requiredAttrStatus = pkg.AttributeStatusFailedScrutiny
}
// 2. check if the attributes that are failing should be filtered (non-critical)
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
if notifyFilterAttributes == pkg.NotifyFilterAttributesCritical {
hasFailingCriticalAttr := false
var statusFailingCrtiticalAttr pkg.AttributeStatus
for attrId, attrData := range smartAttrs.Attributes {
//find failing attribute
if attrData.GetStatus() == pkg.AttributeStatusPassed {
continue //skip all passing attributes
}
// merge the status's of all critical attributes
statusFailingCrtiticalAttr = pkg.AttributeStatusSet(statusFailingCrtiticalAttr, attrData.GetStatus())
//found a failing attribute, see if its critical
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
hasFailingCriticalAttr = true
} else if device.IsNvme() && thresholds.NmveMetadata[attrId].Critical {
hasFailingCriticalAttr = true
} else {
//this is ATA
attrIdInt, err := strconv.Atoi(attrId)
if err != nil {
continue
}
if thresholds.AtaMetadata[attrIdInt].Critical {
hasFailingCriticalAttr = true
}
}
}
if !hasFailingCriticalAttr {
//no critical attributes are failing, and notifyFilterAttributes == "critical"
return false
} else {
// check if any of the critical attributes have a status that we're looking for
return pkg.AttributeStatusHas(statusFailingCrtiticalAttr, requiredAttrStatus)
}
} else {
// 2. SKIP - we are processing every attribute.
// 3. check if the device failure level matches the wanted failure level.
return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus)
}
}
// TODO: include host and/or user label for device.
type Payload struct {
Date string `json:"date"` //populated by Send function.
FailureType string `json:"failure_type"` //EmailTest, SmartFail, ScrutinyFail
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
DeviceName string `json:"device_name"` //dev/sda
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
Test bool `json:"test"` // false
//should not be populated
Subject string `json:"subject"`
Message string `json:"message"`
//private, populated during init (marked as Public for JSON serialization)
Date string `json:"date"` //populated by Send function.
FailureType string `json:"failure_type"` //EmailTest, BothFail, SmartFail, ScrutinyFail
Subject string `json:"subject"`
Message string `json:"message"`
}
func NewPayload(device models.Device, test bool) Payload {
payload := Payload{
DeviceType: device.DeviceType,
DeviceName: device.DeviceName,
DeviceSerial: device.SerialNumber,
Test: test,
}
//validate that the Payload is populated
sendDate := time.Now()
payload.Date = sendDate.Format(time.RFC3339)
payload.FailureType = payload.GenerateFailureType(device.DeviceStatus)
payload.Subject = payload.GenerateSubject()
payload.Message = payload.GenerateMessage()
return payload
}
func (p *Payload) GenerateFailureType(deviceStatus pkg.DeviceStatus) string {
//generate a failure type, given Test and DeviceStatus
if p.Test {
return NotifyFailureTypeEmailTest // must be an email test if "Test" is true
}
if pkg.DeviceStatusHas(deviceStatus, pkg.DeviceStatusFailedSmart) && pkg.DeviceStatusHas(deviceStatus, pkg.DeviceStatusFailedScrutiny) {
return NotifyFailureTypeBothFailure //both failed
} else if pkg.DeviceStatusHas(deviceStatus, pkg.DeviceStatusFailedSmart) {
return NotifyFailureTypeSmartFailure //only SMART failed
} else {
return NotifyFailureTypeScrutinyFailure //only Scrutiny failed
}
}
func (p *Payload) GenerateSubject() string {
@@ -61,6 +167,14 @@ Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceTy
return message
}
func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify {
return Notify{
Logger: logger,
Config: appconfig,
Payload: NewPayload(device, test),
}
}
type Notify struct {
Logger logrus.FieldLogger
Config config.Interface
@@ -68,11 +182,6 @@ type Notify struct {
}
func (n *Notify) Send() error {
//validate that the Payload is populated
sendDate := time.Now()
n.Payload.Date = sendDate.Format(time.RFC3339)
n.Payload.Subject = n.Payload.GenerateSubject()
n.Payload.Message = n.Payload.GenerateMessage()
//retrieve list of notification endpoints from config file
configUrls := n.Config.GetStringSlice("notify.urls")
+161
View File
@@ -0,0 +1,161 @@
package notify
import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/stretchr/testify/require"
"testing"
)
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusPassed,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyLevelFail_FailingSmartDevice(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyLevelFailSmart_FailingSmartDevice(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFailSmart
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyLevelFailScrutiny_FailingSmartDevice(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFailScrutiny
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"5": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusFailedSmart,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"5": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusPassed,
},
"10": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusFailedScrutiny,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"1": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusFailedSmart,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"5": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusPassed,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"5": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusPassed,
},
"10": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusFailedScrutiny,
},
}}
notifyLevel := pkg.NotifyLevelFailSmart
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
}
@@ -36,56 +36,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
Ideal: ObservedThresholdIdealLow,
Critical: false,
Description: "(Vendor specific raw value.) Stores data related to the rate of hardware read errors that occurred when reading data from a disk surface. The raw value has different structure for different vendors and is often not meaningful as a decimal number.",
ObservedThresholds: []ObservedThreshold{
{
Low: 80,
High: 95,
AnnualFailureRate: 0.8879749768303985,
ErrorInterval: []float64{0.682344353388663, 1.136105732920724},
},
{
Low: 95,
High: 110,
AnnualFailureRate: 0.034155719633986996,
ErrorInterval: []float64{0.030188482024981093, 0.038499386872354435},
},
{
Low: 110,
High: 125,
AnnualFailureRate: 0.06390002135229157,
ErrorInterval: []float64{0.05852004676110847, 0.06964160930553712},
},
{
Low: 125,
High: 140,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 140,
High: 155,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 155,
High: 170,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 170,
High: 185,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 185,
High: 200,
AnnualFailureRate: 0.044823775021490854,
ErrorInterval: []float64{0.032022762038723306, 0.06103725943096589},
},
},
},
2: {
ID: 2,
@@ -290,56 +240,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
Ideal: "",
Critical: false,
Description: "(Vendor specific raw value.) Rate of seek errors of the magnetic heads. If there is a partial failure in the mechanical positioning system, then seek errors will arise. Such a failure may be due to numerous factors, such as damage to a servo, or thermal widening of the hard disk. The raw value has different structure for different vendors and is often not meaningful as a decimal number.",
ObservedThresholds: []ObservedThreshold{
{
Low: 58,
High: 76,
AnnualFailureRate: 0.2040131025936549,
ErrorInterval: []float64{0.17032852883286412, 0.2424096283327138},
},
{
Low: 76,
High: 94,
AnnualFailureRate: 0.08725919610118257,
ErrorInterval: []float64{0.08077138510999876, 0.09412943212007528},
},
{
Low: 94,
High: 112,
AnnualFailureRate: 0.01087335627722523,
ErrorInterval: []float64{0.008732197944943352, 0.013380600544561905},
},
{
Low: 112,
High: 130,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 130,
High: 148,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 148,
High: 166,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 166,
High: 184,
AnnualFailureRate: 0,
ErrorInterval: []float64{0, 0},
},
{
Low: 184,
High: 202,
AnnualFailureRate: 0.05316285755900475,
ErrorInterval: []float64{0.03370069132942804, 0.07977038905848267},
},
},
},
8: {
ID: 8,
@@ -19,7 +19,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "low",
Critical: true,
Description: "",
Description: "The grown defect count shows the amount of swapped (defective) blocks since the drive was shipped by it's vendor. Each additional defective block increases the count by one.",
},
"read_errors_corrected_by_eccfast": {
ID: "read_errors_corrected_by_eccfast",
@@ -27,7 +27,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "An error correction was applied to get perfect data (a.k.a. ECC on-the-fly). \"Without substantial delay\" means the correction did not postpone reading of later sectors (e.g. a revolution was not lost). The counter is incremented once for each logical block that requires correction. Two different blocks corrected during the same command are counted as two events.",
},
"read_errors_corrected_by_eccdelayed": {
ID: "read_errors_corrected_by_eccdelayed",
@@ -35,7 +35,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "An error code or algorithm (e.g. ECC, checksum) is applied in order to get perfect data with substantial delay. \"With possible delay\" means the correction took longer than a sector time so that reading/writing of subsequent sectors was delayed (e.g. a lost revolution). The counter is incremented once for each logical block that requires correction. A block with a double error that is correctable counts as one event and two different blocks corrected during the same command count as two events. ",
},
"read_errors_corrected_by_rereads_rewrites": {
ID: "read_errors_corrected_by_rereads_rewrites",
@@ -43,7 +43,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "low",
Critical: true,
Description: "",
Description: "This parameter code specifies the counter counting the number of errors that are corrected by applying retries. This counts errors recovered, not the number of retries. If five retries were required to recover one block of data, the counter increments by one, not five. The counter is incremented once for each logical block that is recovered using retries. If an error is not recoverable while applying retries and is recovered by ECC, it isn't counted by this counter; it will be counted by the counter specified by parameter code 01h - Errors Corrected With Possible Delays. ",
},
"read_total_errors_corrected": {
ID: "read_total_errors_corrected",
@@ -51,7 +51,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "This counter counts the total of parameter code errors 00h, 01h and 02h (i.e. error corrected by ECC: fast and delayed plus errors corrected by rereads and rewrites). There is no \"double counting\" of data errors among these three counters. The sum of all correctable errors can be reached by adding parameter code 01h and 02h errors, not by using this total.",
},
"read_correction_algorithm_invocations": {
ID: "read_correction_algorithm_invocations",
@@ -59,7 +59,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "This parameter code specifies the counter that counts the total number of retries, or \"times the retry algorithm is invoked\". If after five attempts a counter 02h type error is recovered, then five is added to this counter. If three retries are required to get stable ECC syndrome before a counter 01h type error is corrected, then those three retries are also counted here. The number of retries applied to unsuccessfully recover an error (counter 06h type error) are also counted by this counter. ",
},
"read_total_uncorrected_errors": {
ID: "read_total_uncorrected_errors",
@@ -67,7 +67,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "low",
Critical: true,
Description: "",
Description: "This parameter code specifies the counter that contains the total number of blocks for which an uncorrected data error has occurred. ",
},
"write_errors_corrected_by_eccfast": {
ID: "write_errors_corrected_by_eccfast",
@@ -75,7 +75,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "An error correction was applied to get perfect data (a.k.a. ECC on-the-fly). \"Without substantial delay\" means the correction did not postpone reading of later sectors (e.g. a revolution was not lost). The counter is incremented once for each logical block that requires correction. Two different blocks corrected during the same command are counted as two events. ",
},
"write_errors_corrected_by_eccdelayed": {
ID: "write_errors_corrected_by_eccdelayed",
@@ -83,7 +83,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "An error code or algorithm (e.g. ECC, checksum) is applied in order to get perfect data with substantial delay. \"With possible delay\" means the correction took longer than a sector time so that reading/writing of subsequent sectors was delayed (e.g. a lost revolution). The counter is incremented once for each logical block that requires correction. A block with a double error that is correctable counts as one event and two different blocks corrected during the same command count as two events. ",
},
"write_errors_corrected_by_rereads_rewrites": {
ID: "write_errors_corrected_by_rereads_rewrites",
@@ -91,7 +91,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "low",
Critical: true,
Description: "",
Description: "This parameter code specifies the counter counting the number of errors that are corrected by applying retries. This counts errors recovered, not the number of retries. If five retries were required to recover one block of data, the counter increments by one, not five. The counter is incremented once for each logical block that is recovered using retries. If an error is not recoverable while applying retries and is recovered by ECC, it isn't counted by this counter; it will be counted by the counter specified by parameter code 01h - Errors Corrected With Possible Delays.",
},
"write_total_errors_corrected": {
ID: "write_total_errors_corrected",
@@ -99,7 +99,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "This counter counts the total of parameter code errors 00h, 01h and 02h (i.e. error corrected by ECC: fast and delayed plus errors corrected by rereads and rewrites). There is no \"double counting\" of data errors among these three counters. The sum of all correctable errors can be reached by adding parameter code 01h and 02h errors, not by using this total.",
},
"write_correction_algorithm_invocations": {
ID: "write_correction_algorithm_invocations",
@@ -107,7 +107,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "",
Critical: false,
Description: "",
Description: "This parameter code specifies the counter that counts the total number of retries, or \"times the retry algorithm is invoked\". If after five attempts a counter 02h type error is recovered, then five is added to this counter. If three retries are required to get stable ECC syndrome before a counter 01h type error is corrected, then those three retries are also counted here. The number of retries applied to unsuccessfully recover an error (counter 06h type error) are also counted by this counter. ",
},
"write_total_uncorrected_errors": {
ID: "write_total_uncorrected_errors",
@@ -115,6 +115,6 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
DisplayType: "",
Ideal: "low",
Critical: true,
Description: "",
Description: " This parameter code specifies the counter that contains the total number of blocks for which an uncorrected data error has occurred.",
},
}
+1 -1
View File
@@ -2,4 +2,4 @@ package version
// VERSION is the app-global version string, which will be replaced with a
// new value during packaging
const VERSION = "0.4.10"
const VERSION = "0.4.14"
@@ -15,17 +15,16 @@ func SendTestNotification(c *gin.Context) {
appConfig := c.MustGet("CONFIG").(config.Interface)
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
testNotify := notify.Notify{
Logger: logger,
Config: appConfig,
Payload: notify.Payload{
FailureType: "EmailTest",
DeviceSerial: "FAKEWDDJ324KSO",
testNotify := notify.New(
logger,
appConfig,
models.Device{
SerialNumber: "FAKEWDDJ324KSO",
DeviceType: pkg.DeviceProtocolAta,
DeviceName: "/dev/sda",
Test: true,
},
}
true,
)
err := testNotify.Send()
if err != nil {
logger.Errorln("An error occurred while sending test notification", err)
@@ -63,20 +63,16 @@ func UploadDeviceMetrics(c *gin.Context) {
}
//check for error
if updatedDevice.DeviceStatus != pkg.DeviceStatusPassed {
if notify.ShouldNotify(updatedDevice, smartData, appConfig.GetString("notify.level"), appConfig.GetString("notify.filter_attributes")) {
//send notifications
testNotify := notify.Notify{
Config: appConfig,
Payload: notify.Payload{
FailureType: notify.NotifyFailureTypeSmartFailure,
DeviceName: updatedDevice.DeviceName,
DeviceType: updatedDevice.DeviceProtocol,
DeviceSerial: updatedDevice.SerialNumber,
Test: false,
},
Logger: logger,
}
_ = testNotify.Send() //we ignore error message when sending notifications.
liveNotify := notify.New(
logger,
appConfig,
updatedDevice,
false,
)
_ = liveNotify.Send() //we ignore error message when sending notifications.
}
c.JSON(http.StatusOK, gin.H{"success": true})
+18
View File
@@ -186,6 +186,8 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
} else {
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes()
}
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
ae := web.AppEngine{
Config: fakeConfig,
@@ -219,6 +221,8 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
fakeConfig := mock_config.NewMockInterface(mockCtrl)
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -326,6 +330,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
@@ -365,6 +372,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
@@ -404,6 +414,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
@@ -443,6 +456,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
@@ -481,6 +497,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
@@ -21,10 +21,10 @@ export class TreoConfigService
{
let currentScrutinyConfig = defaultConfig
let localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
const localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
if (localConfigStr){
//check localstorage for a value
let localConfig = JSON.parse(localConfigStr)
// check localstorage for a value
const localConfig = JSON.parse(localConfigStr)
currentScrutinyConfig = Object.assign({}, currentScrutinyConfig, localConfig) // make sure defaults are available if missing from localStorage.
}
// Set the private defaults
@@ -38,20 +38,20 @@ export class TreoConfigService
/**
* Setter and getter for config
*/
//Setter
// Setter
set config(value: any)
{
// Merge the new config over to the current config
let config = _.merge({}, this._config.getValue(), value);
const config = _.merge({}, this._config.getValue(), value);
//Store the config in localstorage
// Store the config in localstorage
localStorage.setItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
// Execute the observable
this._config.next(config);
}
//Getter
// Getter
get config$(): Observable<any>
{
return this._config.asObservable();
@@ -1,7 +1,7 @@
import { Layout } from "app/layout/layout.types";
import { Layout } from 'app/layout/layout.types';
// Theme type
export type Theme = "light" | "dark" | "system";
export type Theme = 'light' | 'dark' | 'system';
/**
* AppConfig interface. Update this interface to strictly type your config
@@ -28,12 +28,12 @@ export interface AppConfig
* "ConfigService".
*/
export const appConfig: AppConfig = {
theme : "light",
layout: "material",
theme : 'light',
layout: 'material',
dashboardDisplay: "name",
dashboardSort: "status",
dashboardDisplay: 'name',
dashboardSort: 'status',
temperatureUnit: "celsius",
temperatureUnit: 'celsius',
};
@@ -1,264 +1,264 @@
export const sda = {
"data": {
"device": {
"CreatedAt": "2021-06-24T21:17:31.301226-07:00",
"UpdatedAt": "2021-10-24T16:37:56.981833-07:00",
"DeletedAt": null,
"wwn": "0x5002538e40a22954",
"device_name": "sda",
"manufacturer": "ATA",
"model_name": "Samsung_SSD_860_EVO_500GB",
"interface_type": "SCSI",
"interface_speed": "",
"serial_number": "S3YZNB0KBXXXXXX",
"firmware": "002C",
"rotational_speed": 0,
"capacity": 500107862016,
"form_factor": "",
"smart_support": false,
"device_protocol": "NVMe",
"device_type": "",
"label": "",
"host_id": "",
"device_status": 0
'data': {
'device': {
'CreatedAt': '2021-06-24T21:17:31.301226-07:00',
'UpdatedAt': '2021-10-24T16:37:56.981833-07:00',
'DeletedAt': null,
'wwn': '0x5002538e40a22954',
'device_name': 'sda',
'manufacturer': 'ATA',
'model_name': 'Samsung_SSD_860_EVO_500GB',
'interface_type': 'SCSI',
'interface_speed': '',
'serial_number': 'S3YZNB0KBXXXXXX',
'firmware': '002C',
'rotational_speed': 0,
'capacity': 500107862016,
'form_factor': '',
'smart_support': false,
'device_protocol': 'NVMe',
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
},
"smart_results": [{
"date": "2021-10-24T23:20:44Z",
"device_wwn": "0x5002538e40a22954",
"device_protocol": "NVMe",
"temp": 36,
"power_on_hours": 2401,
"power_cycle_count": 266,
"attrs": {
"available_spare": {
"attribute_id": "available_spare",
"value": 100,
"thresh": 10,
"transformed_value": 0,
"status": 0
'smart_results': [{
'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5002538e40a22954',
'device_protocol': 'NVMe',
'temp': 36,
'power_on_hours': 2401,
'power_cycle_count': 266,
'attrs': {
'available_spare': {
'attribute_id': 'available_spare',
'value': 100,
'thresh': 10,
'transformed_value': 0,
'status': 0
},
"controller_busy_time": {
"attribute_id": "controller_busy_time",
"value": 3060,
"thresh": -1,
"transformed_value": 0,
"status": 0
'controller_busy_time': {
'attribute_id': 'controller_busy_time',
'value': 3060,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"critical_comp_time": {
"attribute_id": "critical_comp_time",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'critical_comp_time': {
'attribute_id': 'critical_comp_time',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"critical_warning": {
"attribute_id": "critical_warning",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'critical_warning': {
'attribute_id': 'critical_warning',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"data_units_read": {
"attribute_id": "data_units_read",
"value": 9511859,
"thresh": -1,
"transformed_value": 0,
"status": 0
'data_units_read': {
'attribute_id': 'data_units_read',
'value': 9511859,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"data_units_written": {
"attribute_id": "data_units_written",
"value": 7773431,
"thresh": -1,
"transformed_value": 0,
"status": 0
'data_units_written': {
'attribute_id': 'data_units_written',
'value': 7773431,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"host_reads": {
"attribute_id": "host_reads",
"value": 111303174,
"thresh": -1,
"transformed_value": 0,
"status": 0
'host_reads': {
'attribute_id': 'host_reads',
'value': 111303174,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"host_writes": {
"attribute_id": "host_writes",
"value": 83170961,
"thresh": -1,
"transformed_value": 0,
"status": 0
'host_writes': {
'attribute_id': 'host_writes',
'value': 83170961,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"media_errors": {
"attribute_id": "media_errors",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'media_errors': {
'attribute_id': 'media_errors',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"num_err_log_entries": {
"attribute_id": "num_err_log_entries",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'num_err_log_entries': {
'attribute_id': 'num_err_log_entries',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"percentage_used": {
"attribute_id": "percentage_used",
"value": 0,
"thresh": 100,
"transformed_value": 0,
"status": 0
'percentage_used': {
'attribute_id': 'percentage_used',
'value': 0,
'thresh': 100,
'transformed_value': 0,
'status': 0
},
"power_cycles": {
"attribute_id": "power_cycles",
"value": 266,
"thresh": -1,
"transformed_value": 0,
"status": 0
'power_cycles': {
'attribute_id': 'power_cycles',
'value': 266,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"power_on_hours": {
"attribute_id": "power_on_hours",
"value": 2401,
"thresh": -1,
"transformed_value": 0,
"status": 0
'power_on_hours': {
'attribute_id': 'power_on_hours',
'value': 2401,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"temperature": {
"attribute_id": "temperature",
"value": 36,
"thresh": -1,
"transformed_value": 0,
"status": 0
'temperature': {
'attribute_id': 'temperature',
'value': 36,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"unsafe_shutdowns": {
"attribute_id": "unsafe_shutdowns",
"value": 43,
"thresh": -1,
"transformed_value": 0,
"status": 0
'unsafe_shutdowns': {
'attribute_id': 'unsafe_shutdowns',
'value': 43,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"warning_temp_time": {
"attribute_id": "warning_temp_time",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'warning_temp_time': {
'attribute_id': 'warning_temp_time',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
}
},
"Status": 0
'Status': 0
}]
},
"metadata": {
"available_spare": {
"display_name": "Available Spare",
"ideal": "high",
"critical": true,
"description": "Contains a normalized percentage (0 to 100%) of the remaining spare capacity available.",
"display_type": ""
'metadata': {
'available_spare': {
'display_name': 'Available Spare',
'ideal': 'high',
'critical': true,
'description': 'Contains a normalized percentage (0 to 100%) of the remaining spare capacity available.',
'display_type': ''
},
"controller_busy_time": {
"display_name": "Controller Busy Time",
"ideal": "",
"critical": false,
"description": "Contains the amount of time the controller is busy with I/O commands. The controller is busy when there is a command outstanding to an I/O Queue (specifically, a command was issued via an I/O Submission Queue Tail doorbell write and the corresponding completion queue entry has not been posted yet to the associated I/O Completion Queue). This value is reported in minutes.",
"display_type": ""
'controller_busy_time': {
'display_name': 'Controller Busy Time',
'ideal': '',
'critical': false,
'description': 'Contains the amount of time the controller is busy with I/O commands. The controller is busy when there is a command outstanding to an I/O Queue (specifically, a command was issued via an I/O Submission Queue Tail doorbell write and the corresponding completion queue entry has not been posted yet to the associated I/O Completion Queue). This value is reported in minutes.',
'display_type': ''
},
"critical_comp_time": {
"display_name": "Critical CompTime",
"ideal": "",
"critical": false,
"description": "Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.",
"display_type": ""
'critical_comp_time': {
'display_name': 'Critical CompTime',
'ideal': '',
'critical': false,
'description': 'Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.',
'display_type': ''
},
"critical_warning": {
"display_name": "Critical Warning",
"ideal": "low",
"critical": true,
"description": "This field indicates critical warnings for the state of the controller. Each bit corresponds to a critical warning type; multiple bits may be set. If a bit is cleared to 0, then that critical warning does not apply. Critical warnings may result in an asynchronous event notification to the host. Bits in this field represent the current associated state and are not persistent.",
"display_type": ""
'critical_warning': {
'display_name': 'Critical Warning',
'ideal': 'low',
'critical': true,
'description': 'This field indicates critical warnings for the state of the controller. Each bit corresponds to a critical warning type; multiple bits may be set. If a bit is cleared to 0, then that critical warning does not apply. Critical warnings may result in an asynchronous event notification to the host. Bits in this field represent the current associated state and are not persistent.',
'display_type': ''
},
"data_units_read": {
"display_name": "Data Units Read",
"ideal": "",
"critical": false,
"description": "Contains the number of 512 byte data units the host has read from the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes read) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data read to 512 byte units.",
"display_type": ""
'data_units_read': {
'display_name': 'Data Units Read',
'ideal': '',
'critical': false,
'description': 'Contains the number of 512 byte data units the host has read from the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes read) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data read to 512 byte units.',
'display_type': ''
},
"data_units_written": {
"display_name": "Data Units Written",
"ideal": "",
"critical": false,
"description": "Contains the number of 512 byte data units the host has written to the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes written) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data written to 512 byte units.",
"display_type": ""
'data_units_written': {
'display_name': 'Data Units Written',
'ideal': '',
'critical': false,
'description': 'Contains the number of 512 byte data units the host has written to the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes written) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data written to 512 byte units.',
'display_type': ''
},
"host_reads": {
"display_name": "Host Reads",
"ideal": "",
"critical": false,
"description": "Contains the number of read commands completed by the controller",
"display_type": ""
'host_reads': {
'display_name': 'Host Reads',
'ideal': '',
'critical': false,
'description': 'Contains the number of read commands completed by the controller',
'display_type': ''
},
"host_writes": {
"display_name": "Host Writes",
"ideal": "",
"critical": false,
"description": "Contains the number of write commands completed by the controller",
"display_type": ""
'host_writes': {
'display_name': 'Host Writes',
'ideal': '',
'critical': false,
'description': 'Contains the number of write commands completed by the controller',
'display_type': ''
},
"media_errors": {
"display_name": "Media Errors",
"ideal": "low",
"critical": true,
"description": "Contains the number of occurrences where the controller detected an unrecovered data integrity error. Errors such as uncorrectable ECC, CRC checksum failure, or LBA tag mismatch are included in this field.",
"display_type": ""
'media_errors': {
'display_name': 'Media Errors',
'ideal': 'low',
'critical': true,
'description': 'Contains the number of occurrences where the controller detected an unrecovered data integrity error. Errors such as uncorrectable ECC, CRC checksum failure, or LBA tag mismatch are included in this field.',
'display_type': ''
},
"num_err_log_entries": {
"display_name": "Numb Err Log Entries",
"ideal": "low",
"critical": true,
"description": "Contains the number of Error Information log entries over the life of the controller.",
"display_type": ""
'num_err_log_entries': {
'display_name': 'Numb Err Log Entries',
'ideal': 'low',
'critical': true,
'description': 'Contains the number of Error Information log entries over the life of the controller.',
'display_type': ''
},
"percentage_used": {
"display_name": "Percentage Used",
"ideal": "low",
"critical": true,
"description": "Contains a vendor specific estimate of the percentage of NVM subsystem life used based on the actual usage and the manufacturers prediction of NVM life. A value of 100 indicates that the estimated endurance of the NVM in the NVM subsystem has been consumed, but may not indicate an NVM subsystem failure. The value is allowed to exceed 100. Percentages greater than 254 shall be represented as 255. This value shall be updated once per power-on hour (when the controller is not in a sleep state).",
"display_type": ""
'percentage_used': {
'display_name': 'Percentage Used',
'ideal': 'low',
'critical': true,
'description': 'Contains a vendor specific estimate of the percentage of NVM subsystem life used based on the actual usage and the manufacturers prediction of NVM life. A value of 100 indicates that the estimated endurance of the NVM in the NVM subsystem has been consumed, but may not indicate an NVM subsystem failure. The value is allowed to exceed 100. Percentages greater than 254 shall be represented as 255. This value shall be updated once per power-on hour (when the controller is not in a sleep state).',
'display_type': ''
},
"power_cycles": {
"display_name": "Power Cycles",
"ideal": "",
"critical": false,
"description": "Contains the number of power cycles.",
"display_type": ""
'power_cycles': {
'display_name': 'Power Cycles',
'ideal': '',
'critical': false,
'description': 'Contains the number of power cycles.',
'display_type': ''
},
"power_on_hours": {
"display_name": "Power on Hours",
"ideal": "",
"critical": false,
"description": "Contains the number of power-on hours. Power on hours is always logging, even when in low power mode.",
"display_type": ""
'power_on_hours': {
'display_name': 'Power on Hours',
'ideal': '',
'critical': false,
'description': 'Contains the number of power-on hours. Power on hours is always logging, even when in low power mode.',
'display_type': ''
},
"temperature": {
"display_name": "Temperature",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'temperature': {
'display_name': 'Temperature',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"unsafe_shutdowns": {
"display_name": "Unsafe Shutdowns",
"ideal": "",
"critical": false,
"description": "Contains the number of unsafe shutdowns. This count is incremented when a shutdown notification (CC.SHN) is not received prior to loss of power.",
"display_type": ""
'unsafe_shutdowns': {
'display_name': 'Unsafe Shutdowns',
'ideal': '',
'critical': false,
'description': 'Contains the number of unsafe shutdowns. This count is incremented when a shutdown notification (CC.SHN) is not received prior to loss of power.',
'display_type': ''
},
"warning_temp_time": {
"display_name": "Warning Temp Time",
"ideal": "",
"critical": false,
"description": "Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater than or equal to the Warning Composite Temperature Threshold (WCTEMP) field and less than the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.",
"display_type": ""
'warning_temp_time': {
'display_name': 'Warning Temp Time',
'ideal': '',
'critical': false,
'description': 'Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater than or equal to the Warning Composite Temperature Threshold (WCTEMP) field and less than the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.',
'display_type': ''
}
},
"success": true
'success': true
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,222 +1,222 @@
export const sdd = {
"data": {
"device": {
"CreatedAt": "2021-06-24T21:17:31.30374-07:00",
"UpdatedAt": "2021-10-24T16:37:57.013758-07:00",
"DeletedAt": null,
"wwn": "0x5000cca252c859cc",
"device_name": "sdd",
"manufacturer": "ATA",
"model_name": "WDC_WD80EFAX-68LHPN0",
"interface_type": "SCSI",
"interface_speed": "",
"serial_number": "7SGLXXXXX",
"firmware": "",
"rotational_speed": 0,
"capacity": 8001563222016,
"form_factor": "",
"smart_support": false,
"device_protocol": "SCSI",
"device_type": "",
"label": "",
"host_id": "",
"device_status": 0
'data': {
'device': {
'CreatedAt': '2021-06-24T21:17:31.30374-07:00',
'UpdatedAt': '2021-10-24T16:37:57.013758-07:00',
'DeletedAt': null,
'wwn': '0x5000cca252c859cc',
'device_name': 'sdd',
'manufacturer': 'ATA',
'model_name': 'WDC_WD80EFAX-68LHPN0',
'interface_type': 'SCSI',
'interface_speed': '',
'serial_number': '7SGLXXXXX',
'firmware': '',
'rotational_speed': 0,
'capacity': 8001563222016,
'form_factor': '',
'smart_support': false,
'device_protocol': 'SCSI',
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
},
"smart_results": [{
"date": "2021-10-24T23:20:44Z",
"device_wwn": "0x5000cca252c859cc",
"device_protocol": "SCSI",
"temp": 34,
"power_on_hours": 43549,
"power_cycle_count": 0,
"attrs": {
"read_correction_algorithm_invocations": {
"attribute_id": "read_correction_algorithm_invocations",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'smart_results': [{
'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5000cca252c859cc',
'device_protocol': 'SCSI',
'temp': 34,
'power_on_hours': 43549,
'power_cycle_count': 0,
'attrs': {
'read_correction_algorithm_invocations': {
'attribute_id': 'read_correction_algorithm_invocations',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_errors_corrected_by_eccdelayed": {
"attribute_id": "read_errors_corrected_by_eccdelayed",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'read_errors_corrected_by_eccdelayed': {
'attribute_id': 'read_errors_corrected_by_eccdelayed',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_errors_corrected_by_eccfast": {
"attribute_id": "read_errors_corrected_by_eccfast",
"value": 300357663,
"thresh": -1,
"transformed_value": 0,
"status": 0
'read_errors_corrected_by_eccfast': {
'attribute_id': 'read_errors_corrected_by_eccfast',
'value': 300357663,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_errors_corrected_by_rereads_rewrites": {
"attribute_id": "read_errors_corrected_by_rereads_rewrites",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'read_errors_corrected_by_rereads_rewrites': {
'attribute_id': 'read_errors_corrected_by_rereads_rewrites',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"read_total_errors_corrected": {
"attribute_id": "read_total_errors_corrected",
"value": 300357663,
"thresh": -1,
"transformed_value": 0,
"status": 0
'read_total_errors_corrected': {
'attribute_id': 'read_total_errors_corrected',
'value': 300357663,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_total_uncorrected_errors": {
"attribute_id": "read_total_uncorrected_errors",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'read_total_uncorrected_errors': {
'attribute_id': 'read_total_uncorrected_errors',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"scsi_grown_defect_list": {
"attribute_id": "scsi_grown_defect_list",
"value": 56,
"thresh": 0,
"transformed_value": 0,
"status": 0
'scsi_grown_defect_list': {
'attribute_id': 'scsi_grown_defect_list',
'value': 56,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"write_correction_algorithm_invocations": {
"attribute_id": "write_correction_algorithm_invocations",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_correction_algorithm_invocations': {
'attribute_id': 'write_correction_algorithm_invocations',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_errors_corrected_by_eccdelayed": {
"attribute_id": "write_errors_corrected_by_eccdelayed",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_errors_corrected_by_eccdelayed': {
'attribute_id': 'write_errors_corrected_by_eccdelayed',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_errors_corrected_by_eccfast": {
"attribute_id": "write_errors_corrected_by_eccfast",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_errors_corrected_by_eccfast': {
'attribute_id': 'write_errors_corrected_by_eccfast',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_errors_corrected_by_rereads_rewrites": {
"attribute_id": "write_errors_corrected_by_rereads_rewrites",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'write_errors_corrected_by_rereads_rewrites': {
'attribute_id': 'write_errors_corrected_by_rereads_rewrites',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"write_total_errors_corrected": {
"attribute_id": "write_total_errors_corrected",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_total_errors_corrected': {
'attribute_id': 'write_total_errors_corrected',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_total_uncorrected_errors": {
"attribute_id": "write_total_uncorrected_errors",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'write_total_uncorrected_errors': {
'attribute_id': 'write_total_uncorrected_errors',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
}
},
"Status": 0
'Status': 0
}]
},
"metadata": {
"read_correction_algorithm_invocations": {
"display_name": "Read Correction Algorithm Invocations",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'metadata': {
'read_correction_algorithm_invocations': {
'display_name': 'Read Correction Algorithm Invocations',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_errors_corrected_by_eccdelayed": {
"display_name": "Read Errors Corrected by ECC Delayed",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'read_errors_corrected_by_eccdelayed': {
'display_name': 'Read Errors Corrected by ECC Delayed',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_errors_corrected_by_eccfast": {
"display_name": "Read Errors Corrected by ECC Fast",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'read_errors_corrected_by_eccfast': {
'display_name': 'Read Errors Corrected by ECC Fast',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_errors_corrected_by_rereads_rewrites": {
"display_name": "Read Errors Corrected by ReReads/ReWrites",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'read_errors_corrected_by_rereads_rewrites': {
'display_name': 'Read Errors Corrected by ReReads/ReWrites',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"read_total_errors_corrected": {
"display_name": "Read Total Errors Corrected",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'read_total_errors_corrected': {
'display_name': 'Read Total Errors Corrected',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_total_uncorrected_errors": {
"display_name": "Read Total Uncorrected Errors",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'read_total_uncorrected_errors': {
'display_name': 'Read Total Uncorrected Errors',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"scsi_grown_defect_list": {
"display_name": "Grown Defect List",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'scsi_grown_defect_list': {
'display_name': 'Grown Defect List',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"write_correction_algorithm_invocations": {
"display_name": "Write Correction Algorithm Invocations",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_correction_algorithm_invocations': {
'display_name': 'Write Correction Algorithm Invocations',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_errors_corrected_by_eccdelayed": {
"display_name": "Write Errors Corrected by ECC Delayed",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_errors_corrected_by_eccdelayed': {
'display_name': 'Write Errors Corrected by ECC Delayed',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_errors_corrected_by_eccfast": {
"display_name": "Write Errors Corrected by ECC Fast",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_errors_corrected_by_eccfast': {
'display_name': 'Write Errors Corrected by ECC Fast',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_errors_corrected_by_rereads_rewrites": {
"display_name": "Write Errors Corrected by ReReads/ReWrites",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'write_errors_corrected_by_rereads_rewrites': {
'display_name': 'Write Errors Corrected by ReReads/ReWrites',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"write_total_errors_corrected": {
"display_name": "Write Total Errors Corrected",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_total_errors_corrected': {
'display_name': 'Write Total Errors Corrected',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_total_uncorrected_errors": {
"display_name": "Write Total Uncorrected Errors",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'write_total_uncorrected_errors': {
'display_name': 'Write Total Uncorrected Errors',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
}
},
"success": true
'success': true
}
@@ -1,222 +1,222 @@
export const sde = {
"data": {
"device": {
"CreatedAt": "2021-06-24T21:17:31.304461-07:00",
"UpdatedAt": "2021-10-24T16:40:16.495248-07:00",
"DeletedAt": null,
"wwn": "0x5000cca264ebc248",
"device_name": "sde",
"manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI",
"interface_speed": "",
"serial_number": "9RK3XXXXX",
"firmware": "",
"rotational_speed": 0,
"capacity": 14000519643136,
"form_factor": "",
"smart_support": false,
"device_protocol": "SCSI",
"device_type": "",
"label": "",
"host_id": "",
"device_status": 0
'data': {
'device': {
'CreatedAt': '2021-06-24T21:17:31.304461-07:00',
'UpdatedAt': '2021-10-24T16:40:16.495248-07:00',
'DeletedAt': null,
'wwn': '0x5000cca264ebc248',
'device_name': 'sde',
'manufacturer': 'ATA',
'model_name': 'WDC_WD140EDFZ-11A0VA0',
'interface_type': 'SCSI',
'interface_speed': '',
'serial_number': '9RK3XXXXX',
'firmware': '',
'rotational_speed': 0,
'capacity': 14000519643136,
'form_factor': '',
'smart_support': false,
'device_protocol': 'SCSI',
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
},
"smart_results": [{
"date": "2021-10-24T23:20:44Z",
"device_wwn": "0x5000cca264ebc248",
"device_protocol": "SCSI",
"temp": 31,
"power_on_hours": 5675,
"power_cycle_count": 0,
"attrs": {
"read_correction_algorithm_invocations": {
"attribute_id": "read_correction_algorithm_invocations",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'smart_results': [{
'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5000cca264ebc248',
'device_protocol': 'SCSI',
'temp': 31,
'power_on_hours': 5675,
'power_cycle_count': 0,
'attrs': {
'read_correction_algorithm_invocations': {
'attribute_id': 'read_correction_algorithm_invocations',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_errors_corrected_by_eccdelayed": {
"attribute_id": "read_errors_corrected_by_eccdelayed",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'read_errors_corrected_by_eccdelayed': {
'attribute_id': 'read_errors_corrected_by_eccdelayed',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_errors_corrected_by_eccfast": {
"attribute_id": "read_errors_corrected_by_eccfast",
"value": 1410362924,
"thresh": -1,
"transformed_value": 0,
"status": 0
'read_errors_corrected_by_eccfast': {
'attribute_id': 'read_errors_corrected_by_eccfast',
'value': 1410362924,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_errors_corrected_by_rereads_rewrites": {
"attribute_id": "read_errors_corrected_by_rereads_rewrites",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'read_errors_corrected_by_rereads_rewrites': {
'attribute_id': 'read_errors_corrected_by_rereads_rewrites',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"read_total_errors_corrected": {
"attribute_id": "read_total_errors_corrected",
"value": 1410362924,
"thresh": -1,
"transformed_value": 0,
"status": 0
'read_total_errors_corrected': {
'attribute_id': 'read_total_errors_corrected',
'value': 1410362924,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"read_total_uncorrected_errors": {
"attribute_id": "read_total_uncorrected_errors",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'read_total_uncorrected_errors': {
'attribute_id': 'read_total_uncorrected_errors',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"scsi_grown_defect_list": {
"attribute_id": "scsi_grown_defect_list",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'scsi_grown_defect_list': {
'attribute_id': 'scsi_grown_defect_list',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"write_correction_algorithm_invocations": {
"attribute_id": "write_correction_algorithm_invocations",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_correction_algorithm_invocations': {
'attribute_id': 'write_correction_algorithm_invocations',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_errors_corrected_by_eccdelayed": {
"attribute_id": "write_errors_corrected_by_eccdelayed",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_errors_corrected_by_eccdelayed': {
'attribute_id': 'write_errors_corrected_by_eccdelayed',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_errors_corrected_by_eccfast": {
"attribute_id": "write_errors_corrected_by_eccfast",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_errors_corrected_by_eccfast': {
'attribute_id': 'write_errors_corrected_by_eccfast',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_errors_corrected_by_rereads_rewrites": {
"attribute_id": "write_errors_corrected_by_rereads_rewrites",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'write_errors_corrected_by_rereads_rewrites': {
'attribute_id': 'write_errors_corrected_by_rereads_rewrites',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
},
"write_total_errors_corrected": {
"attribute_id": "write_total_errors_corrected",
"value": 0,
"thresh": -1,
"transformed_value": 0,
"status": 0
'write_total_errors_corrected': {
'attribute_id': 'write_total_errors_corrected',
'value': 0,
'thresh': -1,
'transformed_value': 0,
'status': 0
},
"write_total_uncorrected_errors": {
"attribute_id": "write_total_uncorrected_errors",
"value": 0,
"thresh": 0,
"transformed_value": 0,
"status": 0
'write_total_uncorrected_errors': {
'attribute_id': 'write_total_uncorrected_errors',
'value': 0,
'thresh': 0,
'transformed_value': 0,
'status': 0
}
},
"Status": 0
'Status': 0
}]
},
"metadata": {
"read_correction_algorithm_invocations": {
"display_name": "Read Correction Algorithm Invocations",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'metadata': {
'read_correction_algorithm_invocations': {
'display_name': 'Read Correction Algorithm Invocations',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_errors_corrected_by_eccdelayed": {
"display_name": "Read Errors Corrected by ECC Delayed",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'read_errors_corrected_by_eccdelayed': {
'display_name': 'Read Errors Corrected by ECC Delayed',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_errors_corrected_by_eccfast": {
"display_name": "Read Errors Corrected by ECC Fast",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'read_errors_corrected_by_eccfast': {
'display_name': 'Read Errors Corrected by ECC Fast',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_errors_corrected_by_rereads_rewrites": {
"display_name": "Read Errors Corrected by ReReads/ReWrites",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'read_errors_corrected_by_rereads_rewrites': {
'display_name': 'Read Errors Corrected by ReReads/ReWrites',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"read_total_errors_corrected": {
"display_name": "Read Total Errors Corrected",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'read_total_errors_corrected': {
'display_name': 'Read Total Errors Corrected',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"read_total_uncorrected_errors": {
"display_name": "Read Total Uncorrected Errors",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'read_total_uncorrected_errors': {
'display_name': 'Read Total Uncorrected Errors',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"scsi_grown_defect_list": {
"display_name": "Grown Defect List",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'scsi_grown_defect_list': {
'display_name': 'Grown Defect List',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"write_correction_algorithm_invocations": {
"display_name": "Write Correction Algorithm Invocations",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_correction_algorithm_invocations': {
'display_name': 'Write Correction Algorithm Invocations',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_errors_corrected_by_eccdelayed": {
"display_name": "Write Errors Corrected by ECC Delayed",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_errors_corrected_by_eccdelayed': {
'display_name': 'Write Errors Corrected by ECC Delayed',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_errors_corrected_by_eccfast": {
"display_name": "Write Errors Corrected by ECC Fast",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_errors_corrected_by_eccfast': {
'display_name': 'Write Errors Corrected by ECC Fast',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_errors_corrected_by_rereads_rewrites": {
"display_name": "Write Errors Corrected by ReReads/ReWrites",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'write_errors_corrected_by_rereads_rewrites': {
'display_name': 'Write Errors Corrected by ReReads/ReWrites',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
},
"write_total_errors_corrected": {
"display_name": "Write Total Errors Corrected",
"ideal": "",
"critical": false,
"description": "",
"display_type": ""
'write_total_errors_corrected': {
'display_name': 'Write Total Errors Corrected',
'ideal': '',
'critical': false,
'description': '',
'display_type': ''
},
"write_total_uncorrected_errors": {
"display_name": "Write Total Uncorrected Errors",
"ideal": "low",
"critical": true,
"description": "",
"display_type": ""
'write_total_uncorrected_errors': {
'display_name': 'Write Total Uncorrected Errors',
'ideal': 'low',
'critical': true,
'description': '',
'display_type': ''
}
},
"success": true
'success': true
}
@@ -1,29 +1,29 @@
export const sdf = {
"data": {
"device": {
"CreatedAt": "2021-06-24T21:17:31.305246-07:00",
"UpdatedAt": "2021-06-24T21:17:31.305246-07:00",
"DeletedAt": null,
"wwn": "0x50014ee20b2a72a9",
"device_name": "sdf",
"manufacturer": "ATA",
"model_name": "WDC_WD60EFRX-68MYMN1",
"interface_type": "SCSI",
"interface_speed": "",
"serial_number": "WD-WXL1HXXXXX",
"firmware": "",
"rotational_speed": 0,
"capacity": 6001175126016,
"form_factor": "",
"smart_support": false,
"device_protocol": "",
"device_type": "",
"label": "",
"host_id": "",
"device_status": 0
'data': {
'device': {
'CreatedAt': '2021-06-24T21:17:31.305246-07:00',
'UpdatedAt': '2021-06-24T21:17:31.305246-07:00',
'DeletedAt': null,
'wwn': '0x50014ee20b2a72a9',
'device_name': 'sdf',
'manufacturer': 'ATA',
'model_name': 'WDC_WD60EFRX-68MYMN1',
'interface_type': 'SCSI',
'interface_speed': '',
'serial_number': 'WD-WXL1HXXXXX',
'firmware': '',
'rotational_speed': 0,
'capacity': 6001175126016,
'form_factor': '',
'smart_support': false,
'device_protocol': '',
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
},
"smart_results": []
'smart_results': []
},
"metadata": null,
"success": true
'metadata': null,
'success': true
}
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
import { Component, OnInit, Inject } from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogService} from "./dashboard-device-delete-dialog.service";
import {Subject} from "rxjs";
import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service';
import {Subject} from 'rxjs';
@Component({
selector: 'app-dashboard-device-delete-dialog',
@@ -9,18 +9,18 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'
import { MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatTabsModule} from "@angular/material/tabs";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatTooltipModule} from "@angular/material/tooltip";
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
import {MatDividerModule} from "@angular/material/divider";
import {MatMenuModule} from "@angular/material/menu";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatSortModule} from "@angular/material/sort";
import {MatTableModule} from "@angular/material/table";
import {NgApexchartsModule} from "ng-apexcharts";
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing';
import {MatDividerModule} from '@angular/material/divider';
import {MatMenuModule} from '@angular/material/menu';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatTableModule} from '@angular/material/table';
import {NgApexchartsModule} from 'ng-apexcharts';
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
@@ -1,13 +1,13 @@
import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
import * as moment from "moment";
import {takeUntil} from "rxjs/operators";
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Subject} from "rxjs";
import * as moment from 'moment';
import {takeUntil} from 'rxjs/operators';
import {AppConfig} from 'app/core/config/app.config';
import {TreoConfigService} from '@treo/services/config';
import {Subject} from 'rxjs';
import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from "app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component";
import {DeviceTitlePipe} from "app/shared/device-title.pipe";
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
@Component({
selector: 'app-dashboard-device',
@@ -15,13 +15,6 @@ import {DeviceTitlePipe} from "app/shared/device-title.pipe";
styleUrls: ['./dashboard-device.component.scss']
})
export class DashboardDeviceComponent implements OnInit {
@Input() deviceSummary: any;
@Input() deviceWWN: string;
@Output() deviceDeleted = new EventEmitter<string>();
config: AppConfig;
private _unsubscribeAll: Subject<any>;
constructor(
private _configService: TreoConfigService,
@@ -30,6 +23,15 @@ export class DashboardDeviceComponent implements OnInit {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
@Input() deviceSummary: any;
@Input() deviceWWN: string;
@Output() deviceDeleted = new EventEmitter<string>();
config: AppConfig;
private _unsubscribeAll: Subject<any>;
readonly humanizeDuration = humanizeDuration;
ngOnInit(): void {
// Subscribe to config changes
@@ -45,7 +47,7 @@ export class DashboardDeviceComponent implements OnInit {
// @ Public methods
// -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary){
classDeviceLastUpdatedOn(deviceSummary): string {
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
@@ -65,16 +67,14 @@ export class DashboardDeviceComponent implements OnInit {
}
}
deviceStatusString(deviceStatus){
if(deviceStatus == 0){
return "passed"
deviceStatusString(deviceStatus): string {
if(deviceStatus === 0){
return 'passed'
} else {
return "failed"
return 'failed'
}
}
readonly humanizeDuration = humanizeDuration;
openDeleteDialog(): void {
@@ -9,20 +9,20 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
import { MatDialogModule } from "@angular/material/dialog";
import { MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatTabsModule} from "@angular/material/tabs";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatTooltipModule} from "@angular/material/tooltip";
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
import {MatDividerModule} from "@angular/material/divider";
import {MatMenuModule} from "@angular/material/menu";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatSortModule} from "@angular/material/sort";
import {MatTableModule} from "@angular/material/table";
import {NgApexchartsModule} from "ng-apexcharts";
import {DashboardDeviceDeleteDialogModule} from "../dashboard-device-delete-dialog/dashboard-device-delete-dialog.module";
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
import {MatDividerModule} from '@angular/material/divider';
import {MatMenuModule} from '@angular/material/menu';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatTableModule} from '@angular/material/table';
import {NgApexchartsModule} from 'ng-apexcharts';
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
@NgModule({
declarations: [
@@ -1,8 +1,8 @@
import { Component, OnInit } from '@angular/core';
import {AppConfig} from 'app/core/config/app.config';
import { TreoConfigService } from '@treo/services/config';
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
@Component({
selector: 'app-dashboard-settings',
@@ -9,12 +9,12 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component'
import { MatDialogModule } from "@angular/material/dialog";
import { MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatTabsModule} from "@angular/material/tabs";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatTooltipModule} from "@angular/material/tooltip";
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
@NgModule({
declarations: [
@@ -9,12 +9,12 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'
import { MatDialogModule } from "@angular/material/dialog";
import { MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatTabsModule} from "@angular/material/tabs";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatTooltipModule} from "@angular/material/tooltip";
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
@NgModule({
declarations: [
@@ -45,7 +45,7 @@ export class LayoutComponent implements OnInit, OnDestroy
// Set the private defaults
this._unsubscribeAll = new Subject();
this.systemPrefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
@@ -7,11 +7,11 @@ import {ApexOptions, ChartComponent} from 'ng-apexcharts';
import { DashboardService } from 'app/modules/dashboard/dashboard.service';
import {MatDialog} from '@angular/material/dialog';
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Router} from "@angular/router";
import {TemperaturePipe} from "app/shared/temperature.pipe";
import {DeviceTitlePipe} from "app/shared/device-title.pipe";
import {AppConfig} from 'app/core/config/app.config';
import {TreoConfigService} from '@treo/services/config';
import {Router} from '@angular/router';
import {TemperaturePipe} from 'app/shared/temperature.pipe';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
@Component({
selector : 'example',
@@ -25,12 +25,12 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
data: any;
hostGroups: { [hostId: string]: string[] } = {}
temperatureOptions: ApexOptions;
tempDurationKey: string = "forever"
tempDurationKey = 'forever'
config: AppConfig;
// Private
private _unsubscribeAll: Subject<any>;
@ViewChild("tempChart", { static: false }) tempChart: ChartComponent;
@ViewChild('tempChart', { static: false }) tempChart: ChartComponent;
/**
* Constructor
@@ -64,17 +64,17 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
//check if the old config and the new config do not match.
let oldConfig = JSON.stringify(this.config)
let newConfig = JSON.stringify(config)
// check if the old config and the new config do not match.
const oldConfig = JSON.stringify(this.config)
const newConfig = JSON.stringify(config)
if(oldConfig != newConfig){
if(oldConfig !== newConfig){
console.log(`Configuration updated: ${newConfig} vs ${oldConfig}`)
// Store the config
this.config = config;
if(oldConfig){
console.log("reloading component...")
console.log('reloading component...')
this.refreshComponent()
}
}
@@ -88,10 +88,10 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// Store the data
this.data = data;
//generate group data.
for(let wwn in this.data.data.summary){
let hostid = this.data.data.summary[wwn].device.host_id
let hostDeviceList = this.hostGroups[hostid] || []
// generate group data.
for(const wwn in this.data.data.summary){
const hostid = this.data.data.summary[wwn].device.host_id
const hostDeviceList = this.hostGroups[hostid] || []
hostDeviceList.push(wwn)
this.hostGroups[hostid] = hostDeviceList
}
@@ -121,34 +121,34 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
private refreshComponent(){
private refreshComponent(): void {
let currentUrl = this.router.url;
const currentUrl = this.router.url;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.router.navigate([currentUrl]);
}
private _deviceDataTemperatureSeries() {
var deviceTemperatureSeries = []
private _deviceDataTemperatureSeries(): any[] {
const deviceTemperatureSeries = []
console.log("DEVICE DATA SUMMARY", this.data)
console.log('DEVICE DATA SUMMARY', this.data)
for(const wwn in this.data.data.summary){
var deviceSummary = this.data.data.summary[wwn]
const deviceSummary = this.data.data.summary[wwn]
if (!deviceSummary.temp_history){
continue
}
let deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
var deviceSeriesMetadata = {
const deviceSeriesMetadata = {
name: deviceName,
data: []
}
for(let tempHistory of deviceSummary.temp_history){
let newDate = new Date(tempHistory.date);
for(const tempHistory of deviceSummary.temp_history){
const newDate = new Date(tempHistory.date);
deviceSeriesMetadata.data.push({
x: newDate,
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperatureUnit, false)
@@ -216,9 +216,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// @ Public methods
// -----------------------------------------------------------------------------------------------------
deviceSummariesForHostGroup(hostGroupWWNs: string[]) {
let deviceSummaries = []
for(let wwn of hostGroupWWNs){
deviceSummariesForHostGroup(hostGroupWWNs: string[]): any[] {
const deviceSummaries = []
for(const wwn of hostGroupWWNs){
if(this.data.data.summary[wwn]){
deviceSummaries.push(this.data.data.summary[wwn])
}
@@ -226,7 +226,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
return deviceSummaries
}
openDialog() {
openDialog(): void {
const dialogRef = this.dialog.open(DashboardSettingsComponent);
dialogRef.afterClosed().subscribe(result => {
@@ -234,7 +234,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
});
}
onDeviceDeleted(wwn: string) {
onDeviceDeleted(wwn: string): void {
delete this.data.data.summary[wwn] // remove the device from the summary list.
}
@@ -12,8 +12,8 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { NgApexchartsModule } from 'ng-apexcharts';
import { MatTooltipModule } from '@angular/material/tooltip'
import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module";
import { DashboardDeviceModule } from "app/layout/common/dashboard-device/dashboard-device.module";
import { DashboardSettingsModule } from 'app/layout/common/dashboard-settings/dashboard-settings.module';
import { DashboardDeviceModule } from 'app/layout/common/dashboard-device/dashboard-device.module';
@NgModule({
declarations: [
@@ -1,6 +1,6 @@
import { Route } from '@angular/router';
import { DashboardComponent } from 'app/modules/dashboard/dashboard.component';
import {DashboardResolver} from "./dashboard.resolvers";
import {DashboardResolver} from 'app/modules/dashboard/dashboard.resolvers';
export const dashboardRoutes: Route[] = [
{
@@ -55,9 +55,9 @@ export class DashboardService
getSummaryTempData(durationKey: string): Observable<any>
{
let params = {}
const params = {}
if(durationKey){
params["duration_key"] = durationKey
params['duration_key'] = durationKey
}
return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
@@ -143,6 +143,7 @@
<table class="w-full bg-transparent"
mat-table
matSort
multiTemplateDataRows
[dataSource]="smartAttributeDataSource"
[trackBy]="trackByFn"
#smartAttributeTable>
@@ -203,7 +204,7 @@
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 whitespace-no-wrap" matTooltip="{{getAttributeDescription(attribute)}}">
<span class="pr-6 whitespace-no-wrap" matTooltip="click for more details.">
{{getAttributeName(attribute)}} <mat-icon *ngIf="getAttributeDescription(attribute)" class="icon-size-10" [svgIcon]="'info'"></mat-icon>
</span>
</td>
@@ -324,6 +325,72 @@
</td>
</ng-container>
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let attribute" [attr.colspan]="smartAttributeTableColumns.length">
<div class="attribute-detail"
[@detailExpand]="attribute == expandedAttribute ? 'expanded' : 'collapsed'">
<div class="flex flex-auto w-1/3 min-w-80 py-4">
<div class="flex flex-col flex-auto justify-end text-md pb-3">
{{getAttributeDescription(attribute)}}
</div>
</div>
<div class="flex flex-auto w-2/3 min-w-80">
<div class="flex flex-col flex-auto justify-end text-md px-6 pb-3">
<div class="flex items-center justify-between py-3 border-b last:border-b-0 ng-star-inserted">
<div class="flex items-center w-1/4">Type</div>
<div class="flex items-center w-1/4">Value</div>
<div class="flex items-center w-1/4">Worst/Thresh</div>
<div class="flex items-center w-1/4">Failure %</div>
</div>
<div class="flex items-center justify-between py-3 border-b last:border-b-0 ng-star-inserted">
<div class="flex items-center w-1/4">
<div class="flex-shrink-0 w-2 h-2 mr-3 rounded-full"
[ngClass]="{'bg-red': getAttributeScrutinyStatusName(attribute.status) === 'failed',
'bg-green': getAttributeScrutinyStatusName(attribute.status) === 'passed',
'bg-yellow': getAttributeScrutinyStatusName(attribute.status) === 'warn'}"></div>
<div class="truncate">Scrutiny</div>
</div>
<div class="w-1/4 items-center font-medium">{{getAttributeValue(attribute)}}</div>
<div class="w-1/4 items-center text-secondary">--</div>
<div class="w-1/4 items-center text-secondary">{{(attribute.failure_rate | percent) || '--'}}</div>
</div>
<div class="flex items-center justify-between py-3 border-b last:border-b-0 ng-star-inserted">
<div class="flex items-center w-1/4">
<div class="flex-shrink-0 w-2 h-2 mr-3 rounded-full"
[ngClass]="{'bg-red': getAttributeSmartStatusName(attribute.status) === 'failed',
'bg-green': getAttributeSmartStatusName(attribute.status) === 'passed'}"
></div>
<div class="truncate">Normalized</div>
</div>
<div class="w-1/4 items-center font-medium">{{attribute.value}}</div>
<div class="w-1/4 items-center text-secondary">{{getAttributeWorst(attribute) || '--' }}/{{getAttributeThreshold(attribute)}}</div>
<div class="w-1/4 items-center text-secondary">--</div>
</div>
<div class="flex items-center justify-between py-3 border-b last:border-b-0 ng-star-inserted">
<div class="flex items-center w-1/4">
<div class="flex-shrink-0 w-2 h-2 mr-3 rounded-full"></div>
<div class="truncate">Raw</div>
</div>
<div class="w-1/4 items-center font-medium">{{attribute.raw_value}}</div>
<div class="w-1/4 items-center text-secondary">--</div>
<div class="w-1/4 items-center text-secondary">--</div>
</div>
</div>
</div>
</div>
</td>
</ng-container>
<!-- Footer -->
<ng-container matColumnDef="recentOrdersTableFooter">
<td class="px-3 border-none"
@@ -344,7 +411,10 @@
<tr class="attribute-row h-16"
mat-row
[ngClass]="{'yellow-50': getAttributeCritical(row)}"
[class.attribute-expanded-row]="expandedAttribute === row"
(click)="expandedAttribute = expandedAttribute === row ? null : row"
*matRowDef="let row; columns: smartAttributeTableColumns;"></tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="attribute-detail-row"></tr>
<tr class="h-16"
mat-footer-row
*matFooterRowDef="['recentOrdersTableFooter']"></tr>
@@ -1,7 +1,6 @@
@import 'treo';
detail {
}
// -----------------------------------------------------------------------------------------------------
@@ -20,5 +19,35 @@ detail {
}
}
//table {
// width: 100%;
//}
$primary: map-get($theme, primary);
$is-dark: map-get($theme, is-dark);
tr.attribute-detail-row {
height: 0;
}
//tr.attribute-row:not(.attribute-expanded-row):hover {
// @if ($is-dark) {
// background: rgba(0, 0, 0, 0.05);
// } @else {
// background: map-get($primary, 50);
// }
//}
tr.attribute-row:not(.attribute-expanded-row):active {
background: #efefef;
}
.attribute-row td {
border-bottom-width: 0;
}
.attribute-detail {
overflow: hidden;
display: flex;
}
@@ -1,45 +1,41 @@
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ApexOptions} from "ng-apexcharts";
import {MatTableDataSource} from "@angular/material/table";
import {MatSort} from "@angular/material/sort";
import {Subject} from "rxjs";
import {DetailService} from "./detail.service";
import {takeUntil} from "rxjs/operators";
import {fadeOut} from "../../../@treo/animations/fade";
import {DetailSettingsComponent} from "app/layout/common/detail-settings/detail-settings.component";
import {MatDialog} from "@angular/material/dialog";
import {ApexOptions} from 'ng-apexcharts';
import {MatTableDataSource} from '@angular/material/table';
import {MatSort} from '@angular/material/sort';
import {Subject} from 'rxjs';
import {DetailService} from './detail.service';
import {takeUntil} from 'rxjs/operators';
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component';
import {MatDialog} from '@angular/material/dialog';
import humanizeDuration from 'humanize-duration';
import {TreoConfigService} from "../../../@treo/services/config";
import {AppConfig} from "../../core/config/app.config";
import {TreoConfigService} from '@treo/services/config';
import {AppConfig} from 'app/core/config/app.config';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {formatDate} from '@angular/common';
import { LOCALE_ID, Inject } from '@angular/core';
// from Constants.go - these must match
const AttributeStatusPassed = 0
const AttributeStatusFailedSmart = 1
const AttributeStatusWarningScrutiny = 2
const AttributeStatusFailedScrutiny = 4
@Component({
selector: 'detail',
templateUrl: './detail.component.html',
styleUrls: ['./detail.component.scss']
styleUrls: ['./detail.component.scss'],
animations: [
trigger('detailExpand', [
state('collapsed', style({height: '0px', minHeight: '0'})),
state('expanded', style({height: '*'})),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
})
export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
config: AppConfig;
onlyCritical: boolean = true;
// data: any;
metadata: any;
device: any;
smart_results: any[];
commonSparklineOptions: Partial<ApexOptions>;
smartAttributeDataSource: MatTableDataSource<any>;
smartAttributeTableColumns: string[];
@ViewChild('smartAttributeTable', {read: MatSort})
smartAttributeTableMatSort: MatSort;
// Private
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
@@ -49,7 +45,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
private _detailService: DetailService,
public dialog: MatDialog,
private _configService: TreoConfigService,
@Inject(LOCALE_ID) public locale: string
)
{
@@ -60,8 +56,34 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
this.smartAttributeDataSource = new MatTableDataSource();
// this.recentTransactionsTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh'];
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history'];
this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
config: AppConfig;
onlyCritical = true;
// data: any;
expandedAttribute: any | null;
metadata: any;
device: any;
smart_results: any[];
commonSparklineOptions: Partial<ApexOptions>;
smartAttributeDataSource: MatTableDataSource<any>;
smartAttributeTableColumns: string[];
@ViewChild('smartAttributeTable', {read: MatSort})
smartAttributeTableMatSort: MatSort;
// Private
private _unsubscribeAll: Subject<any>;
private systemPrefersDark: boolean;
readonly humanizeDuration = humanizeDuration;
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
@@ -121,29 +143,46 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
getAttributeStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise
// from Constants.go
// AttributeStatusPassed AttributeStatus = 0
// AttributeStatusFailedSmart AttributeStatus = 1
// AttributeStatusWarningScrutiny AttributeStatus = 2
// AttributeStatusFailedScrutiny AttributeStatus = 4
if(attributeStatus === 0){
if(attributeStatus === AttributeStatusPassed){
return 'passed'
} else if ((attributeStatus & 1) !== 0 || (attributeStatus & 4) !== 0 ){
} else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0 ){
return 'failed'
} else if ((attributeStatus & 2) !== 0){
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){
return 'warn'
}
return ''
// tslint:enable:no-bitwise
}
getAttributeScrutinyStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise
if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0){
return 'failed'
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){
return 'warn'
} else {
return 'passed'
}
// tslint:enable:no-bitwise
}
getAttributeSmartStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise
if ((attributeStatus & AttributeStatusFailedSmart) !== 0){
return 'failed'
} else {
return 'passed'
}
// tslint:enable:no-bitwise
}
getAttributeName(attribute_data): string {
let attribute_metadata = this.metadata[attribute_data.attribute_id]
const attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){
return 'Unknown Attribute Name'
} else {
@@ -151,7 +190,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
getAttributeDescription(attribute_data){
let attribute_metadata = this.metadata[attribute_data.attribute_id]
const attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){
return 'Unknown'
} else {
@@ -162,12 +201,12 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
getAttributeValue(attribute_data){
if(this.isAta()) {
let attribute_metadata = this.metadata[attribute_data.attribute_id]
const attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){
return attribute_data.value
} else if (attribute_metadata.display_type == "raw") {
} else if (attribute_metadata.display_type == 'raw') {
return attribute_data.raw_value
} else if (attribute_metadata.display_type == "transformed" && attribute_data.transformed_value) {
} else if (attribute_metadata.display_type == 'transformed' && attribute_data.transformed_value) {
return attribute_data.transformed_value
} else {
return attribute_data.value
@@ -180,7 +219,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
getAttributeValueType(attribute_data){
if(this.isAta()) {
let attribute_metadata = this.metadata[attribute_data.attribute_id]
const attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){
return ''
} else {
@@ -193,25 +232,25 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
getAttributeIdeal(attribute_data){
if(this.isAta()){
return this.metadata[attribute_data.attribute_id]?.display_type == "raw" ? this.metadata[attribute_data.attribute_id]?.ideal : ''
return this.metadata[attribute_data.attribute_id]?.display_type == 'raw' ? this.metadata[attribute_data.attribute_id]?.ideal : ''
} else {
return this.metadata[attribute_data.attribute_id]?.ideal
}
}
getAttributeWorst(attribute_data){
let attribute_metadata = this.metadata[attribute_data.attribute_id]
const attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){
return attribute_data.worst
} else {
return attribute_metadata?.display_type == "normalized" ? attribute_data.worst : ''
return attribute_metadata?.display_type == 'normalized' ? attribute_data.worst : ''
}
}
getAttributeThreshold(attribute_data){
if(this.isAta()){
let attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata || attribute_metadata.display_type == "normalized"){
const attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata || attribute_metadata.display_type == 'normalized'){
return attribute_data.thresh
} else {
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
@@ -235,7 +274,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
}
let attributes_length = 0
let attributes = this.smart_results[0]?.attrs
const attributes = this.smart_results[0]?.attrs
if (attributes) {
attributes_length = Object.keys(attributes).length
}
@@ -254,12 +293,12 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
}
private _generateSmartAttributeTableDataSource(smart_results){
var smartAttributeDataSource = [];
const smartAttributeDataSource = [];
if(smart_results.length == 0){
return smartAttributeDataSource
}
var latest_smart_result = smart_results[0];
const latest_smart_result = smart_results[0];
let attributes = {}
if(this.isScsi()) {
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history'];
@@ -268,21 +307,35 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history'];
attributes = latest_smart_result.attrs
} else {
//ATA
// ATA
attributes = latest_smart_result.attrs
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history'];
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh','ideal', 'failure', 'history'];
}
for(const attrId in attributes){
var attr = attributes[attrId]
const attr = attributes[attrId]
//chart history data
// chart history data
if (!attr.chartData) {
var attrHistory = []
for (let smart_result of smart_results){
attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId]))
const attrHistory = []
for (const smart_result of smart_results){
// attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId]))
const chartDatapoint = {
x: formatDate(smart_result.date, 'MMMM dd, yyyy - HH:mm', this.locale),
y: this.getAttributeValue(smart_result.attrs[attrId])
}
const attributeStatusName = this.getAttributeStatusName(smart_result.attrs[attrId].status)
if(attributeStatusName === 'failed') {
chartDatapoint['strokeColor'] = '#F05252'
chartDatapoint['fillColor'] = '#F05252'
} else if (attributeStatusName === 'warn'){
chartDatapoint['strokeColor'] = '#C27803'
chartDatapoint['fillColor'] = '#C27803'
}
attrHistory.push(chartDatapoint)
}
// var rawHistory = (attr.history || []).map(hist_attr => this.getAttributeValue(hist_attr)).reverse()
@@ -290,12 +343,12 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
attributes[attrId].chartData = [
{
name: "chart-line-sparkline",
name: 'chart-line-sparkline',
data: attrHistory
}
]
}
//determine when to include the attributes in table.
// determine when to include the attributes in table.
if(!this.onlyCritical || this.onlyCritical && this.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh){
smartAttributeDataSource.push(attr)
@@ -315,7 +368,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
// Account balance
this.commonSparklineOptions = {
chart: {
type: "bar",
type: 'bar',
width: 100,
height: 25,
sparkline: {
@@ -325,23 +378,30 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
enabled: false
}
},
// theme:{
// // @ts-ignore
// // mode:
// mode: 'dark',
// },
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
show: true
},
y: {
title: {
formatter: function(seriesName) {
return "";
return '';
}
}
},
marker: {
show: false
}
},
theme: this.determineTheme(this.config)
},
stroke: {
width: 2,
@@ -350,12 +410,19 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
};
}
private determineTheme(config:AppConfig): string {
if (config.theme === 'system') {
return this.systemPrefersDark ? 'dark' : 'light'
} else {
return config.theme
}
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
toHex(decimalNumb){
return "0x" + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
}
toggleOnlyCritical(){
this.onlyCritical = !this.onlyCritical
@@ -383,6 +450,4 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
// return item.id || index;
}
readonly humanizeDuration = humanizeDuration;
}
@@ -13,7 +13,7 @@ import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip'
import { NgApexchartsModule } from 'ng-apexcharts';
import { TreoCardModule } from '@treo/components/card';
import {DetailSettingsModule} from "app/layout/common/detail-settings/detail-settings.module";
import {DetailSettingsModule} from 'app/layout/common/detail-settings/detail-settings.module';
@NgModule({
declarations: [
@@ -1,6 +1,6 @@
import { Route } from '@angular/router';
import { DetailComponent } from 'app/modules/detail/detail.component';
import {DetailResolver} from "./detail.resolvers";
import {DetailResolver} from './detail.resolvers';
export const detailRoutes: Route[] = [
{
@@ -1,33 +1,33 @@
import { Pipe, PipeTransform } from '@angular/core';
import {DeviceTitlePipe} from "./device-title.pipe";
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
@Pipe({
name: 'deviceSort'
})
export class DeviceSortPipe implements PipeTransform {
statusCompareFn(a: any, b: any) {
statusCompareFn(a: any, b: any): number {
function deviceStatus(deviceSummary): number {
if(!deviceSummary.smart){
return 0
} else if (deviceSummary.device.device_status == 0){
} else if (deviceSummary.device.device_status === 0){
return 1
} else {
return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3
}
}
let left = deviceStatus(a)
let right = deviceStatus(b)
const left = deviceStatus(a)
const right = deviceStatus(b)
return left - right;
}
titleCompareFn(dashboardDisplay: string) {
return function (a: any, b: any){
let _dashboardDisplay = dashboardDisplay
let left = DeviceTitlePipe.deviceTitleForType(a.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(a.device, 'name')
let right = DeviceTitlePipe.deviceTitleForType(b.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(b.device, 'name')
const _dashboardDisplay = dashboardDisplay
const left = DeviceTitlePipe.deviceTitleForType(a.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(a.device, 'name')
const right = DeviceTitlePipe.deviceTitleForType(b.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(b.device, 'name')
if( left < right )
return -1;
@@ -39,7 +39,7 @@ export class DeviceSortPipe implements PipeTransform {
}
}
ageCompareFn(a: any, b: any) {
ageCompareFn(a: any, b: any): number {
const left = a.smart?.power_on_hours
const right = b.smart?.power_on_hours
@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import {formatNumber} from "@angular/common";
import {formatNumber} from '@angular/common';
@Pipe({
name: 'temperature'
-4
View File
@@ -17,8 +17,4 @@
color: #0694a2 !important
}
}
.apexcharts-tooltip {
background: #242b38 !important;
//color: orange;
}
}