diff --git a/webapp/backend/pkg/database/interface.go b/webapp/backend/pkg/database/interface.go index ee7066e7..fdde51c0 100644 --- a/webapp/backend/pkg/database/interface.go +++ b/webapp/backend/pkg/database/interface.go @@ -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) diff --git a/webapp/backend/pkg/database/migrations/m20250221084400/device.go b/webapp/backend/pkg/database/migrations/m20250221084400/device.go index b0a2e5af..9dfafd69 100644 --- a/webapp/backend/pkg/database/migrations/m20250221084400/device.go +++ b/webapp/backend/pkg/database/migrations/m20250221084400/device.go @@ -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 diff --git a/webapp/backend/pkg/database/migrations/m20251108044508/device.go b/webapp/backend/pkg/database/migrations/m20251108044508/device.go new file mode 100644 index 00000000..273c2293 --- /dev/null +++ b/webapp/backend/pkg/database/migrations/m20251108044508/device.go @@ -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"` +} diff --git a/webapp/backend/pkg/database/mock/mock_database.go b/webapp/backend/pkg/database/mock/mock_database.go index f5fefc2a..9a31f284 100644 --- a/webapp/backend/pkg/database/mock/mock_database.go +++ b/webapp/backend/pkg/database/mock/mock_database.go @@ -66,6 +66,20 @@ func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, wwn, archived in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, wwn, archived) } +// UpdateDeviceMuted mocks base method. +func (m *MockDeviceRepo) UpdateDeviceMuted(ctx context.Context, wwn string, archived bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDeviceMuted", ctx, wwn) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateDeviceMuted indicates an expected call of UpdateDeviceMuted. +func (mr *MockDeviceRepoMockRecorder) UpdateDeviceMuted(ctx, wwn, archived interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceMuted", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceMuted), ctx, wwn, archived) +} + // DeleteDevice mocks base method. func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string) error { m.ctrl.T.Helper() diff --git a/webapp/backend/pkg/database/scrutiny_repository_device.go b/webapp/backend/pkg/database/scrutiny_repository_device.go index 8ba0e8c8..4147cc37 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device.go @@ -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 diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index e12fae83..f8380d85 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -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" @@ -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 { diff --git a/webapp/backend/pkg/models/device.go b/webapp/backend/pkg/models/device.go index a891652e..4702ee80 100644 --- a/webapp/backend/pkg/models/device.go +++ b/webapp/backend/pkg/models/device.go @@ -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 diff --git a/webapp/backend/pkg/notify/notify.go b/webapp/backend/pkg/notify/notify.go index bf927ee3..155e782d 100644 --- a/webapp/backend/pkg/notify/notify.go +++ b/webapp/backend/pkg/notify/notify.go @@ -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 diff --git a/webapp/backend/pkg/web/handler/mute_device.go b/webapp/backend/pkg/web/handler/mute_device.go new file mode 100644 index 00000000..38c581d1 --- /dev/null +++ b/webapp/backend/pkg/web/handler/mute_device.go @@ -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}) +} diff --git a/webapp/backend/pkg/web/handler/unmute_device.go b/webapp/backend/pkg/web/handler/unmute_device.go new file mode 100644 index 00000000..11bb2306 --- /dev/null +++ b/webapp/backend/pkg/web/handler/unmute_device.go @@ -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}) +} diff --git a/webapp/backend/pkg/web/server.go b/webapp/backend/pkg/web/server.go index 38383c92..826ffd15 100644 --- a/webapp/backend/pkg/web/server.go +++ b/webapp/backend/pkg/web/server.go @@ -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 diff --git a/webapp/frontend/src/app/core/models/device-model.ts b/webapp/frontend/src/app/core/models/device-model.ts index bddb776c..9f63d474 100644 --- a/webapp/frontend/src/app/core/models/device-model.ts +++ b/webapp/frontend/src/app/core/models/device-model.ts @@ -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; diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html index 01ffb5f8..91e05909 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html @@ -17,7 +17,11 @@
{{deviceSummary.device | deviceTitle:config.dashboard_display}} + class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.scss b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.scss index 60e0fcd2..cc0d4023 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.scss +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.scss @@ -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; +} diff --git a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.html b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.html index 649148d8..fe597d36 100644 --- a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.html +++ b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.html @@ -15,9 +15,9 @@

Scrutiny Settings

Notifications - - Enabled - Disabled + + Enabled + Disabled
@@ -26,5 +26,5 @@

Scrutiny Settings

- + diff --git a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.ts b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.ts index c3542c74..27f9e0b8 100644 --- a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.ts +++ b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.ts @@ -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', @@ -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 { + } } diff --git a/webapp/frontend/src/app/modules/detail/detail.component.html b/webapp/frontend/src/app/modules/detail/detail.component.html index 13b19c29..88408170 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.html +++ b/webapp/frontend/src/app/modules/detail/detail.component.html @@ -4,20 +4,25 @@
-

Drive Details - {{device | deviceTitle:config.dashboard_display}}

+

Drive Details - {{device | deviceTitle:config.dashboard_display}}

Dive into S.M.A.R.T data
- +