Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions webapp/backend/pkg/database/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type DeviceRepo interface {
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error)
GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error)
UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error
UpdateDeviceMuted(ctx context.Context, wwn string, archived bool) error
DeleteDevice(ctx context.Context, wwn string) error

SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
)

type Device struct {
Archived bool `json:"archived"`
//GORM attributes, see: http://gorm.io/docs/conventions.html
Archived bool `json:"archived"`
Muted bool `json:muted`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Expand Down
43 changes: 43 additions & 0 deletions webapp/backend/pkg/database/migrations/m20251108044508/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package m20251108044508

import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"time"
)


type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
Archived bool `json:"archived"`
Muted bool `json:muted`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time

WWN string `json:"wwn" gorm:"primary_key"`

DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`

Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.

// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`

// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
14 changes: 14 additions & 0 deletions webapp/backend/pkg/database/mock/mock_database.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions webapp/backend/pkg/database/scrutiny_repository_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn stri
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error
}

// Update Device Muted State
func (sr *scrutinyRepository) UpdateDeviceMuted(ctx context.Context, wwn string, muted bool) error {
var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
return fmt.Errorf("Could not get device from DB: %v", err)
}

return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("muted", muted).Error
}

func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error {
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil {
return err
Expand Down
10 changes: 10 additions & 0 deletions webapp/backend/pkg/database/scrutiny_repository_migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20250221084400"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20251108044508"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
Expand Down Expand Up @@ -409,6 +410,15 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return tx.AutoMigrate(m20250221084400.Device{})
},
},
{
ID: "m20251108044508", // add archived to device data
Migrate: func(tx *gorm.DB) error {

//migrate the device database.
// adding column (muted)
return tx.AutoMigrate(m20251108044508.Device{})
},
},
})

