Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
52972b2
chore(deps): bump @angular-eslint/schematics in /webapp/frontend
dependabot[bot] May 18, 2026
c461109
chore(deps): bump ng-apexcharts from 2.3.0 to 2.4.0 in /webapp/frontend
dependabot[bot] May 18, 2026
d3cb84a
chore(deps): bump eslint-plugin-prettier in /webapp/frontend
dependabot[bot] May 18, 2026
999fe72
chore(deps): bump github.com/go-gormigrate/gormigrate/v2
dependabot[bot] Jun 4, 2026
2bd67aa
chore(deps): bump github.com/nicholas-fedor/shoutrrr
dependabot[bot] Jun 8, 2026
0de62db
chore(deps): bump the dev-dependencies group across 1 directory with …
dependabot[bot] Jun 8, 2026
6432a2c
feat(notify): add heartbeat_enabled field to notify_urls
Starosdev Jun 8, 2026
51d8662
feat(notify): LoadHeartbeatDatabaseUrls filters by heartbeat_enabled
Starosdev Jun 8, 2026
24f359a
feat(heartbeat): filter notify URLs by heartbeat_enabled
Starosdev Jun 8, 2026
99bb242
feat(api): expose and update heartbeat_enabled on notify URLs
Starosdev Jun 8, 2026
e6bbfea
feat(ui): add heartbeat toggle per notification URL
Starosdev Jun 8, 2026
8c375c2
fix(api): return 404 when heartbeat toggle targets non-existent notif…
Starosdev Jun 8, 2026
8f59d18
Merge pull request #601 from Starosdev/feature/SCR-521-heartbeat-url-…
Starosdev Jun 9, 2026
6e1f66e
Merge remote-tracking branch 'origin/master' into develop
github-actions[bot] Jun 9, 2026
8ceb12b
feat(ui): add contextual help links for settings docs (#492)
Starosdev Jun 9, 2026
7b484a0
feat(ui): add contextual help links pointing to docs folder (#492)
Starosdev Jun 9, 2026
fca1ae3
feat(ui): add navigation visibility settings for ZFS, MDADM, Btrfs, W…
Starosdev Jun 9, 2026
ba5b6c3
fix(ui): style learn more link in Consumer Drive Profiles hint (#492)
Starosdev Jun 9, 2026
3e3aa73
feat(ui): group MDADM and Workload pages by host (#579)
Starosdev Jun 9, 2026
e05f438
Merge pull request #584 from Starosdev/dependabot/go_modules/develop/…
Starosdev Jun 10, 2026
4fded14
Merge pull request #597 from Starosdev/dependabot/go_modules/develop/…
Starosdev Jun 10, 2026
ea5281d
Merge pull request #525 from Starosdev/dependabot/npm_and_yarn/webapp…
Starosdev Jun 10, 2026
b078941
Merge pull request #526 from Starosdev/dependabot/npm_and_yarn/webapp…
Starosdev Jun 10, 2026
080b0e2
Merge pull request #527 from Starosdev/dependabot/npm_and_yarn/webapp…
Starosdev Jun 10, 2026
2684685
Merge pull request #598 from Starosdev/dependabot/npm_and_yarn/webapp…
Starosdev Jun 10, 2026
be1dd65
chore(ui): fix prettier formatting in mobile-tab-bar
Starosdev Jun 11, 2026
2755d9c
test(ui): provide ScrutinyConfigService mock in mobile-tab-bar spec
Starosdev Jun 11, 2026
8158a34
fix(db): simplify migration func to satisfy unlambda linter
Starosdev Jun 11, 2026
231aef3
Merge remote-tracking branch 'origin/master' into develop
github-actions[bot] Jun 11, 2026
7e26ccb
Merge remote-tracking branch 'origin/master' into develop
github-actions[bot] Jun 11, 2026
87c825b
[SCR-491] v1.0.0 — Persist ATA SMART self-test history
Starosdev Jun 17, 2026
91ac8f3
[SCR-491] v1.1.0 — Add SMART self-test history endpoint
Starosdev Jun 17, 2026
5f1f391
[SCR-491] v1.2.0 — Show SMART self-test history in device details
Starosdev Jun 17, 2026
770b5e5
[SCR-491] v1.3.0 — Document ATA self-test history API and UI
Starosdev Jun 17, 2026
be05416
fix(backend): satisfy gocritic hugeParam and govet fieldalignment lint
Starosdev Jun 18, 2026
e027c33
fix(backend): pass SmartInfo by pointer to syncDeviceSelfTests (hugeP…
Starosdev Jun 18, 2026
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,12 @@ All notable changes to Scrutiny will be documented in this file.
### Refactoring

* **database:** extract hardcoded time ranges to constants ([deb2df0](https://github.com/Starosdev/scrutiny/commit/deb2df0bc718461c5a9826d6b6c1c1307b7122e8)), closes [#49](https://github.com/Starosdev/scrutiny/issues/49)
## Unreleased

### Features

* **smart/ui:** add SMART self-test history endpoint and device detail view


## [1.1.1](https://github.com/Starosdev/scrutiny/compare/v1.1.0...v1.1.1) (2025-12-09)

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ These S.M.A.R.T hard drive self-tests can help you detect and replace failing ha
- **Mobile-Optimized Interface** - Bottom tab bar (Home, Drives, ZFS, Workload, Settings), health overview home tab, card-based data views, and responsive layouts below 960px
- **API Timeout Configuration** - Adjust timeouts for slow storage systems
- **Performance Benchmarking** - fio-based benchmarks for throughput, IOPS, and latency with historical tracking
- **ATA Self-Test History** - Persist and display recent ATA SMART self-test log entries on device detail pages
- **Scheduled Reports** [WIP] - Automated health reports on daily/weekly/monthly schedules with HTML emails and PDF export
- **Missed Ping Digest** - Batch notification when multiple collectors go unreachable
- **HTML Email Notifications** - Rich HTML formatting with plain-text fallback for SMTP notifications, including reports, test notifications, collector errors, missed ping digests, heartbeat, performance degradation, replacement risk, and MDADM degradation alerts
Expand Down Expand Up @@ -503,6 +504,16 @@ Some SSDs report misleading wear percentages through SMART. To supplement those

This value is user-supplied and does not replace the existing SMART wear indicators.

## ATA SMART Self-Test History

Scrutiny now retains ATA SMART self-test log entries that are already present in uploaded `smartctl` payloads. On ATA drive detail pages, the web UI shows a **SMART Self-Tests** card with the recorded test type, pass/attention status, controller detail string, and power-on age.

- Data is read from normal SMART uploads; no separate self-test collector is required
- Only ATA devices show this section today
- Scrutiny keeps the most recent 21 recorded entries per physical ATA device identity
- The API route `GET /api/device/{id}/selftest` returns the same history used by the device detail page
- This feature records and displays history only; it does not trigger or schedule drive self-tests from the web UI

## SMART Attribute Overrides

Scrutiny allows you to customize how individual SMART attributes are evaluated. Use this to suppress false positives, ignore noisy attributes, force specific statuses, or set custom warning/failure thresholds.
Expand Down
3 changes: 2 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ That includes:

- authentication and session login
- health and diagnostics
- device registration, uploads, details, actions, and performance
- device registration, uploads, details, self-test history, actions, and performance
- settings, SMART overrides, and notification URLs
- replacement-risk metadata, including consumer ATA profile usage
- report generation and report history
Expand All @@ -41,6 +41,7 @@ See [AUTH.md](./AUTH.md) for configuration and deployment details.

- The OpenAPI document is the source of truth. Do not add new standalone API tables elsewhere in the repo.
- Some collector payloads are intentionally documented as structured objects with representative fields because the backend accepts large collector-origin JSON models.
- `GET /api/device/{id}/selftest` returns ATA SMART self-test history already recorded during normal SMART uploads. The separate `POST /api/device/{id}/selftest` route remains reserved for future ingestion work.
- Notification URL endpoints cover existing Shoutrrr syntax, explicit `apprise+...` targets, `script://` targets, and raw `http(s)` webhooks.
- The replacement-risk endpoint includes ATA-specific metadata describing whether a bundled consumer-drive profile was enabled and applied for that score.
- For operator-facing behavior and the global opt-out setting, see [CONSUMER_DRIVE_PROFILES.md](./CONSUMER_DRIVE_PROFILES.md).
Expand Down
72 changes: 72 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,23 @@ paths:
"500":
$ref: "#/components/responses/ErrorResponse"
/api/device/{id}/selftest:
get:
tags: [Devices]
summary: Get ATA SMART self-test history for a device
description: Returns the ATA SMART self-test entries already recorded from normal SMART uploads for the resolved device identity.
parameters:
- $ref: "#/components/parameters/DeviceId"
responses:
"200":
description: ATA SMART self-test history
content:
application/json:
schema:
$ref: "#/components/schemas/DeviceSelfTestsResponse"
"404":
$ref: "#/components/responses/ErrorResponse"
"500":
$ref: "#/components/responses/ErrorResponse"
post:
tags: [Devices]
summary: Upload self-test data for a device
Expand Down Expand Up @@ -1608,6 +1625,61 @@ components:
type: object
additionalProperties: true
required: [success, data]
DeviceSelfTest:
type: object
properties:
id:
type: integer
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
deleted_at:
type: string
format: date-time
nullable: true
device_id:
type: string
device_wwn:
type: string
type_value:
type: integer
type_string:
type: string
status_value:
type: integer
status_string:
type: string
status_passed:
type: boolean
lifetime_hours:
type: integer
required:
- id
- device_id
- device_wwn
- type_value
- type_string
- status_value
- status_string
- status_passed
- lifetime_hours
DeviceSelfTestsResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
self_tests:
type: array
items:
$ref: "#/components/schemas/DeviceSelfTest"
required: [self_tests]
required: [success, data]
PerformanceUploadRequest:
type: object
properties:
Expand Down
1 change: 1 addition & 0 deletions webapp/backend/pkg/database/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type DeviceRepo interface {

SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error)
GetDeviceSelfTests(ctx context.Context, deviceID string) ([]models.DeviceSelfTest, error)
// GetPreviousSmartSubmission returns the previous raw SMART submission (without daily aggregation)
// for use in repeat notification detection. Returns the submission before the most recent one.
GetPreviousSmartSubmission(ctx context.Context, wwn string) ([]measurements.Smart, error)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package m20260616000000

import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"gorm.io/gorm"
)

func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&models.DeviceSelfTest{})
}
43 changes: 29 additions & 14 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.

103 changes: 103 additions & 0 deletions webapp/backend/pkg/database/scrutiny_repository_device_self_tests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package database

import (
"context"
"fmt"
"strings"

"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)

const maxDeviceSelfTestsPerDevice = 21

func deviceSelfTestIdentity(device *models.Device) string {
if strings.TrimSpace(device.WWN) != "" {
return strings.TrimSpace(device.WWN)
}
return device.DeviceID
}

func (sr *scrutinyRepository) syncDeviceSelfTests(ctx context.Context, device *models.Device, collectorSmartData *collector.SmartInfo) error {
if collectorSmartData.Device.Protocol != pkg.DeviceProtocolAta {
return nil
}

selfTestEntries := collectorSmartData.AtaSmartSelfTestLog.Entries()
if len(selfTestEntries) == 0 {
return nil
}

deviceIdentity := deviceSelfTestIdentity(device)

return sr.gormClient.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for _, selfTestEntry := range selfTestEntries {
row := models.DeviceSelfTest{
DeviceID: device.DeviceID,
DeviceWWN: device.WWN,
DeviceIdentity: deviceIdentity,
TypeValue: selfTestEntry.Type.Value,
TypeString: selfTestEntry.Type.String,
StatusValue: selfTestEntry.Status.Value,
StatusString: selfTestEntry.Status.String,
StatusPassed: selfTestEntry.Status.Passed,
LifetimeHours: selfTestEntry.LifetimeHours,
}

if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{
{Name: "device_identity"},
{Name: "type_value"},
{Name: "lifetime_hours"},
},
DoUpdates: clause.AssignmentColumns([]string{
"device_id",
"device_wwn",
"type_string",
"status_value",
"status_string",
"status_passed",
"updated_at",
}),
}).Create(&row).Error; err != nil {
return err
}
}

var staleIDs []uint
if err := tx.Model(&models.DeviceSelfTest{}).
Where("device_identity = ?", deviceIdentity).
Order("lifetime_hours DESC, updated_at DESC, id DESC").
Offset(maxDeviceSelfTestsPerDevice).
Pluck("id", &staleIDs).Error; err != nil {
return err
}

if len(staleIDs) == 0 {
return nil
}

return tx.Delete(&models.DeviceSelfTest{}, staleIDs).Error
})
}

func (sr *scrutinyRepository) GetDeviceSelfTests(ctx context.Context, deviceID string) ([]models.DeviceSelfTest, error) {
device, err := sr.GetDeviceDetails(ctx, deviceID)
if err != nil {
return nil, err
}

deviceIdentity := deviceSelfTestIdentity(&device)
selfTests := []models.DeviceSelfTest{}
if err := sr.gormClient.WithContext(ctx).
Where("device_identity = ?", deviceIdentity).
Order("lifetime_hours DESC, id DESC").
Find(&selfTests).Error; err != nil {
return nil, fmt.Errorf("could not get device self-tests from DB: %v", err)
}

return selfTests, nil
}
Loading
Loading