if err := m.Migrate(); err != nil {
Expand Down
1 change: 1 addition & 0 deletions webapp/backend/pkg/models/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type DeviceWrapper struct {
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
Archived bool `json:"archived"`
Muted bool `json:"muted"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Expand Down
5 changes: 5 additions & 0 deletions webapp/backend/pkg/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me
return false
}

// If the device is muted, skip notification regardless of status
if device.Muted {
return false
}

//TODO: cannot check for warning notifyLevel yet.

// setup constants for comparison
Expand Down
22 changes: 22 additions & 0 deletions webapp/backend/pkg/web/handler/mute_device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package handler

import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)

func MuteDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)

err := deviceRepo.UpdateDeviceMuted(c, c.Param("wwn"), true)
if err != nil {
logger.Errorln("An error occurred while muting device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}

c.JSON(http.StatusOK, gin.H{"success": true})
}
22 changes: 22 additions & 0 deletions webapp/backend/pkg/web/handler/unmute_device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package handler

import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)

func UnmuteDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)

err := deviceRepo.UpdateDeviceMuted(c, c.Param("wwn"), false)
if err != nil {
logger.Errorln("An error occurred while muting device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}

c.JSON(http.StatusOK, gin.H{"success": true})
}
2 changes: 2 additions & 0 deletions webapp/backend/pkg/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
api.POST("/device/:wwn/archive", handler.ArchiveDevice) //used by UI to archive device
api.POST("/device/:wwn/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device
api.POST("/device/:wwn/mute", handler.MuteDevice) //used by UI to mute device
api.POST("/device/:wwn/unmute", handler.UnmuteDevice) //used by UI to unmute device
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device

api.GET("/settings", handler.GetSettings) //used to get settings
Expand Down
1 change: 1 addition & 0 deletions webapp/frontend/src/app/core/models/device-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// maps to webapp/backend/pkg/models/device.go
export interface DeviceModel {
archived?: boolean;
muted: boolean;
wwn: string;
device_name?: string;
device_uuid?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}</a>
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}} <mat-icon
[svgIcon]="'notifications_off'"
*ngIf="deviceSummary.device.muted"
class="muted-icon"
></mat-icon></a>
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
.text-disabled{
opacity: 0.8;
}

.mat-icon.muted-icon {
/* Treo is messing with the selector a little bit, so we have to assert !importance here */
display: inline-block !important;
width: 16px;
height: 16px;
min-width: 16px;
min-height: 16px;

// This gives a slightly better alignment on default zoom.
// Need to figure out a better way. See comment in app/modules/detail/detail.component.scss
vertical-align: middle;
}

.mat-icon.muted-icon {
width: 16px;
height: 16px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ <h2 mat-dialog-title>Scrutiny Settings</h2>
<div class="flex flex-col gt-xs:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3">
<mat-label>Notifications</mat-label>
<mat-select [value]="'enable'">
<mat-option value="enable">Enabled</mat-option>
<mat-option value="disable" disabled>Disabled</mat-option>
<mat-select [(value)]="muted">
<mat-option [value]="false">Enabled</mat-option>
<mat-option [value]="true">Disabled</mat-option>
</mat-select>
</mat-form-field>
</div>
Expand All @@ -26,5 +26,5 @@ <h2 mat-dialog-title>Scrutiny Settings</h2>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button matTooltip="not yet implemented" [mat-dialog-close]="true" cdkFocusInitial>Save</button>
<button mat-button [mat-dialog-close]="{ muted }" cdkFocusInitial>Save</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';

@Component({
selector: 'app-detail-settings',
Expand All @@ -7,9 +8,14 @@ import { Component, OnInit } from '@angular/core';
})
export class DetailSettingsComponent implements OnInit {

constructor() { }
muted: boolean;

ngOnInit(): void {
constructor(
@Inject(MAT_DIALOG_DATA) public data: { curMuted: boolean }
) {
this.muted = data.curMuted;
}

ngOnInit(): void {
}
}
13 changes: 9 additions & 4 deletions webapp/frontend/src/app/modules/detail/detail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@

<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
<div class="mr-6">
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboard_display}} </h2>
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboard_display}} <mat-icon
[svgIcon]="'notifications_off'"
*ngIf="device && device.muted"
class="muted-icon"
></mat-icon></h2>
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
</div>
<!-- Action buttons -->
<div class="flex items-center">
<button class="xs:hidden"
<button class="ml-2 xs:hidden"
matTooltip="not yet implemented"
mat-stroked-button>
<mat-icon class="icon-size-20"
[svgIcon]="'save'"></mat-icon>
<span class="ml-2">Export</span>
</button>

<button class="ml-2 xs:hidden"
matTooltip="not yet implemented"
(click)="openSettingsDialog()"
mat-stroked-button>
<mat-icon class="icon-size-20 rotate-90 mirror"
[svgIcon]="'tune'"></mat-icon>
Expand All @@ -38,7 +43,7 @@ <h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboard_display}
<span class="ml-2">Export</span>
</button>
<button mat-menu-item
(click)="openDialog()">
(click)="openSettingsDialog()">
<mat-icon class="icon-size-20 rotate-90 mirror"
[svgIcon]="'tune'"></mat-icon>
<span class="ml-2">Settings</span>
Expand Down
7 changes: 7 additions & 0 deletions webapp/frontend/src/app/modules/detail/detail.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ tr.attribute-row:not(.attribute-expanded-row):active {
overflow: hidden;
display: flex;
}

.muted-icon {
// Giving vertical-align does not solve alignment for all optical font sizes.
// Actually aligning by baseline shows a more decent alignment on default zoom on Firefox w/ Inter
// Commenting this out for now. Need to figure out a better way.
// vertical-align: middle;
}
18 changes: 14 additions & 4 deletions webapp/frontend/src/app/modules/detail/detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,21 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results);
}

openDialog(): void {
const dialogRef = this.dialog.open(DetailSettingsComponent);
openSettingsDialog(): void {
const dialogRef = this.dialog.open(DetailSettingsComponent, {
data: {
curMuted: this.device.muted
},
});

dialogRef.afterClosed().subscribe(result => {
console.log(`Dialog result: ${result}`);
dialogRef.afterClosed().subscribe((result: undefined | null | { muted: boolean }) => {
console.log('Settings dialog result', result);
if (!result) return;
if (result.muted !== this.device.muted) {
this._detailService.setMuted(this.device.wwn, result.muted).toPromise().then(() => {
return this._detailService.getData(this.device.wwn).toPromise();
});
}
});
}

Expand Down
9 changes: 9 additions & 0 deletions webapp/frontend/src/app/modules/detail/detail.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,13 @@ export class DetailService {
})
);
}

/**
* Mute / Unmute certain device
*/
setMuted(wwn, muted): Observable<any> {
console.log('Set muted', muted);
const action = muted ? 'mute' : 'unmute';
return this._httpClient.post(getBasePath() + `/api/device/${wwn}/${action}`, {});
}
}