├── .dockerignore ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yaml │ ├── docker-build.yaml │ ├── docker-nightly.yaml │ ├── release-frontend.yaml │ ├── release.yaml │ └── sponsors.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── REFERENCES.md ├── collector ├── cmd │ ├── collector-metrics │ │ └── collector-metrics.go │ └── collector-selftest │ │ └── collector-selftest.go └── pkg │ ├── collector │ ├── base.go │ ├── metrics.go │ ├── metrics_test.go │ └── selftest.go │ ├── common │ └── shell │ │ ├── factory.go │ │ ├── interface.go │ │ ├── local_shell.go │ │ ├── local_shell_test.go │ │ └── mock │ │ └── mock_shell.go │ ├── config │ ├── config.go │ ├── config_test.go │ ├── factory.go │ ├── interface.go │ ├── mock │ │ └── mock_config.go │ └── testdata │ │ ├── allow_listed_devices_present.yaml │ │ ├── device_type_comma.yaml │ │ ├── ignore_device.yaml │ │ ├── invalid_commands_includes_device.yaml │ │ ├── invalid_commands_missing_json.yaml │ │ ├── override_commands.yaml │ │ ├── override_device_commands.yaml │ │ ├── raid_device.yaml │ │ └── simple_device.yaml │ ├── detect │ ├── detect.go │ ├── detect_test.go │ ├── devices_darwin.go │ ├── devices_freebsd.go │ ├── devices_linux.go │ ├── devices_linux_test.go │ ├── devices_windows.go │ ├── testdata │ │ ├── smartctl_info_nvme.json │ │ ├── smartctl_scan_megaraid.json │ │ ├── smartctl_scan_nvme.json │ │ └── smartctl_scan_simple.json │ ├── wwn.go │ └── wwn_test.go │ ├── errors │ ├── errors.go │ └── errors_test.go │ └── models │ ├── device.go │ ├── scan.go │ └── scan_override.go ├── docker ├── Dockerfile ├── Dockerfile.collector ├── Dockerfile.web ├── README.md ├── entrypoint-collector.sh ├── example.hubspoke.docker-compose.yml └── example.omnibus.docker-compose.yml ├── docs ├── DOWNSAMPLING.md ├── INSTALL_ANSIBLE.md ├── INSTALL_HUB_SPOKE.md ├── INSTALL_MANUAL.md ├── INSTALL_MANUAL_WINDOWS.md ├── INSTALL_NAS.md ├── INSTALL_PFSENSE.md ├── INSTALL_SYNOLOGY_COLLECTOR.md ├── INSTALL_UNRAID.md ├── SUPPORTED_NAS_OS.md ├── TESTERS.md ├── TROUBLESHOOTING_DEVICE_COLLECTOR.md ├── TROUBLESHOOTING_DOCKER.md ├── TROUBLESHOOTING_INFLUXDB.md ├── TROUBLESHOOTING_NOTIFICATIONS.md ├── TROUBLESHOOTING_REVERSE_PROXY.md ├── TROUBLESHOOTING_UDEV.md ├── dashboard.png ├── dbdiagram.io.txt ├── details-full.png ├── details.png ├── influxdb-admin-token.png ├── multiple-host-ids.png └── sponsors.png ├── example.collector.yaml ├── example.scrutiny.yaml ├── go.mod ├── go.sum ├── packagr.yml ├── rootfs └── etc │ ├── cont-init.d │ ├── 01-timezone │ └── 50-cron-config │ ├── cron.d │ └── scrutiny │ └── services.d │ ├── collector-once │ └── run │ ├── cron │ ├── finish │ └── run │ ├── influxdb │ └── run │ └── scrutiny │ └── run └── webapp ├── backend ├── cmd │ └── scrutiny │ │ └── scrutiny.go └── pkg │ ├── config │ ├── config.go │ ├── config_test.go │ ├── factory.go │ ├── interface.go │ └── mock │ │ └── mock_config.go │ ├── constants.go │ ├── database │ ├── interface.go │ ├── migrations │ │ ├── m20201107210306 │ │ │ ├── device.go │ │ │ ├── smart.go │ │ │ ├── smart_ata_attribute.go │ │ │ ├── smart_nvme_attribute.go │ │ │ └── smart_scsci_attribute.go │ │ ├── m20220503120000 │ │ │ └── device.go │ │ ├── m20220509170100 │ │ │ └── device.go │ │ ├── m20220716214900 │ │ │ └── setting.go │ │ └── m20250221084400 │ │ │ └── device.go │ ├── mock │ │ └── mock_database.go │ ├── scrutiny_repository.go │ ├── scrutiny_repository_device.go │ ├── scrutiny_repository_device_smart_attributes.go │ ├── scrutiny_repository_migrations.go │ ├── scrutiny_repository_settings.go │ ├── scrutiny_repository_tasks.go │ ├── scrutiny_repository_tasks_test.go │ ├── scrutiny_repository_temperature.go │ └── scrutiny_repository_temperature_test.go │ ├── errors │ ├── errors.go │ └── errors_test.go │ ├── models │ ├── collector │ │ ├── smart.go │ │ └── smart_test.go │ ├── device.go │ ├── device_summary.go │ ├── measurements │ │ ├── smart.go │ │ ├── smart_ata_attribute.go │ │ ├── smart_attribute.go │ │ ├── smart_nvme_attribute.go │ │ ├── smart_scsci_attribute.go │ │ ├── smart_temperature.go │ │ └── smart_test.go │ ├── setting_entry.go │ ├── settings.go │ └── testdata │ │ ├── helper.go │ │ ├── smart-ata-date.json │ │ ├── smart-ata-date2.json │ │ ├── smart-ata-failed-scrutiny.json │ │ ├── smart-ata-full.json │ │ ├── smart-ata.json │ │ ├── smart-ata2.json │ │ ├── smart-fail.json │ │ ├── smart-fail2.json │ │ ├── smart-megaraid0.json │ │ ├── smart-megaraid1.json │ │ ├── smart-nvme-failed.json │ │ ├── smart-nvme.json │ │ ├── smart-nvme2.json │ │ ├── smart-pass.json │ │ ├── smart-raid.json │ │ ├── smart-sat.json │ │ ├── smart-scsi.json │ │ └── smart-scsi2.json │ ├── notify │ ├── notify.go │ └── notify_test.go │ ├── thresholds │ ├── ata_attribute_metadata.go │ ├── nvme_attribute_metadata.go │ └── scsi_attribute_metadata.go │ ├── version │ └── version.go │ └── web │ ├── handler │ ├── archive_device.go │ ├── delete_device.go │ ├── get_device_details.go │ ├── get_devices_summary.go │ ├── get_devices_summary_temp_history.go │ ├── get_settings.go │ ├── health_check.go │ ├── register_devices.go │ ├── save_settings.go │ ├── send_test_notification.go │ ├── unarchive_device.go │ ├── upload_device_metrics.go │ └── upload_device_self_tests.go │ ├── middleware │ ├── config.go │ ├── logger.go │ └── repository.go │ ├── server.go │ ├── server_test.go │ └── testdata │ ├── register-devices-req-2.json │ ├── register-devices-req.json │ ├── register-devices-single-req.json │ └── upload-device-metrics-req.json └── frontend ├── .editorconfig ├── .gitignore ├── CREDITS ├── LICENSE.md ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── git.version.sh ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── @treo │ ├── animations │ │ ├── defaults.ts │ │ ├── expand-collapse.ts │ │ ├── fade.ts │ │ ├── index.ts │ │ ├── public-api.ts │ │ ├── shake.ts │ │ ├── slide.ts │ │ └── zoom.ts │ ├── components │ │ ├── card │ │ │ ├── card.component.html │ │ │ ├── card.component.scss │ │ │ ├── card.component.ts │ │ │ ├── card.module.ts │ │ │ ├── index.ts │ │ │ └── public-api.ts │ │ ├── date-range │ │ │ ├── date-range.component.html │ │ │ ├── date-range.component.scss │ │ │ ├── date-range.component.ts │ │ │ ├── date-range.module.ts │ │ │ ├── index.ts │ │ │ └── public-api.ts │ │ ├── drawer │ │ │ ├── drawer.component.html │ │ │ ├── drawer.component.scss │ │ │ ├── drawer.component.ts │ │ │ ├── drawer.module.ts │ │ │ ├── drawer.service.ts │ │ │ ├── drawer.types.ts │ │ │ ├── index.ts │ │ │ └── public-api.ts │ │ ├── highlight │ │ │ ├── highlight.component.html │ │ │ ├── highlight.component.scss │ │ │ ├── highlight.component.ts │ │ │ ├── highlight.module.ts │ │ │ ├── highlight.service.ts │ │ │ ├── index.ts │ │ │ └── public-api.ts │ │ ├── message │ │ │ ├── index.ts │ │ │ ├── message.component.html │ │ │ ├── message.component.scss │ │ │ ├── message.component.ts │ │ │ ├── message.module.ts │ │ │ ├── message.service.ts │ │ │ ├── message.types.ts │ │ │ └── public-api.ts │ │ └── navigation │ │ │ ├── horizontal │ │ │ ├── components │ │ │ │ ├── basic │ │ │ │ │ ├── basic.component.html │ │ │ │ │ └── basic.component.ts │ │ │ │ ├── branch │ │ │ │ │ ├── branch.component.html │ │ │ │ │ └── branch.component.ts │ │ │ │ ├── divider │ │ │ │ │ ├── divider.component.html │ │ │ │ │ └── divider.component.ts │ │ │ │ └── spacer │ │ │ │ │ ├── spacer.component.html │ │ │ │ │ └── spacer.component.ts │ │ │ ├── horizontal.component.html │ │ │ ├── horizontal.component.scss │ │ │ └── horizontal.component.ts │ │ │ ├── index.ts │ │ │ ├── navigation.module.ts │ │ │ ├── navigation.service.ts │ │ │ ├── navigation.types.ts │ │ │ ├── public-api.ts │ │ │ └── vertical │ │ │ ├── components │ │ │ ├── aside │ │ │ │ ├── aside.component.html │ │ │ │ └── aside.component.ts │ │ │ ├── basic │ │ │ │ ├── basic.component.html │ │ │ │ └── basic.component.ts │ │ │ ├── collapsable │ │ │ │ ├── collapsable.component.html │ │ │ │ └── collapsable.component.ts │ │ │ ├── divider │ │ │ │ ├── divider.component.html │ │ │ │ └── divider.component.ts │ │ │ ├── group │ │ │ │ ├── group.component.html │ │ │ │ └── group.component.ts │ │ │ └── spacer │ │ │ │ ├── spacer.component.html │ │ │ │ └── spacer.component.ts │ │ │ ├── vertical.component.html │ │ │ ├── vertical.component.scss │ │ │ └── vertical.component.ts │ ├── directives │ │ ├── autogrow │ │ │ ├── autogrow.directive.ts │ │ │ ├── autogrow.module.ts │ │ │ ├── index.ts │ │ │ └── public-api.ts │ │ └── scrollbar │ │ │ ├── index.ts │ │ │ ├── public-api.ts │ │ │ ├── scrollbar.directive.ts │ │ │ ├── scrollbar.interfaces.ts │ │ │ └── scrollbar.module.ts │ ├── index.ts │ ├── lib │ │ └── mock-api │ │ │ ├── index.ts │ │ │ ├── mock-api.interceptor.ts │ │ │ ├── mock-api.interfaces.ts │ │ │ ├── mock-api.module.ts │ │ │ ├── mock-api.request-handler.ts │ │ │ ├── mock-api.service.ts │ │ │ └── mock-api.utils.ts │ ├── pipes │ │ └── find-by-key │ │ │ ├── find-by-key.module.ts │ │ │ ├── find-by-key.pipe.ts │ │ │ ├── index.ts │ │ │ └── public-api.ts │ ├── services │ │ ├── config │ │ │ ├── config.constants.ts │ │ │ ├── config.module.ts │ │ │ ├── config.service.ts │ │ │ ├── index.ts │ │ │ └── public-api.ts │ │ ├── media-watcher │ │ │ ├── index.ts │ │ │ ├── media-watcher.module.ts │ │ │ ├── media-watcher.service.ts │ │ │ └── public-api.ts │ │ └── splash-screen │ │ │ ├── index.ts │ │ │ ├── public-api.ts │ │ │ ├── splash-screen.module.ts │ │ │ └── splash-screen.service.ts │ ├── styles │ │ ├── base │ │ │ ├── _colors.scss │ │ │ ├── _preflight.scss │ │ │ ├── _theming.scss │ │ │ └── _typography.scss │ │ ├── components │ │ │ ├── _card.scss │ │ │ ├── _input.scss │ │ │ └── _table.scss │ │ ├── layout │ │ │ └── _content.scss │ │ ├── main.scss │ │ ├── overrides │ │ │ ├── _angular-material.scss │ │ │ ├── _highlightjs.scss │ │ │ ├── _perfect-scrollbar.scss │ │ │ └── _quill.scss │ │ ├── treo.scss │ │ ├── utilities │ │ │ ├── _breakpoints.scss │ │ │ ├── _colors.scss │ │ │ ├── _elevations.scss │ │ │ ├── _icons.scss │ │ │ ├── _keyframes.scss │ │ │ └── _theming.scss │ │ └── vendors │ │ │ ├── _angular-material.scss │ │ │ └── _normalize.scss │ ├── tailwind │ │ ├── export.css │ │ ├── export.js │ │ ├── exported │ │ │ ├── _variables.scss │ │ │ └── variables.ts │ │ └── plugins │ │ │ ├── index.js │ │ │ ├── utilities │ │ │ ├── color-combinations.js │ │ │ ├── color-contrasts.js │ │ │ ├── icon-color.js │ │ │ ├── icon-size.js │ │ │ └── mirror.js │ │ │ └── variants │ │ │ ├── dark-light.js │ │ │ ├── export-box-shadow.js │ │ │ ├── export-colors.js │ │ │ ├── export-font-family.js │ │ │ └── export-screens.js │ ├── treo.module.ts │ └── validators │ │ ├── index.ts │ │ ├── public-api.ts │ │ └── validators.ts ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routing.ts │ ├── core │ │ ├── config │ │ │ ├── app.config.ts │ │ │ ├── scrutiny-config.module.ts │ │ │ └── scrutiny-config.service.ts │ │ ├── core.module.ts │ │ └── models │ │ │ ├── device-details-response-wrapper.ts │ │ │ ├── device-model.ts │ │ │ ├── device-summary-model.ts │ │ │ ├── device-summary-response-wrapper.ts │ │ │ ├── device-summary-temp-response-wrapper.ts │ │ │ ├── measurements │ │ │ ├── smart-attribute-model.ts │ │ │ ├── smart-model.ts │ │ │ └── smart-temperature-model.ts │ │ │ └── thresholds │ │ │ └── attribute-metadata-model.ts │ ├── data │ │ └── mock │ │ │ ├── device │ │ │ └── details │ │ │ │ ├── index.ts │ │ │ │ ├── sda.ts │ │ │ │ ├── sdb.ts │ │ │ │ ├── sdc.ts │ │ │ │ ├── sdd.ts │ │ │ │ ├── sde.ts │ │ │ │ └── sdf.ts │ │ │ ├── index.ts │ │ │ └── summary │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ └── temp_history.ts │ ├── layout │ │ ├── common │ │ │ ├── dashboard-device-archive-dialog │ │ │ │ ├── dashboard-device-archive-dialog.component.html │ │ │ │ ├── dashboard-device-archive-dialog.component.scss │ │ │ │ ├── dashboard-device-archive-dialog.component.spec.ts │ │ │ │ ├── dashboard-device-archive-dialog.component.ts │ │ │ │ ├── dashboard-device-archive-dialog.module.ts │ │ │ │ └── dashboard-device-archive-dialog.service.ts │ │ │ ├── dashboard-device-delete-dialog │ │ │ │ ├── dashboard-device-delete-dialog.component.html │ │ │ │ ├── dashboard-device-delete-dialog.component.scss │ │ │ │ ├── dashboard-device-delete-dialog.component.spec.ts │ │ │ │ ├── dashboard-device-delete-dialog.component.ts │ │ │ │ ├── dashboard-device-delete-dialog.module.ts │ │ │ │ └── dashboard-device-delete-dialog.service.ts │ │ │ ├── dashboard-device │ │ │ │ ├── dashboard-device.component.html │ │ │ │ ├── dashboard-device.component.scss │ │ │ │ ├── dashboard-device.component.spec.ts │ │ │ │ ├── dashboard-device.component.ts │ │ │ │ └── dashboard-device.module.ts │ │ │ ├── dashboard-settings │ │ │ │ ├── dashboard-settings.component.html │ │ │ │ ├── dashboard-settings.component.scss │ │ │ │ ├── dashboard-settings.component.ts │ │ │ │ └── dashboard-settings.module.ts │ │ │ ├── detail-settings │ │ │ │ ├── detail-settings.component.html │ │ │ │ ├── detail-settings.component.scss │ │ │ │ ├── detail-settings.component.spec.ts │ │ │ │ ├── detail-settings.component.ts │ │ │ │ └── detail-settings.module.ts │ │ │ └── search │ │ │ │ ├── search.component.html │ │ │ │ ├── search.component.scss │ │ │ │ ├── search.component.ts │ │ │ │ └── search.module.ts │ │ ├── layout.component.html │ │ ├── layout.component.scss │ │ ├── layout.component.ts │ │ ├── layout.module.ts │ │ ├── layout.types.ts │ │ └── layouts │ │ │ ├── empty │ │ │ ├── empty.component.html │ │ │ ├── empty.component.scss │ │ │ ├── empty.component.ts │ │ │ └── empty.module.ts │ │ │ └── horizontal │ │ │ └── material │ │ │ ├── material.component.html │ │ │ ├── material.component.scss │ │ │ ├── material.component.ts │ │ │ └── material.module.ts │ ├── modules │ │ ├── dashboard │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.ts │ │ │ ├── dashboard.module.ts │ │ │ ├── dashboard.resolvers.ts │ │ │ ├── dashboard.routing.ts │ │ │ ├── dashboard.service.spec.ts │ │ │ └── dashboard.service.ts │ │ ├── detail │ │ │ ├── detail.component.html │ │ │ ├── detail.component.scss │ │ │ ├── detail.component.ts │ │ │ ├── detail.module.ts │ │ │ ├── detail.resolvers.ts │ │ │ ├── detail.routing.ts │ │ │ ├── detail.service.spec.ts │ │ │ └── detail.service.ts │ │ └── landing │ │ │ └── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.ts │ │ │ ├── home.module.ts │ │ │ └── home.routing.ts │ └── shared │ │ ├── device-hours.pipe.spec.ts │ │ ├── device-hours.pipe.ts │ │ ├── device-sort.pipe.spec.ts │ │ ├── device-sort.pipe.ts │ │ ├── device-status.pipe.spec.ts │ │ ├── device-status.pipe.ts │ │ ├── device-title.pipe.spec.ts │ │ ├── device-title.pipe.ts │ │ ├── file-size.pipe.spec.ts │ │ ├── file-size.pipe.ts │ │ ├── shared.module.ts │ │ ├── temperature.pipe.spec.ts │ │ └── temperature.pipe.ts ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── assets │ ├── .gitkeep │ ├── fonts │ │ ├── ibm-plex-mono │ │ │ ├── ibm-plex-mono-v12-latin-500.eot │ │ │ ├── ibm-plex-mono-v12-latin-500.svg │ │ │ ├── ibm-plex-mono-v12-latin-500.ttf │ │ │ ├── ibm-plex-mono-v12-latin-500.woff │ │ │ ├── ibm-plex-mono-v12-latin-500.woff2 │ │ │ ├── ibm-plex-mono-v12-latin-600.eot │ │ │ ├── ibm-plex-mono-v12-latin-600.svg │ │ │ ├── ibm-plex-mono-v12-latin-600.ttf │ │ │ ├── ibm-plex-mono-v12-latin-600.woff │ │ │ ├── ibm-plex-mono-v12-latin-600.woff2 │ │ │ ├── ibm-plex-mono-v12-latin-700.eot │ │ │ ├── ibm-plex-mono-v12-latin-700.svg │ │ │ ├── ibm-plex-mono-v12-latin-700.ttf │ │ │ ├── ibm-plex-mono-v12-latin-700.woff │ │ │ ├── ibm-plex-mono-v12-latin-700.woff2 │ │ │ ├── ibm-plex-mono-v12-latin-italic.eot │ │ │ ├── ibm-plex-mono-v12-latin-italic.svg │ │ │ ├── ibm-plex-mono-v12-latin-italic.ttf │ │ │ ├── ibm-plex-mono-v12-latin-italic.woff │ │ │ ├── ibm-plex-mono-v12-latin-italic.woff2 │ │ │ ├── ibm-plex-mono-v12-latin-regular.eot │ │ │ ├── ibm-plex-mono-v12-latin-regular.svg │ │ │ ├── ibm-plex-mono-v12-latin-regular.ttf │ │ │ ├── ibm-plex-mono-v12-latin-regular.woff │ │ │ ├── ibm-plex-mono-v12-latin-regular.woff2 │ │ │ └── ibm-plex-mono.css │ │ ├── inter │ │ │ ├── Inter-Black.woff │ │ │ ├── Inter-Black.woff2 │ │ │ ├── Inter-Bold.woff │ │ │ ├── Inter-Bold.woff2 │ │ │ ├── Inter-ExtraBold.woff │ │ │ ├── Inter-ExtraBold.woff2 │ │ │ ├── Inter-Italic.woff │ │ │ ├── Inter-Italic.woff2 │ │ │ ├── Inter-Medium.woff │ │ │ ├── Inter-Medium.woff2 │ │ │ ├── Inter-Regular.woff │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── Inter-SemiBold.woff │ │ │ ├── Inter-SemiBold.woff2 │ │ │ └── inter.css │ │ ├── material-icons │ │ │ ├── MaterialIcons-Regular.codepoints │ │ │ ├── MaterialIcons-Regular.ttf │ │ │ ├── MaterialIconsOutlined-Regular.codepoints │ │ │ ├── MaterialIconsOutlined-Regular.otf │ │ │ ├── MaterialIconsRound-Regular.codepoints │ │ │ ├── MaterialIconsRound-Regular.otf │ │ │ ├── MaterialIconsSharp-Regular.codepoints │ │ │ ├── MaterialIconsSharp-Regular.otf │ │ │ ├── MaterialIconsTwoTone-Regular.codepoints │ │ │ ├── MaterialIconsTwoTone-Regular.otf │ │ │ ├── README.md │ │ │ └── material-icons.css │ │ └── roboto │ │ │ ├── roboto-v30-latin-300.eot │ │ │ ├── roboto-v30-latin-300.svg │ │ │ ├── roboto-v30-latin-300.ttf │ │ │ ├── roboto-v30-latin-300.woff │ │ │ ├── roboto-v30-latin-300.woff2 │ │ │ ├── roboto-v30-latin-500.eot │ │ │ ├── roboto-v30-latin-500.svg │ │ │ ├── roboto-v30-latin-500.ttf │ │ │ ├── roboto-v30-latin-500.woff │ │ │ ├── roboto-v30-latin-500.woff2 │ │ │ ├── roboto-v30-latin-700.eot │ │ │ ├── roboto-v30-latin-700.svg │ │ │ ├── roboto-v30-latin-700.ttf │ │ │ ├── roboto-v30-latin-700.woff │ │ │ ├── roboto-v30-latin-700.woff2 │ │ │ ├── roboto-v30-latin-900.eot │ │ │ ├── roboto-v30-latin-900.svg │ │ │ ├── roboto-v30-latin-900.ttf │ │ │ ├── roboto-v30-latin-900.woff │ │ │ ├── roboto-v30-latin-900.woff2 │ │ │ ├── roboto-v30-latin-italic.eot │ │ │ ├── roboto-v30-latin-italic.svg │ │ │ ├── roboto-v30-latin-italic.ttf │ │ │ ├── roboto-v30-latin-italic.woff │ │ │ ├── roboto-v30-latin-italic.woff2 │ │ │ ├── roboto-v30-latin-regular.eot │ │ │ ├── roboto-v30-latin-regular.svg │ │ │ ├── roboto-v30-latin-regular.ttf │ │ │ ├── roboto-v30-latin-regular.woff │ │ │ ├── roboto-v30-latin-regular.woff2 │ │ │ └── roboto.css │ ├── icons │ │ ├── dripicons.svg │ │ ├── feather.svg │ │ ├── heroicons-outline.svg │ │ ├── heroicons-solid.svg │ │ ├── iconsmind.svg │ │ ├── material-outline.svg │ │ └── material-twotone.svg │ └── images │ │ ├── dashboard-placeholder.png │ │ └── logo │ │ ├── noun_Glasses_775232.svg │ │ ├── scrutiny-logo-dark-social.png │ │ ├── scrutiny-logo-dark-social.psd │ │ ├── scrutiny-logo-dark-text.png │ │ ├── scrutiny-logo-dark-text.psd │ │ ├── scrutiny-logo-dark.png │ │ ├── scrutiny-logo-dark.svg │ │ ├── scrutiny-logo-white-text.png │ │ ├── scrutiny-logo-white-text.psd │ │ ├── scrutiny-logo-white.psd │ │ └── scrutiny-logo-white.svg ├── browserconfig.xml ├── environments │ ├── environment.prod.ts │ ├── environment.ts │ └── versions.ts ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── index.html ├── main.ts ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── ms-icon-70x70.png ├── polyfills.ts ├── styles │ ├── styles.scss │ ├── tailwind.scss │ ├── themes.scss │ └── vendors.scss ├── tailwind │ ├── config.js │ └── main.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /.github 4 | /.git 5 | /webapp/frontend/node_modules 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false 2 | *.scss linguist-detectable=false 3 | *.js linguist-detectable=false 4 | *.ts linguist-detectable=false 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Log Files** 20 | If related to missing devices or SMART data, please run the `collector` in DEBUG mode, and attach the log file. 21 | See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md) for other troubleshooting tips. 22 | 23 | ``` 24 | docker run -it --rm -p 8080:8080 \ 25 | -v `pwd`/config:/opt/scrutiny/config \ 26 | -v /run/udev:/run/udev:ro \ 27 | --cap-add SYS_RAWIO \ 28 | --device=/dev/sda \ 29 | --device=/dev/sdb \ 30 | -e DEBUG=true \ 31 | -e COLLECTOR_LOG_FILE=/opt/scrutiny/config/collector.log \ 32 | -e SCRUTINY_LOG_FILE=/opt/scrutiny/config/web.log \ 33 | --name scrutiny \ 34 | ghcr.io/analogj/scrutiny:master-omnibus 35 | 36 | # in another terminal trigger the collector 37 | docker exec scrutiny scrutiny-collector-metrics run 38 | ``` 39 | 40 | The log files will be available on your host in the `config` directory. Please attach them to this issue. 41 | 42 | Please also provide the output of `docker info` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/sponsors.yaml: -------------------------------------------------------------------------------- 1 | name: Label sponsors 2 | on: 3 | pull_request: 4 | types: [opened] 5 | issues: 6 | types: [opened] 7 | jobs: 8 | build: 9 | name: is-sponsor-label 10 | runs-on: ubuntu-latest 11 | if: ${{ false }} 12 | steps: 13 | - uses: JasonEtco/is-sponsor-label-action@v1.2.0 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jason Kulatunga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /REFERENCES.md: -------------------------------------------------------------------------------- 1 | 2 | # Gorm 3 | - https://www.reddit.com/r/golang/comments/exmwos/golang_gorm_preload_with_last/ 4 | - https://blog.depado.eu/post/gorm-gotchas 5 | - 6 | 7 | 8 | # Smart Data 9 | - https://kb.acronis.com/content/9123 10 | -------------------------------------------------------------------------------- /collector/pkg/collector/metrics_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | func TestApiEndpointParse(t *testing.T) { 10 | baseURL, _ := url.Parse("http://localhost:8080/") 11 | 12 | url1, _ := baseURL.Parse("d/e") 13 | require.Equal(t, "http://localhost:8080/d/e", url1.String()) 14 | 15 | url2, _ := baseURL.Parse("/d/e") 16 | require.Equal(t, "http://localhost:8080/d/e", url2.String()) 17 | } 18 | 19 | func TestApiEndpointParse_WithBasepathWithoutTrailingSlash(t *testing.T) { 20 | baseURL, _ := url.Parse("http://localhost:8080/scrutiny") 21 | 22 | //This testcase is unexpected and can cause issues. We need to ensure the apiEndpoint always has a trailing slash. 23 | url1, _ := baseURL.Parse("d/e") 24 | require.Equal(t, "http://localhost:8080/d/e", url1.String()) 25 | 26 | url2, _ := baseURL.Parse("/d/e") 27 | require.Equal(t, "http://localhost:8080/d/e", url2.String()) 28 | } 29 | 30 | func TestApiEndpointParse_WithBasepathWithTrailingSlash(t *testing.T) { 31 | baseURL, _ := url.Parse("http://localhost:8080/scrutiny/") 32 | 33 | url1, _ := baseURL.Parse("d/e") 34 | require.Equal(t, "http://localhost:8080/scrutiny/d/e", url1.String()) 35 | 36 | url2, _ := baseURL.Parse("/d/e") 37 | require.Equal(t, "http://localhost:8080/d/e", url2.String()) 38 | } 39 | -------------------------------------------------------------------------------- /collector/pkg/collector/selftest.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "net/url" 6 | ) 7 | 8 | type SelfTestCollector struct { 9 | BaseCollector 10 | 11 | apiEndpoint *url.URL 12 | logger *logrus.Entry 13 | } 14 | 15 | func CreateSelfTestCollector(logger *logrus.Entry, apiEndpoint string) (SelfTestCollector, error) { 16 | apiEndpointUrl, err := url.Parse(apiEndpoint) 17 | if err != nil { 18 | return SelfTestCollector{}, err 19 | } 20 | 21 | stc := SelfTestCollector{ 22 | apiEndpoint: apiEndpointUrl, 23 | logger: logger, 24 | } 25 | 26 | return stc, nil 27 | } 28 | 29 | func (sc *SelfTestCollector) Run() error { 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /collector/pkg/common/shell/factory.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | func Create() Interface { 4 | return new(localShell) 5 | } 6 | -------------------------------------------------------------------------------- /collector/pkg/common/shell/interface.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | // Create mock using: 8 | // mockgen -source=collector/pkg/common/shell/interface.go -destination=collector/pkg/common/shell/mock/mock_shell.go 9 | type Interface interface { 10 | Command(logger *logrus.Entry, cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) 11 | } 12 | -------------------------------------------------------------------------------- /collector/pkg/common/shell/local_shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "github.com/sirupsen/logrus" 7 | "io" 8 | "os/exec" 9 | "path" 10 | "strings" 11 | ) 12 | 13 | type localShell struct{} 14 | 15 | func (s *localShell) Command(logger *logrus.Entry, cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) { 16 | logger.Infof("Executing command: %s %s", cmdName, strings.Join(cmdArgs, " ")) 17 | 18 | cmd := exec.Command(cmdName, cmdArgs...) 19 | var stdBuffer bytes.Buffer 20 | 21 | logWriters := []io.Writer{ 22 | &stdBuffer, 23 | } 24 | if logger.Logger.Level == logrus.DebugLevel { 25 | logWriters = append(logWriters, logger.Logger.Out) 26 | } 27 | 28 | mw := io.MultiWriter(logWriters...) 29 | 30 | cmd.Stdout = mw 31 | cmd.Stderr = mw 32 | 33 | if environ != nil { 34 | cmd.Env = environ 35 | } 36 | if workingDir != "" && path.IsAbs(workingDir) { 37 | cmd.Dir = workingDir 38 | } else if workingDir != "" { 39 | return "", errors.New("Working Directory must be an absolute path") 40 | } 41 | 42 | err := cmd.Run() 43 | return stdBuffer.String(), err 44 | 45 | } 46 | -------------------------------------------------------------------------------- /collector/pkg/config/factory.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | func Create() (Interface, error) { 4 | config := new(configuration) 5 | if err := config.Init(); err != nil { 6 | return nil, err 7 | } 8 | return config, nil 9 | } 10 | -------------------------------------------------------------------------------- /collector/pkg/config/interface.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/collector/pkg/models" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // Create mock using: 9 | // mockgen -source=collector/pkg/config/interface.go -destination=collector/pkg/config/mock/mock_config.go 10 | type Interface interface { 11 | Init() error 12 | ReadConfig(configFilePath string) error 13 | Set(key string, value interface{}) 14 | SetDefault(key string, value interface{}) 15 | 16 | AllSettings() map[string]interface{} 17 | IsSet(key string) bool 18 | Get(key string) interface{} 19 | GetBool(key string) bool 20 | GetInt(key string) int 21 | GetString(key string) string 22 | GetStringSlice(key string) []string 23 | UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error 24 | 25 | GetDeviceOverrides() []models.ScanOverride 26 | GetCommandMetricsInfoArgs(deviceName string) string 27 | GetCommandMetricsSmartArgs(deviceName string) string 28 | 29 | IsAllowlistedDevice(deviceName string) bool 30 | } 31 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/allow_listed_devices_present.yaml: -------------------------------------------------------------------------------- 1 | allow_listed_devices: 2 | - /dev/sda 3 | - /dev/sdb 4 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/device_type_comma.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | devices: 3 | # the scrutiny config parser will detect `sat,auto` as two separate items in a list. If you want to use `-d sat,auto` you must 4 | # set 'sat,auto' in a list (see eg. /dev/sbd) 5 | - device: /dev/sda 6 | type: 'sat,auto' 7 | - device: /dev/sdb 8 | type: 9 | - sat,auto 10 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/ignore_device.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | devices: 3 | - device: /dev/sda 4 | ignore: true 5 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/invalid_commands_includes_device.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | metrics_scan_args: '--scan --json' # used to detect devices 3 | metrics_info_args: '--info --json --device=sat' # used to determine device unique ID & register device with Scrutiny 4 | metrics_smart_args: '--xall --json -d sat' # used to retrieve smart data for each device. 5 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/invalid_commands_missing_json.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | metrics_scan_args: '--scan' # used to detect devices 3 | metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny 4 | metrics_smart_args: '--xall --json' # used to retrieve smart data for each device. 5 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/override_commands.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | metrics_scan_args: '--scan --json' # used to detect devices 3 | metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny 4 | metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device. 5 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/override_device_commands.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | devices: 3 | - device: /dev/sda 4 | commands: 5 | metrics_info_args: "--info --json -T permissive" -------------------------------------------------------------------------------- /collector/pkg/config/testdata/raid_device.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | devices: 3 | - device: /dev/bus/0 4 | type: 5 | - megaraid,14 6 | - megaraid,15 7 | - megaraid,18 8 | - megaraid,19 9 | - megaraid,20 10 | - megaraid,21 11 | 12 | - device: /dev/twa0 13 | type: 14 | - 3ware,0 15 | - 3ware,1 16 | - 3ware,2 17 | - 3ware,3 18 | - 3ware,4 19 | - 3ware,5 20 | -------------------------------------------------------------------------------- /collector/pkg/config/testdata/simple_device.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | devices: 3 | - device: /dev/sda 4 | type: 'sat' 5 | # 6 | # # example to show how to ignore a specific disk/device. 7 | # - device: /dev/sda 8 | # ignore: true 9 | # 10 | # # examples showing how to force smartctl to detect disks inside a raid array/virtual disk 11 | # - device: /dev/bus/0 12 | # type: 13 | # - megaraid,14 14 | # - megaraid,15 15 | # - megaraid,18 16 | # - megaraid,19 17 | # - megaraid,20 18 | # - megaraid,21 19 | # 20 | # - device: /dev/twa0 21 | # type: 22 | # - 3ware,0 23 | # - 3ware,1 24 | # - 3ware,2 25 | # - 3ware,3 26 | # - 3ware,4 27 | # - 3ware,5 28 | -------------------------------------------------------------------------------- /collector/pkg/detect/devices_linux_test.go: -------------------------------------------------------------------------------- 1 | package detect_test 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/collector/pkg/detect" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | func TestDevicePrefix(t *testing.T) { 10 | //setup 11 | 12 | //test 13 | 14 | //assert 15 | require.Equal(t, "/dev/", detect.DevicePrefix()) 16 | } 17 | -------------------------------------------------------------------------------- /collector/pkg/detect/devices_windows.go: -------------------------------------------------------------------------------- 1 | package detect 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/collector/pkg/common/shell" 5 | "github.com/analogj/scrutiny/collector/pkg/models" 6 | "strings" 7 | ) 8 | 9 | func DevicePrefix() string { 10 | return "" 11 | } 12 | 13 | func (d *Detect) Start() ([]models.Device, error) { 14 | d.Shell = shell.Create() 15 | // call the base/common functionality to get a list of devices 16 | detectedDevices, err := d.SmartctlScan() 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | //inflate device info for detected devices. 22 | for ndx, _ := range detectedDevices { 23 | d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors. 24 | } 25 | 26 | return detectedDevices, nil 27 | } 28 | 29 | //WWN values NVMe and SCSI 30 | func (d *Detect) wwnFallback(detectedDevice *models.Device) { 31 | 32 | //fallback to serial number 33 | if len(detectedDevice.WWN) == 0 { 34 | detectedDevice.WWN = detectedDevice.SerialNumber 35 | } 36 | 37 | //wwn must always be lowercase. 38 | detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) 39 | } 40 | -------------------------------------------------------------------------------- /collector/pkg/detect/testdata/smartctl_info_nvme.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_format_version": [ 3 | 1, 4 | 0 5 | ], 6 | "smartctl": { 7 | "version": [ 8 | 7, 9 | 2 10 | ], 11 | "svn_revision": "5155", 12 | "platform_info": "x86_64-linux-6.1.69-talos", 13 | "build_info": "(local build)", 14 | "argv": [ 15 | "smartctl", 16 | "--info", 17 | "--json", 18 | "/dev/nvme4" 19 | ], 20 | "exit_status": 0 21 | }, 22 | "device": { 23 | "name": "/dev/nvme4", 24 | "info_name": "/dev/nvme4", 25 | "type": "nvme", 26 | "protocol": "NVMe" 27 | }, 28 | "model_name": "KCD61LUL3T84", 29 | "serial_number": "61Q0A05UT7B8", 30 | "firmware_version": "8002", 31 | "nvme_pci_vendor": { 32 | "id": 7695, 33 | "subsystem_id": 7695 34 | }, 35 | "nvme_ieee_oui_identifier": 9233294, 36 | "nvme_total_capacity": 3840755982336, 37 | "nvme_unallocated_capacity": 0, 38 | "nvme_controller_id": 1, 39 | "nvme_version": { 40 | "string": "1.4", 41 | "value": 66560 42 | }, 43 | "nvme_number_of_namespaces": 16, 44 | "local_time": { 45 | "time_t": 1706045146, 46 | "asctime": "Tue Jan 23 21:25:46 2024 UTC" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /collector/pkg/detect/testdata/smartctl_scan_megaraid.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_format_version": [ 3 | 1, 4 | 0 5 | ], 6 | "smartctl": { 7 | "version": [ 8 | 7, 9 | 1 10 | ], 11 | "svn_revision": "5022", 12 | "platform_info": "x86_64-linux-5.4.0-45-generic", 13 | "build_info": "(local build)", 14 | "argv": [ 15 | "smartctl", 16 | "-j", 17 | "--scan" 18 | ], 19 | "exit_status": 0 20 | }, 21 | "devices": [ 22 | { 23 | "name": "/dev/bus/0", 24 | "info_name": "/dev/bus/0 [megaraid_disk_00]", 25 | "type": "megaraid,0", 26 | "protocol": "SCSI" 27 | }, 28 | { 29 | "name": "/dev/bus/0", 30 | "info_name": "/dev/bus/0 [megaraid_disk_01]", 31 | "type": "megaraid,1", 32 | "protocol": "SCSI" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /collector/pkg/detect/testdata/smartctl_scan_nvme.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_format_version": [ 3 | 1, 4 | 0 5 | ], 6 | "smartctl": { 7 | "version": [ 8 | 7, 9 | 0 10 | ], 11 | "svn_revision": "4883", 12 | "platform_info": "x86_64-linux-4.19.107-Unraid", 13 | "build_info": "(local build)", 14 | "argv": [ 15 | "smartctl", 16 | "-j", 17 | "--scan" 18 | ], 19 | "exit_status": 0 20 | }, 21 | "devices": [ 22 | { 23 | "name": "/dev/nvme0", 24 | "info_name": "/dev/nvme0", 25 | "type": "nvme", 26 | "protocol": "NVMe" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /collector/pkg/detect/wwn_test.go: -------------------------------------------------------------------------------- 1 | package detect_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/analogj/scrutiny/collector/pkg/detect" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | ) 9 | 10 | func TestWwn_FromStringTable(t *testing.T) { 11 | 12 | //setup 13 | var tests = []struct { 14 | wwnStr string 15 | wwn detect.Wwn 16 | }{ 17 | 18 | {"0x5002538e40a22954", detect.Wwn{Naa: 5, Oui: 9528, Id: 61213911380}}, //sda 19 | {"0x5000cca264eb01d7", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283057623}}, //sdb 20 | {"0x5000cca264ec3183", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283135363}}, //sdc 21 | {"0x5000cca252c859cc", detect.Wwn{Naa: 5, Oui: 3274, Id: 9978796492}}, //sdd 22 | {"0x50014ee20b2a72a9", detect.Wwn{Naa: 5, Oui: 5358, Id: 8777265833}}, //sde 23 | {"0x5000cca264ebc248", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283106888}}, //sdf 24 | {"0x5000c500673e6b5f", detect.Wwn{Naa: 5, Oui: 3152, Id: 1732143967}}, //sdg 25 | } 26 | //test 27 | for _, tt := range tests { 28 | testname := fmt.Sprintf("%s", tt.wwnStr) 29 | t.Run(testname, func(t *testing.T) { 30 | str := tt.wwn.ToString() 31 | require.Equal(t, tt.wwnStr, str) 32 | }) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /collector/pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Raised when config file is missing 8 | type ConfigFileMissingError string 9 | 10 | func (str ConfigFileMissingError) Error() string { 11 | return fmt.Sprintf("ConfigFileMissingError: %q", string(str)) 12 | } 13 | 14 | // Raised when the config file doesnt match schema 15 | type ConfigValidationError string 16 | 17 | func (str ConfigValidationError) Error() string { 18 | return fmt.Sprintf("ConfigValidationError: %q", string(str)) 19 | } 20 | 21 | // Raised when a dependency (like smartd or ssh-agent) is missing 22 | type DependencyMissingError string 23 | 24 | func (str DependencyMissingError) Error() string { 25 | return fmt.Sprintf("DependencyMissingError: %q", string(str)) 26 | } 27 | 28 | // Raised when there was an error communicating with API server 29 | type ApiServerCommunicationError string 30 | 31 | func (str ApiServerCommunicationError) Error() string { 32 | return fmt.Sprintf("ApiServerCommunicationError: %q", string(str)) 33 | } 34 | -------------------------------------------------------------------------------- /collector/pkg/errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors_test 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/collector/pkg/errors" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | //func TestCheckErr_WithoutError(t *testing.T) { 10 | // t.Parallel() 11 | // 12 | // //assert 13 | // require.NotPanics(t, func() { 14 | // errors.CheckErr(nil) 15 | // }) 16 | //} 17 | 18 | //func TestCheckErr_Error(t *testing.T) { 19 | // t.Parallel() 20 | // 21 | // //assert 22 | // require.Panics(t, func() { 23 | // errors.CheckErr(stderrors.New("This is an error")) 24 | // }) 25 | //} 26 | 27 | func TestErrors(t *testing.T) { 28 | t.Parallel() 29 | 30 | //assert 31 | require.Implements(t, (*error)(nil), errors.ConfigFileMissingError("test"), "should implement the error interface") 32 | require.Implements(t, (*error)(nil), errors.ConfigValidationError("test"), "should implement the error interface") 33 | require.Implements(t, (*error)(nil), errors.DependencyMissingError("test"), "should implement the error interface") 34 | } 35 | -------------------------------------------------------------------------------- /collector/pkg/models/device.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Device struct { 4 | WWN string `json:"wwn"` 5 | 6 | DeviceName string `json:"device_name"` 7 | DeviceUUID string `json:"device_uuid"` 8 | DeviceSerialID string `json:"device_serial_id"` 9 | DeviceLabel string `json:"device_label"` 10 | 11 | Manufacturer string `json:"manufacturer"` 12 | ModelName string `json:"model_name"` 13 | InterfaceType string `json:"interface_type"` 14 | InterfaceSpeed string `json:"interface_speed"` 15 | SerialNumber string `json:"serial_number"` 16 | Firmware string `json:"firmware"` 17 | RotationSpeed int `json:"rotational_speed"` 18 | Capacity int64 `json:"capacity"` 19 | FormFactor string `json:"form_factor"` 20 | SmartSupport bool `json:"smart_support"` 21 | DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI) 22 | DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector. 23 | 24 | // User provided metadata 25 | Label string `json:"label"` 26 | HostId string `json:"host_id"` 27 | } 28 | 29 | type DeviceWrapper struct { 30 | Success bool `json:"success,omitempty"` 31 | Errors []error `json:"errors,omitempty"` 32 | Data []Device `json:"data"` 33 | } 34 | -------------------------------------------------------------------------------- /collector/pkg/models/scan.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Scan struct { 4 | JSONFormatVersion []int `json:"json_format_version"` 5 | Smartctl struct { 6 | Version []int `json:"version"` 7 | SvnRevision string `json:"svn_revision"` 8 | PlatformInfo string `json:"platform_info"` 9 | BuildInfo string `json:"build_info"` 10 | Argv []string `json:"argv"` 11 | ExitStatus int `json:"exit_status"` 12 | } `json:"smartctl"` 13 | Devices []ScanDevice `json:"devices"` 14 | } 15 | type ScanDevice struct { 16 | Name string `json:"name"` 17 | InfoName string `json:"info_name"` 18 | Type string `json:"type"` 19 | Protocol string `json:"protocol"` 20 | } 21 | -------------------------------------------------------------------------------- /collector/pkg/models/scan_override.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ScanOverride struct { 4 | Device string `mapstructure:"device"` 5 | DeviceType []string `mapstructure:"type"` 6 | Ignore bool `mapstructure:"ignore"` 7 | Commands struct { 8 | MetricsInfoArgs string `mapstructure:"metrics_info_args"` 9 | MetricsSmartArgs string `mapstructure:"metrics_smart_args"` 10 | } `mapstructure:"commands"` 11 | } 12 | -------------------------------------------------------------------------------- /docker/Dockerfile.collector: -------------------------------------------------------------------------------- 1 | ######################################################################################################################## 2 | # Collector Image 3 | ######################################################################################################################## 4 | 5 | 6 | ######## 7 | FROM golang:1.20-bookworm as backendbuild 8 | 9 | WORKDIR /go/src/github.com/analogj/scrutiny 10 | 11 | COPY . /go/src/github.com/analogj/scrutiny 12 | 13 | RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/* 14 | RUN make binary-clean binary-collector 15 | 16 | ######## 17 | FROM debian:bookworm-slim as runtime 18 | WORKDIR /opt/scrutiny 19 | ENV PATH="/opt/scrutiny/bin:${PATH}" 20 | 21 | RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates 22 | 23 | COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh 24 | COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny 25 | COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/ 26 | RUN chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \ 27 | chmod +x /entrypoint-collector.sh && \ 28 | chmod 0644 /etc/cron.d/scrutiny && \ 29 | rm -f /etc/cron.daily/apt /etc/cron.daily/dpkg /etc/cron.daily/passwd 30 | 31 | CMD ["/entrypoint-collector.sh"] 32 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | `rootfs` is only used by Dockerfile and Dockerfile.collector 2 | -------------------------------------------------------------------------------- /docker/entrypoint-collector.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Cron runs in its own isolated environment (usually using only /etc/environment ) 4 | # So when the container starts up, we will do a dump of the runtime environment into a .env file that we 5 | # will then source into the crontab file (/etc/cron.d/scrutiny) 6 | (set -o posix; export -p) > /env.sh 7 | 8 | # adding ability to customize the cron schedule. 9 | COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"} 10 | COLLECTOR_RUN_STARTUP=${COLLECTOR_RUN_STARTUP:-"false"} 11 | COLLECTOR_RUN_STARTUP_SLEEP=${COLLECTOR_RUN_STARTUP_SLEEP:-"1"} 12 | 13 | # if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes 14 | [[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}" 15 | 16 | # replace placeholder with correct value 17 | sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny 18 | 19 | if [[ "${COLLECTOR_RUN_STARTUP}" == "true" ]]; then 20 | sleep ${COLLECTOR_RUN_STARTUP_SLEEP} 21 | echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)" 22 | /opt/scrutiny/bin/scrutiny-collector-metrics run 23 | fi 24 | 25 | 26 | # now that we have the env start cron in the foreground 27 | echo "starting cron" 28 | exec su -c "cron -f -L 15" root 29 | -------------------------------------------------------------------------------- /docker/example.omnibus.docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | scrutiny: 5 | container_name: scrutiny 6 | image: ghcr.io/analogj/scrutiny:master-omnibus 7 | cap_add: 8 | - SYS_RAWIO 9 | ports: 10 | - "8080:8080" # webapp 11 | - "8086:8086" # influxDB admin 12 | volumes: 13 | - /run/udev:/run/udev:ro 14 | - ./config:/opt/scrutiny/config 15 | - ./influxdb:/opt/scrutiny/influxdb 16 | devices: 17 | - "/dev/sda" 18 | - "/dev/sdb" 19 | -------------------------------------------------------------------------------- /docs/INSTALL_ANSIBLE.md: -------------------------------------------------------------------------------- 1 | # Ansible Install 2 | 3 | [Zorlin](https://github.com/Zorlin) has developed and now maintains [an Ansible playbook](https://github.com/Zorlin/scrutiny-playbook) which automates the steps involved in manually setting up Scrutiny. 4 | 5 | Using it is simple: 6 | 7 | * Grab a copy of the playbook 8 | * Follow the directions in the playbook repository 9 | * Run `ansible-playbook site.yml` 10 | * Visit http://your-machine:8080 to see your new Scrutiny installation. 11 | 12 | It will automatically pull metrics from machines once a day, at 1am. 13 | 14 | You can see it in action below. 15 | 16 | [![asciicast](https://asciinema.org/a/493531.svg)](https://asciinema.org/a/493531) 17 | 18 | -------------------------------------------------------------------------------- /docs/INSTALL_NAS.md: -------------------------------------------------------------------------------- 1 | package docs 2 | -------------------------------------------------------------------------------- /docs/SUPPORTED_NAS_OS.md: -------------------------------------------------------------------------------- 1 | # Officially Supported NAS/OS's 2 | 3 | These are the officially supported NAS OS's (with documentation and setup guides). Once a guide is created ( 4 | in `docs/guides/` or elsewhere) it will be linked here. 5 | 6 | - [x] [freenas/truenas](https://blog.stefandroid.com/2022/01/14/smart-scrutiny.html) 7 | - [x] [unraid](./INSTALL_UNRAID.md) 8 | - [ ] ESXI 9 | - [ ] Proxmox 10 | - [x] Synology 11 | - [Hub/Spoke Deployment - Collector](./INSTALL_SYNOLOGY_COLLECTOR.md) 12 | - [Omnibus Deployment](https://drfrankenstein.co.uk/2022/07/28/scrutiny-in-docker-on-a-synology-nas) - Docker Package 13 | - [Omnibus Deployment](https://drfrankenstein.co.uk/scrutiny-in-container-manager-on-a-synology-nas/) - Container Manager Package 14 | - [ ] OMV 15 | - [ ] Amahi 16 | - [ ] Running in a LXC container 17 | - [x] [PFSense](./INSTALL_PFSENSE.md) 18 | - [x] QNAP 19 | - [x] [RockStor](https://rockstor.com/docs/interface/docker-based-rock-ons/scrutiny.html) 20 | - [ ] Solaris/OmniOS CE Support 21 | - [ ] Kubernetes 22 | - [x] [Windows](./INSTALL_MANUAL_WINDOWS.md) 23 | -------------------------------------------------------------------------------- /docs/TESTERS.md: -------------------------------------------------------------------------------- 1 | # Testers 2 | 3 | Scrutiny supports many operating systems, CPU architectures and runtime environments. Unfortunately that makes it incredibly 4 | difficult to test. 5 | Thankfully the following users have been gracious enough to test/validate Scrutiny works on their system. 6 | 7 | > NOTE: If you're interested in volunteering to test Scrutiny beta builds on your system, please [open an issue](https://github.com/AnalogJ/scrutiny/issues). 8 | 9 | | Architecture Name | Binaries | Docker | 10 | | --- | --- | --- | 11 | | linux-amd64 | @TizzAmmazz | @feroxy @rshxyz | 12 | | linux-arm-5 | -- | | 13 | | linux-arm-6 | -- | | 14 | | linux-arm-7 | @Zorlin | @martini1992 | 15 | | linux-arm64 | @SiM22 @Zorlin | @ViRb3 @agneevX @benamajin | 16 | | freebsd-amd64 | @BadCo-NZ @varunsridharan @martadinata666 @KenwoodFox @FingerlessGlov3s | | 17 | | macos-amd64 | -- | -- | 18 | | macos-arm64 | -- | -- | 19 | | windows-amd64 | @gabrielv33 | -- | 20 | | windows-arm64 | -- | -- | 21 | -------------------------------------------------------------------------------- /docs/TROUBLESHOOTING_UDEV.md: -------------------------------------------------------------------------------- 1 | # Operating systems without udev 2 | 3 | Some operating systems do not come with `udev` out of the box, for example Alpine Linux. In these instances you will not be able to bind `/run/udev` to the container for sharing device metadata. Some operating systems offer `udev` as a package that can be installed separately, or an alternative (such as `eudev` in the case of Alpine Linux) that provides the same functionality. 4 | 5 | To install `eudev` in Alpine Linux (run as root): 6 | 7 | ``` 8 | apk add eudev 9 | setup-udev 10 | ``` 11 | 12 | Once your `udev` implementation is installed, create `/run/udev` with the following command: 13 | 14 | ``` 15 | udevadm trigger 16 | ``` 17 | 18 | On Alpine Linux, this also has the benefit of creating symlinks to device serial numbers in `/dev/disk/by-id`. 19 | -------------------------------------------------------------------------------- /docs/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/docs/dashboard.png -------------------------------------------------------------------------------- /docs/details-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/docs/details-full.png -------------------------------------------------------------------------------- /docs/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/docs/details.png -------------------------------------------------------------------------------- /docs/influxdb-admin-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/docs/influxdb-admin-token.png -------------------------------------------------------------------------------- /docs/multiple-host-ids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/docs/multiple-host-ids.png -------------------------------------------------------------------------------- /docs/sponsors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/docs/sponsors.png -------------------------------------------------------------------------------- /packagr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engine_enable_code_mutation: true 3 | mgr_keep_lock_file: true 4 | engine_version_metadata_path: 'webapp/backend/pkg/version/version.go' 5 | engine_golang_package_path: 'github.com/analogj/scrutiny' 6 | 7 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/01-timezone: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | 3 | if [ -n "${TZ}" ] 4 | then 5 | ln -snf "/usr/share/zoneinfo/${TZ}" /etc/localtime 6 | echo "${TZ}" > /etc/timezone 7 | fi 8 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/50-cron-config: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | 3 | # Cron runs in its own isolated environment (usually using only /etc/environment ) 4 | # So when the container starts up, we will do a dump of the runtime environment into a .env file that we 5 | # will then source into the crontab file (/etc/cron.d/scrutiny) 6 | (set -o posix; export -p) > /env.sh 7 | 8 | # adding ability to customize the cron schedule. 9 | COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"} 10 | 11 | # if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes 12 | [[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}" 13 | 14 | # replace placeholder with correct value 15 | sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny 16 | -------------------------------------------------------------------------------- /rootfs/etc/cron.d/scrutiny: -------------------------------------------------------------------------------- 1 | MAILTO="" 2 | # Example of job definition: 3 | # .---------------- minute (0 - 59) 4 | # | .------------- hour (0 - 23) 5 | # | | .---------- day of month (1 - 31) 6 | # | | | .------- month (1 - 12) OR jan,feb,mar,apr ... 7 | # | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat 8 | # | | | | | 9 | # * * * * * user-name command to be executed 10 | 11 | # correctly route collector logs (STDOUT & STDERR) to Cron foreground (collectable by Docker STDOUT) 12 | # cron schedule to run daily at midnight: '0 0 * * *' 13 | # System environmental variables are stripped by cron, source our dump of the docker environmental variables before each command (/env.sh) 14 | {COLLECTOR_CRON_SCHEDULE} root . /env.sh; /opt/scrutiny/bin/scrutiny-collector-metrics run >/proc/1/fd/1 2>/proc/1/fd/2 15 | # An empty line is required at the end of this file for a valid cron file. 16 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/collector-once/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | 3 | # ensure not run (successfully) before 4 | if [ -f /tmp/custom-init-performed ]; then 5 | echo 'INFO: custom init already performed' 6 | s6-svc -D /run/service/collector-once # prevent s6 from restarting service 7 | exit 0 8 | fi 9 | 10 | echo "waiting for scrutiny service to start" 11 | s6-svwait -u /run/service/scrutiny 12 | 13 | # wait until scrutiny is "Ready" 14 | until $(curl --output /dev/null --silent --head --fail http://localhost:8080/api/health); do echo "scrutiny api not ready" && sleep 5; done 15 | 16 | echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)" 17 | /opt/scrutiny/bin/scrutiny-collector-metrics run 18 | 19 | # prevent script's core logic from running again 20 | touch /tmp/custom-init-performed 21 | 22 | # prevent s6 from restarting service 23 | s6-svc -D /run/service/collector-once 24 | 25 | exit 0 26 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/cron/finish: -------------------------------------------------------------------------------- 1 | #!/command/execlineb -S0 2 | 3 | echo "cron exiting" 4 | s6-svscanctl -t /var/run/s6/services 5 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/cron/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | 3 | echo "starting cron" 4 | cron -f -L 15 5 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/influxdb/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | 3 | mkdir -p /opt/scrutiny/influxdb/ 4 | 5 | if [ -f "/opt/scrutiny/influxdb/config.yaml" ]; then 6 | echo "influxdb config file already exists. skipping." 7 | else 8 | cat << 'EOF' > /opt/scrutiny/influxdb/config.yaml 9 | bolt-path: /opt/scrutiny/influxdb/influxd.bolt 10 | engine-path: /opt/scrutiny/influxdb/engine 11 | http-bind-address: ":8086" 12 | reporting-disabled: true 13 | EOF 14 | fi 15 | 16 | echo "starting influxdb" 17 | influxd run 18 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/scrutiny/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | 3 | echo "waiting for influxdb" 4 | until $(curl --output /dev/null --silent --head --fail http://localhost:8086/health); do echo "influxdb not ready" && sleep 5; done 5 | 6 | echo "starting scrutiny" 7 | scrutiny start 8 | -------------------------------------------------------------------------------- /webapp/backend/pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | func Test_MergeConfigMap(t *testing.T) { 10 | //setup 11 | testConfig := configuration{ 12 | Viper: viper.New(), 13 | } 14 | testConfig.Set("user.dashboard_display", "hello") 15 | testConfig.SetDefault("user.layout", "hello") 16 | 17 | mergeSettings := map[string]interface{}{ 18 | "user": map[string]interface{}{ 19 | "dashboard_display": "dashboard_display", 20 | "layout": "layout", 21 | }, 22 | } 23 | //test 24 | err := testConfig.MergeConfigMap(mergeSettings) 25 | 26 | //verify 27 | require.NoError(t, err) 28 | 29 | // if using Set, the MergeConfigMap functionality will not override 30 | // if using SetDefault, the MergeConfigMap will override correctly 31 | require.Equal(t, "hello", testConfig.GetString("user.dashboard_display")) 32 | require.Equal(t, "layout", testConfig.GetString("user.layout")) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /webapp/backend/pkg/config/factory.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | func Create() (Interface, error) { 4 | config := new(configuration) 5 | if err := config.Init(); err != nil { 6 | return nil, err 7 | } 8 | return config, nil 9 | } 10 | -------------------------------------------------------------------------------- /webapp/backend/pkg/config/interface.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | ) 6 | 7 | // Create mock using: 8 | // mockgen -source=webapp/backend/pkg/config/interface.go -destination=webapp/backend/pkg/config/mock/mock_config.go 9 | type Interface interface { 10 | Init() error 11 | ReadConfig(configFilePath string) error 12 | WriteConfig() error 13 | Set(key string, value interface{}) 14 | SetDefault(key string, value interface{}) 15 | MergeConfigMap(cfg map[string]interface{}) error 16 | 17 | Sub(key string) Interface 18 | AllSettings() map[string]interface{} 19 | AllKeys() []string 20 | SubKeys(key string) []string 21 | IsSet(key string) bool 22 | Get(key string) interface{} 23 | GetBool(key string) bool 24 | GetInt(key string) int 25 | GetInt64(key string) int64 26 | GetString(key string) string 27 | GetStringSlice(key string) []string 28 | UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error 29 | } 30 | -------------------------------------------------------------------------------- /webapp/backend/pkg/database/migrations/m20201107210306/smart.go: -------------------------------------------------------------------------------- 1 | package m20201107210306 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | // Deprecated: m20201107210306.Smart is deprecated, only used by db migrations 9 | type Smart struct { 10 | gorm.Model 11 | 12 | DeviceWWN string `json:"device_wwn"` 13 | Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key 14 | 15 | TestDate time.Time `json:"date"` 16 | SmartStatus string `json:"smart_status"` // SmartStatusPassed or SmartStatusFailed 17 | 18 | //Metrics 19 | Temp int64 `json:"temp"` 20 | PowerOnHours int64 `json:"power_on_hours"` 21 | PowerCycleCount int64 `json:"power_cycle_count"` 22 | 23 | AtaAttributes []SmartAtaAttribute `json:"ata_attributes" gorm:"foreignkey:SmartId"` 24 | NvmeAttributes []SmartNvmeAttribute `json:"nvme_attributes" gorm:"foreignkey:SmartId"` 25 | ScsiAttributes []SmartScsiAttribute `json:"scsi_attributes" gorm:"foreignkey:SmartId"` 26 | } 27 | -------------------------------------------------------------------------------- /webapp/backend/pkg/database/migrations/m20201107210306/smart_ata_attribute.go: -------------------------------------------------------------------------------- 1 | package m20201107210306 2 | 3 | import "gorm.io/gorm" 4 | 5 | // Deprecated: m20201107210306.SmartAtaAttribute is deprecated, only used by db migrations 6 | type SmartAtaAttribute struct { 7 | gorm.Model 8 | 9 | SmartId int `json:"smart_id"` 10 | Smart Device `json:"-" gorm:"foreignkey:SmartId"` // use SmartId as foreign key 11 | 12 | AttributeId int `json:"attribute_id"` 13 | Name string `json:"name"` 14 | Value int `json:"value"` 15 | Worst int `json:"worst"` 16 | Threshold int `json:"thresh"` 17 | RawValue int64 `json:"raw_value"` 18 | RawString string `json:"raw_string"` 19 | WhenFailed string `json:"when_failed"` 20 | 21 | TransformedValue int64 `json:"transformed_value"` 22 | Status string `gorm:"-" json:"status,omitempty"` 23 | StatusReason string `gorm:"-" json:"status_reason,omitempty"` 24 | FailureRate float64 `gorm:"-" json:"failure_rate,omitempty"` 25 | History []SmartAtaAttribute `gorm:"-" json:"history,omitempty"` 26 | } 27 | -------------------------------------------------------------------------------- /webapp/backend/pkg/database/migrations/m20201107210306/smart_nvme_attribute.go: -------------------------------------------------------------------------------- 1 | package m20201107210306 2 | 3 | import "gorm.io/gorm" 4 | 5 | // Deprecated: m20201107210306.SmartNvmeAttribute is deprecated, only used by db migrations 6 | type SmartNvmeAttribute struct { 7 | gorm.Model 8 | 9 | SmartId int `json:"smart_id"` 10 | Smart Device `json:"-" gorm:"foreignkey:SmartId"` // use SmartId as foreign key 11 | 12 | AttributeId string `json:"attribute_id"` //json string from smartctl 13 | Name string `json:"name"` 14 | Value int `json:"value"` 15 | Threshold int `json:"thresh"` 16 | 17 | TransformedValue int64 `json:"transformed_value"` 18 | Status string `gorm:"-" json:"status,omitempty"` 19 | StatusReason string `gorm:"-" json:"status_reason,omitempty"` 20 | FailureRate float64 `gorm:"-" json:"failure_rate,omitempty"` 21 | History []SmartNvmeAttribute `gorm:"-" json:"history,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /webapp/backend/pkg/database/migrations/m20201107210306/smart_scsci_attribute.go: -------------------------------------------------------------------------------- 1 | package m20201107210306 2 | 3 | import "gorm.io/gorm" 4 | 5 | // Deprecated: m20201107210306.SmartScsiAttribute is deprecated, only used by db migrations 6 | type SmartScsiAttribute struct { 7 | gorm.Model 8 | 9 | SmartId int `json:"smart_id"` 10 | Smart Device `json:"-" gorm:"foreignkey:SmartId"` // use SmartId as foreign key 11 | 12 | AttributeId string `json:"attribute_id"` //json string from smartctl 13 | Name string `json:"name"` 14 | Value int `json:"value"` 15 | Threshold int `json:"thresh"` 16 | 17 | TransformedValue int64 `json:"transformed_value"` 18 | Status string `gorm:"-" json:"status,omitempty"` 19 | StatusReason string `gorm:"-" json:"status_reason,omitempty"` 20 | FailureRate float64 `gorm:"-" json:"failure_rate,omitempty"` 21 | History []SmartScsiAttribute `gorm:"-" json:"history,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /webapp/backend/pkg/database/migrations/m20220716214900/setting.go: -------------------------------------------------------------------------------- 1 | package m20220716214900 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Setting struct { 8 | //GORM attributes, see: http://gorm.io/docs/conventions.html 9 | gorm.Model 10 | 11 | SettingKeyName string `json:"setting_key_name"` 12 | SettingKeyDescription string `json:"setting_key_description"` 13 | SettingDataType string `json:"setting_data_type"` 14 | 15 | SettingValueNumeric int `json:"setting_value_numeric"` 16 | SettingValueString string `json:"setting_value_string"` 17 | SettingValueBool bool `json:"setting_value_bool"` 18 | } 19 | -------------------------------------------------------------------------------- /webapp/backend/pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Raised when config file is missing 8 | type ConfigFileMissingError string 9 | 10 | func (str ConfigFileMissingError) Error() string { 11 | return fmt.Sprintf("ConfigFileMissingError: %q", string(str)) 12 | } 13 | 14 | // Raised when the config file doesnt match schema 15 | type ConfigValidationError string 16 | 17 | func (str ConfigValidationError) Error() string { 18 | return fmt.Sprintf("ConfigValidationError: %q", string(str)) 19 | } 20 | 21 | // Raised when a dependency (like smartd or ssh-agent) is missing 22 | type DependencyMissingError string 23 | 24 | func (str DependencyMissingError) Error() string { 25 | return fmt.Sprintf("DependencyMissingError: %q", string(str)) 26 | } 27 | 28 | // Raised when the notification system is incorrectly configured 29 | type NotificationValidationError string 30 | 31 | func (str NotificationValidationError) Error() string { 32 | return fmt.Sprintf("NotificationValidationError: %q", string(str)) 33 | } 34 | -------------------------------------------------------------------------------- /webapp/backend/pkg/errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors_test 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/errors" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | //func TestCheckErr_WithoutError(t *testing.T) { 10 | // t.Parallel() 11 | // 12 | // //assert 13 | // require.NotPanics(t, func() { 14 | // errors.CheckErr(nil) 15 | // }) 16 | //} 17 | 18 | //func TestCheckErr_Error(t *testing.T) { 19 | // t.Parallel() 20 | // 21 | // //assert 22 | // require.Panics(t, func() { 23 | // errors.CheckErr(stderrors.New("This is an error")) 24 | // }) 25 | //} 26 | 27 | func TestErrors(t *testing.T) { 28 | t.Parallel() 29 | 30 | //assert 31 | require.Implements(t, (*error)(nil), errors.ConfigFileMissingError("test"), "should implement the error interface") 32 | require.Implements(t, (*error)(nil), errors.ConfigValidationError("test"), "should implement the error interface") 33 | require.Implements(t, (*error)(nil), errors.DependencyMissingError("test"), "should implement the error interface") 34 | } 35 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/collector/smart_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSmartInfo_Capacity(t *testing.T) { 10 | t.Run("should report nvme capacity", func(t *testing.T) { 11 | smartInfo := SmartInfo{ 12 | UserCapacity: UserCapacity{ 13 | Bytes: 1234, 14 | }, 15 | NvmeTotalCapacity: 5678, 16 | } 17 | assert.Equal(t, int64(5678), smartInfo.Capacity()) 18 | }) 19 | 20 | t.Run("should report user capacity", func(t *testing.T) { 21 | smartInfo := SmartInfo{ 22 | UserCapacity: UserCapacity{ 23 | Bytes: 1234, 24 | }, 25 | } 26 | assert.Equal(t, int64(1234), smartInfo.Capacity()) 27 | }) 28 | 29 | t.Run("should report 0 for unknown capacities", func(t *testing.T) { 30 | var smartInfo SmartInfo 31 | assert.Zero(t, smartInfo.Capacity()) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/device_summary.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" 5 | "time" 6 | ) 7 | 8 | type DeviceSummaryWrapper struct { 9 | Success bool `json:"success"` 10 | Errors []error `json:"errors"` 11 | Data struct { 12 | Summary map[string]*DeviceSummary `json:"summary"` 13 | } `json:"data"` 14 | } 15 | 16 | type DeviceSummary struct { 17 | Device Device `json:"device"` 18 | 19 | SmartResults *SmartSummary `json:"smart,omitempty"` 20 | TempHistory []measurements.SmartTemperature `json:"temp_history,omitempty"` 21 | } 22 | type SmartSummary struct { 23 | // Collector Summary Data 24 | CollectorDate time.Time `json:"collector_date,omitempty"` 25 | Temp int64 `json:"temp,omitempty"` 26 | PowerOnHours int64 `json:"power_on_hours,omitempty"` 27 | } 28 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/measurements/smart_attribute.go: -------------------------------------------------------------------------------- 1 | package measurements 2 | 3 | import "github.com/analogj/scrutiny/webapp/backend/pkg" 4 | 5 | type SmartAttribute interface { 6 | Flatten() (fields map[string]interface{}) 7 | Inflate(key string, val interface{}) 8 | GetStatus() pkg.AttributeStatus 9 | GetTransformedValue() int64 10 | } 11 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/measurements/smart_temperature.go: -------------------------------------------------------------------------------- 1 | package measurements 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type SmartTemperature struct { 8 | Date time.Time `json:"date"` 9 | Temp int64 `json:"temp"` 10 | } 11 | 12 | func (st *SmartTemperature) Flatten() (tags map[string]string, fields map[string]interface{}) { 13 | fields = map[string]interface{}{ 14 | "temp": st.Temp, 15 | } 16 | tags = map[string]string{} 17 | 18 | return tags, fields 19 | } 20 | 21 | func (st *SmartTemperature) Inflate(key string, val interface{}) { 22 | if val == nil { 23 | return 24 | } 25 | 26 | if key == "temp" { 27 | switch t := val.(type) { 28 | case int64: 29 | st.Temp = t 30 | case float64: 31 | st.Temp = int64(t) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/setting_entry.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // SettingEntry matches a setting row in the database 8 | type SettingEntry struct { 9 | //GORM attributes, see: http://gorm.io/docs/conventions.html 10 | gorm.Model 11 | 12 | SettingKeyName string `json:"setting_key_name" gorm:"unique;not null"` 13 | SettingKeyDescription string `json:"setting_key_description"` 14 | SettingDataType string `json:"setting_data_type"` 15 | 16 | SettingValueNumeric int `json:"setting_value_numeric"` 17 | SettingValueString string `json:"setting_value_string"` 18 | SettingValueBool bool `json:"setting_value_bool"` 19 | } 20 | 21 | func (s SettingEntry) TableName() string { 22 | return "settings" 23 | } 24 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/testdata/smart-fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_format_version": [ 3 | 1, 4 | 0 5 | ], 6 | "smartctl": { 7 | "version": [ 8 | 7, 9 | 0 10 | ], 11 | "svn_revision": "4883", 12 | "platform_info": "x86_64-w64-mingw32-win7-sp1", 13 | "build_info": "(sf-7.0-1)", 14 | "argv": [ 15 | "smartctl", 16 | "--all", 17 | "-j", 18 | "/dev/sda" 19 | ], 20 | "messages": [ 21 | { 22 | "string": "Smartctl open device: /dev/sda failed: \\\\.\\PhysicalDrive0: Open failed, Error=5", 23 | "severity": "error" 24 | } 25 | ], 26 | "exit_status": 2 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/testdata/smart-pass.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_format_version": [ 3 | 1, 4 | 0 5 | ], 6 | "smartctl": { 7 | "version": [ 8 | 7, 9 | 0 10 | ], 11 | "svn_revision": "4883", 12 | "platform_info": "x86_64-linux-5.4.6-arch3-1", 13 | "build_info": "(local build)", 14 | "argv": [ 15 | "smartctl", 16 | "-jH", 17 | "/dev/sdc" 18 | ], 19 | "exit_status": 0 20 | }, 21 | "device": { 22 | "name": "/dev/sdc", 23 | "info_name": "/dev/sdc [SAT]", 24 | "type": "sat", 25 | "protocol": "ATA" 26 | }, 27 | "smart_status": { 28 | "passed": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webapp/backend/pkg/models/testdata/smart-raid.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_format_version": [ 3 | 1, 4 | 0 5 | ], 6 | "smartctl": { 7 | "version": [ 8 | 7, 9 | 0 10 | ], 11 | "svn_revision": "4883", 12 | "platform_info": "x86_64-w64-mingw32-w10-1809", 13 | "build_info": "(sf-7.0-1)", 14 | "argv": [ 15 | "smartctl", 16 | "--all", 17 | "-j", 18 | "/dev/sdb" 19 | ], 20 | "exit_status": 4 21 | }, 22 | "device": { 23 | "name": "/dev/sdb", 24 | "info_name": "/dev/sdb", 25 | "type": "scsi", 26 | "protocol": "SCSI" 27 | }, 28 | "vendor": "Intel", 29 | "product": "Raid 1 Volume", 30 | "model_name": "Intel Raid 1 Volume", 31 | "revision": "1.0.", 32 | "scsi_version": "SPC-3", 33 | "user_capacity": { 34 | "blocks": 1953519616, 35 | "bytes": 1000202043392 36 | }, 37 | "logical_block_size": 512, 38 | "physical_block_size": 4096, 39 | "rotation_rate": 7200, 40 | "serial_number": "Volume1", 41 | "device_type": { 42 | "scsi_value": 0, 43 | "name": "disk" 44 | }, 45 | "local_time": { 46 | "time_t": 1637039918, 47 | "asctime": "Wed Oct 09 10:31:07 2019 RDT" 48 | }, 49 | "temperature": { 50 | "current": 0, 51 | "drive_trip": 0 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /webapp/backend/pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // VERSION is the app-global version string, which will be replaced with a 4 | // new value during packaging 5 | const VERSION = "0.8.1" 6 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/archive_device.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | ) 9 | 10 | func ArchiveDevice(c *gin.Context) { 11 | logger := c.MustGet("LOGGER").(*logrus.Entry) 12 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 13 | 14 | err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), true) 15 | if err != nil { 16 | logger.Errorln("An error occurred while archiving device", err) 17 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 18 | return 19 | } 20 | 21 | c.JSON(http.StatusOK, gin.H{"success": true}) 22 | } 23 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/delete_device.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | ) 9 | 10 | func DeleteDevice(c *gin.Context) { 11 | logger := c.MustGet("LOGGER").(*logrus.Entry) 12 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 13 | 14 | err := deviceRepo.DeleteDevice(c, c.Param("wwn")) 15 | if err != nil { 16 | logger.Errorln("An error occurred while deleting device", err) 17 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 18 | return 19 | } 20 | 21 | c.JSON(http.StatusOK, gin.H{"success": true}) 22 | } 23 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/get_devices_summary.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | ) 9 | 10 | func GetDevicesSummary(c *gin.Context) { 11 | logger := c.MustGet("LOGGER").(*logrus.Entry) 12 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 13 | 14 | summary, err := deviceRepo.GetSummary(c) 15 | if err != nil { 16 | logger.Errorln("An error occurred while retrieving device summary", err) 17 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 18 | return 19 | } 20 | 21 | //this must match DeviceSummaryWrapper (webapp/backend/pkg/models/device_summary.go) 22 | c.JSON(http.StatusOK, gin.H{ 23 | "success": true, 24 | "data": map[string]interface{}{ 25 | "summary": summary, 26 | //"temperature": tem 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/get_devices_summary_temp_history.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | ) 9 | 10 | func GetDevicesSummaryTempHistory(c *gin.Context) { 11 | logger := c.MustGet("LOGGER").(*logrus.Entry) 12 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 13 | 14 | durationKey, exists := c.GetQuery("duration_key") 15 | if !exists { 16 | durationKey = "week" 17 | } 18 | 19 | tempHistory, err := deviceRepo.GetSmartTemperatureHistory(c, durationKey) 20 | if err != nil { 21 | logger.Errorln("An error occurred while retrieving summary/temp history", err) 22 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 23 | return 24 | } 25 | 26 | c.JSON(http.StatusOK, gin.H{ 27 | "success": true, 28 | "data": map[string]interface{}{ 29 | "temp_history": tempHistory, 30 | }, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/get_settings.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | ) 9 | 10 | func GetSettings(c *gin.Context) { 11 | logger := c.MustGet("LOGGER").(*logrus.Entry) 12 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 13 | 14 | settings, err := deviceRepo.LoadSettings(c) 15 | if err != nil { 16 | logger.Errorln("An error occurred while retrieving settings", err) 17 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 18 | return 19 | } 20 | 21 | c.JSON(http.StatusOK, gin.H{ 22 | "success": true, 23 | "settings": settings, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/health_check.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | ) 9 | 10 | func HealthCheck(c *gin.Context) { 11 | logger := c.MustGet("LOGGER").(*logrus.Entry) 12 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 13 | logger.Infof("Checking Influxdb & Sqlite health") 14 | 15 | //check sqlite and influxdb health 16 | err := deviceRepo.HealthCheck(c) 17 | if err != nil { 18 | logger.Errorln("An error occurred during healthcheck", err) 19 | c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) 20 | return 21 | } 22 | 23 | //TODO: 24 | // check if the /web folder is populated. 25 | 26 | c.JSON(http.StatusOK, gin.H{ 27 | "success": true, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/save_settings.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/analogj/scrutiny/webapp/backend/pkg/models" 6 | "github.com/gin-gonic/gin" 7 | "github.com/sirupsen/logrus" 8 | "net/http" 9 | ) 10 | 11 | func SaveSettings(c *gin.Context) { 12 | logger := c.MustGet("LOGGER").(*logrus.Entry) 13 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 14 | 15 | var settings models.Settings 16 | err := c.BindJSON(&settings) 17 | if err != nil { 18 | logger.Errorln("Cannot parse updated settings", err) 19 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 20 | return 21 | } 22 | 23 | err = deviceRepo.SaveSettings(c, settings) 24 | if err != nil { 25 | logger.Errorln("An error occurred while saving settings", err) 26 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 27 | return 28 | } 29 | 30 | c.JSON(http.StatusOK, gin.H{ 31 | "success": true, 32 | "settings": settings, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/send_test_notification.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg" 5 | "github.com/analogj/scrutiny/webapp/backend/pkg/config" 6 | "github.com/analogj/scrutiny/webapp/backend/pkg/models" 7 | "github.com/analogj/scrutiny/webapp/backend/pkg/notify" 8 | "github.com/gin-gonic/gin" 9 | "github.com/sirupsen/logrus" 10 | "net/http" 11 | ) 12 | 13 | // Send test notification 14 | func SendTestNotification(c *gin.Context) { 15 | appConfig := c.MustGet("CONFIG").(config.Interface) 16 | logger := c.MustGet("LOGGER").(*logrus.Entry) 17 | 18 | testNotify := notify.New( 19 | logger, 20 | appConfig, 21 | models.Device{ 22 | SerialNumber: "FAKEWDDJ324KSO", 23 | DeviceType: pkg.DeviceProtocolAta, 24 | DeviceName: "/dev/sda", 25 | }, 26 | true, 27 | ) 28 | err := testNotify.Send() 29 | if err != nil { 30 | logger.Errorln("An error occurred while sending test notification", err) 31 | c.JSON(http.StatusInternalServerError, gin.H{ 32 | "success": false, 33 | "errors": []string{err.Error()}, 34 | }) 35 | } else { 36 | c.JSON(http.StatusOK, models.DeviceWrapper{ 37 | Success: true, 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/unarchive_device.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | ) 9 | 10 | func UnarchiveDevice(c *gin.Context) { 11 | logger := c.MustGet("LOGGER").(*logrus.Entry) 12 | deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) 13 | 14 | err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false) 15 | if err != nil { 16 | logger.Errorln("An error occurred while unarchiving device", err) 17 | c.JSON(http.StatusInternalServerError, gin.H{"success": false}) 18 | return 19 | } 20 | 21 | c.JSON(http.StatusOK, gin.H{"success": true}) 22 | } 23 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/handler/upload_device_self_tests.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func UploadDeviceSelfTests(c *gin.Context) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/middleware/config.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/analogj/scrutiny/webapp/backend/pkg/config" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func ConfigMiddleware(appConfig config.Interface) gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | c.Set("CONFIG", appConfig) 11 | c.Next() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/middleware/repository.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/analogj/scrutiny/webapp/backend/pkg/config" 6 | "github.com/analogj/scrutiny/webapp/backend/pkg/database" 7 | "github.com/gin-gonic/gin" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldLogger) gin.HandlerFunc { 12 | 13 | deviceRepo, err := database.NewScrutinyRepository(appConfig, globalLogger) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | // ensure the settings have been loaded into the app config during startup. 19 | _, err = deviceRepo.LoadSettings(context.Background()) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | //settings.UpdateSettingEntries() 25 | 26 | //TODO: determine where we can call defer deviceRepo.Close() 27 | return func(c *gin.Context) { 28 | c.Set("DEVICE_REPOSITORY", deviceRepo) 29 | c.Next() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/testdata/register-devices-req-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "wwn": "a4c8e8ed-11a0-4c97-9bba-306440f1b944", 5 | "device_name": "nvme0", 6 | "manufacturer": "", 7 | "model_name": "Force MP510", 8 | "interface_type": "", 9 | "interface_speed": "", 10 | "serial_number": "a4c8e8ed-11a0-4c97-9bba-306440f1b944", 11 | "firmware": "ECFM12.3", 12 | "rotational_speed": 0, 13 | "capacity": 480103981056, 14 | "form_factor": "", 15 | "smart_support": false, 16 | "device_protocol": "NVMe", 17 | "device_type": "nvme" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /webapp/backend/pkg/web/testdata/register-devices-single-req.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "wwn": "0x5000cca264eb01d7", 5 | "device_name": "sdb", 6 | "manufacturer": "ATA", 7 | "model_name": "WDC_WD140EDFZ-11A0VA0", 8 | "interface_type": "SCSI", 9 | "interface_speed": "", 10 | "serial_number": "9RK1XXXXX", 11 | "firmware": "", 12 | "rotational_speed": 0, 13 | "capacity": 14000519643136, 14 | "form_factor": "", 15 | "smart_support": false 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /webapp/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /webapp/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | /dist 49 | 50 | /coverage 51 | -------------------------------------------------------------------------------- /webapp/frontend/LICENSE.md: -------------------------------------------------------------------------------- 1 | Envato Standard License 2 | 3 | Copyright (c) Sercan Yemen 4 | 5 | This project is protected by Envato's Standard License. For more information, 6 | check the official license page at [https://themeforest.net/licenses/standard](https://themeforest.net/licenses/standard) 7 | -------------------------------------------------------------------------------- /webapp/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Treo - Admin template and Starter project for Angular 2 | 3 | ## Development server 4 | 5 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 6 | 7 | ## Code scaffolding 8 | 9 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 10 | 11 | ## Build 12 | 13 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 14 | 15 | ## Running unit tests 16 | 17 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 18 | 19 | ## Running end-to-end tests 20 | 21 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 22 | 23 | ## Further help 24 | 25 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 26 | -------------------------------------------------------------------------------- /webapp/frontend/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /webapp/frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const {SpecReporter} = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs : [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities : { 16 | browserName: 'chrome' 17 | }, 18 | directConnect : true, 19 | baseUrl : 'http://localhost:4200/', 20 | framework : 'jasmine', 21 | jasmineNodeOpts : { 22 | showColors : true, 23 | defaultTimeoutInterval: 30000, 24 | print : function () 25 | { 26 | } 27 | }, 28 | onPrepare() 29 | { 30 | require('ts-node').register({ 31 | project: require('path').join(__dirname, './tsconfig.json') 32 | }); 33 | jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}})); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /webapp/frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to Treo!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /webapp/frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage 4 | { 5 | navigateTo(): Promise 6 | { 7 | return browser.get(browser.baseUrl) as Promise; 8 | } 9 | 10 | getTitleText(): Promise 11 | { 12 | return element(by.css('app-root h1')).getText() as Promise; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /webapp/frontend/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webapp/frontend/git.version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -z "${CI}" ]]; then 4 | echo "running locally (not in Github Actions). generating version file from git client" 5 | GIT_TAG=`git describe --tags` 6 | GIT_BRANCH=`git rev-parse --abbrev-ref HEAD` 7 | 8 | if [[ "$GIT_BRANCH" == "master" ]]; then 9 | VERSION_INFO="${GIT_TAG}" 10 | else 11 | VERSION_INFO="${GIT_BRANCH}#${GIT_TAG}" 12 | fi 13 | else 14 | echo "running in Github Actions, generating version file from environmental variables" 15 | # https://docs.github.com/en/actions/learn-github-actions/environment-variables 16 | VERSION_INFO="${GITHUB_REF_NAME}" 17 | 18 | if [[ "$GITHUB_REF_TYPE" == "branch" ]]; then 19 | VERSION_INFO="${VERSION_INFO}#${GITHUB_SHA::7}" 20 | fi 21 | fi 22 | 23 | echo "writing version file (version: ${VERSION_INFO})" 24 | cat < src/environments/versions.ts 25 | // this file is automatically generated by git.version.ts script 26 | export const versionInfo = { 27 | version: '${VERSION_INFO}', 28 | }; 29 | EOT 30 | -------------------------------------------------------------------------------- /webapp/frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) 5 | { 6 | config.set({ 7 | basePath : '', 8 | frameworks : ['jasmine', '@angular-devkit/build-angular'], 9 | plugins : [ 10 | require('karma-jasmine'), 11 | require('karma-chrome-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage'), 14 | require('@angular-devkit/build-angular/plugins/karma') 15 | ], 16 | client: { 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, './coverage'), 21 | reports: ['html', 'lcovonly', 'text-summary'], 22 | fixWebpackSourcePaths: true 23 | }, 24 | reporters: ['progress', 'kjhtml'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: true, 29 | browsers: ['Chrome'], 30 | singleRun: false, 31 | restartOnFileChange: true 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/animations/defaults.ts: -------------------------------------------------------------------------------- 1 | export class TreoAnimationCurves 2 | { 3 | static STANDARD_CURVE = 'cubic-bezier(0.4, 0.0, 0.2, 1)'; 4 | static DECELERATION_CURVE = 'cubic-bezier(0.0, 0.0, 0.2, 1)'; 5 | static ACCELERATION_CURVE = 'cubic-bezier(0.4, 0.0, 1, 1)'; 6 | static SHARP_CURVE = 'cubic-bezier(0.4, 0.0, 0.6, 1)'; 7 | } 8 | 9 | export class TreoAnimationDurations 10 | { 11 | static COMPLEX = '375ms'; 12 | static ENTERING = '225ms'; 13 | static EXITING = '195ms'; 14 | } 15 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/animations/expand-collapse.ts: -------------------------------------------------------------------------------- 1 | import { animate, state, style, transition, trigger } from '@angular/animations'; 2 | import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults'; 3 | 4 | // ----------------------------------------------------------------------------------------------------- 5 | // @ Expand / collapse 6 | // ----------------------------------------------------------------------------------------------------- 7 | const expandCollapse = trigger('expandCollapse', 8 | [ 9 | state('void, collapsed', 10 | style({ 11 | height: '0' 12 | }) 13 | ), 14 | 15 | state('*, expanded', 16 | style('*') 17 | ), 18 | 19 | // Prevent the transition if the state is false 20 | transition('void <=> false, collapsed <=> false, expanded <=> false', []), 21 | 22 | // Transition 23 | transition('void <=> *, collapsed <=> expanded', 24 | animate('{{timings}}'), 25 | { 26 | params: { 27 | timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}` 28 | } 29 | } 30 | ) 31 | ] 32 | ); 33 | 34 | export { expandCollapse }; 35 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/animations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/animations/public-api.ts: -------------------------------------------------------------------------------- 1 | import { expandCollapse } from './expand-collapse'; 2 | import { fadeIn, fadeInBottom, fadeInLeft, fadeInRight, fadeInTop, fadeOut, fadeOutBottom, fadeOutLeft, fadeOutRight, fadeOutTop } from './fade'; 3 | import { shake } from './shake'; 4 | import { slideInBottom, slideInLeft, slideInRight, slideInTop, slideOutBottom, slideOutLeft, slideOutRight, slideOutTop } from './slide'; 5 | import { zoomIn, zoomOut } from './zoom'; 6 | 7 | export const TreoAnimations = [ 8 | expandCollapse, 9 | fadeIn, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight, 10 | fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight, 11 | shake, 12 | slideInTop, slideInBottom, slideInLeft, slideInRight, 13 | slideOutTop, slideOutBottom, slideOutLeft, slideOutRight, 14 | zoomIn, zoomOut 15 | ]; 16 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/card/card.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
26 | 27 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/card/card.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TreoCardComponent } from '@treo/components/card/card.component'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | TreoCardComponent 8 | ], 9 | imports : [ 10 | CommonModule 11 | ], 12 | exports : [ 13 | TreoCardComponent 14 | ] 15 | }) 16 | export class TreoCardModule 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/card/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/card/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/card/card.component'; 2 | export * from '@treo/components/card/card.module'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/date-range/date-range.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatDatepickerModule } from '@angular/material/datepicker'; 6 | import { MatFormFieldModule } from '@angular/material/form-field'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { MatMomentDateModule } from '@angular/material-moment-adapter'; 10 | import { TreoDateRangeComponent } from '@treo/components/date-range/date-range.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | TreoDateRangeComponent 15 | ], 16 | imports : [ 17 | CommonModule, 18 | ReactiveFormsModule, 19 | MatButtonModule, 20 | MatDatepickerModule, 21 | MatFormFieldModule, 22 | MatInputModule, 23 | MatIconModule, 24 | MatMomentDateModule 25 | ], 26 | exports : [ 27 | TreoDateRangeComponent 28 | ] 29 | }) 30 | export class TreoDateRangeModule 31 | { 32 | } 33 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/date-range/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/date-range/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/date-range/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/date-range/date-range.component'; 2 | export * from '@treo/components/date-range/date-range.module'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/drawer/drawer.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/drawer/drawer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TreoDrawerComponent } from '@treo/components/drawer/drawer.component'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | TreoDrawerComponent 8 | ], 9 | imports : [ 10 | CommonModule 11 | ], 12 | exports : [ 13 | TreoDrawerComponent 14 | ] 15 | }) 16 | export class TreoDrawerModule 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/drawer/drawer.types.ts: -------------------------------------------------------------------------------- 1 | export type TreoDrawerMode = 'over' | 'side'; 2 | export type TreoDrawerPosition = 'left' | 'right'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/drawer/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/drawer/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/drawer/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/drawer/drawer.component'; 2 | export * from '@treo/components/drawer/drawer.module'; 3 | export * from '@treo/components/drawer/drawer.service'; 4 | export * from '@treo/components/drawer/drawer.types'; 5 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/highlight/highlight.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
8 |
9 | 10 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/highlight/highlight.component.scss: -------------------------------------------------------------------------------- 1 | textarea[treo-highlight] { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/highlight/highlight.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TreoHighlightComponent } from '@treo/components/highlight/highlight.component'; 4 | 5 | @NgModule({ 6 | declarations : [ 7 | TreoHighlightComponent 8 | ], 9 | imports : [ 10 | CommonModule 11 | ], 12 | exports : [ 13 | TreoHighlightComponent 14 | ], 15 | entryComponents: [ 16 | TreoHighlightComponent 17 | ] 18 | }) 19 | export class TreoHighlightModule 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/highlight/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/highlight/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/highlight/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/highlight/highlight.component'; 2 | export * from '@treo/components/highlight/highlight.module'; 3 | export * from '@treo/components/highlight/highlight.service'; 4 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/message/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/message/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/message/message.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { TreoMessageComponent } from '@treo/components/message/message.component'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | TreoMessageComponent 10 | ], 11 | imports : [ 12 | CommonModule, 13 | MatButtonModule, 14 | MatIconModule 15 | ], 16 | exports : [ 17 | TreoMessageComponent 18 | ] 19 | }) 20 | export class TreoMessageModule 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/message/message.types.ts: -------------------------------------------------------------------------------- 1 | export type TreoMessageAppearance = 'border' | 'fill' | 'outline'; 2 | export type TreoMessageType = 'primary' | 'accent' | 'warn' | 'basic' | 'info' | 'success' | 'warning' | 'error'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/message/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/message/message.component'; 2 | export * from '@treo/components/message/message.module'; 3 | export * from '@treo/components/message/message.service'; 4 | export * from '@treo/components/message/message.types'; 5 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/navigation/horizontal/components/divider/divider.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/navigation/horizontal/components/spacer/spacer.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/navigation/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/navigation/navigation.types.ts: -------------------------------------------------------------------------------- 1 | export interface TreoNavigationItem 2 | { 3 | id?: string; 4 | title?: string; 5 | subtitle?: string; 6 | type: 'aside' | 'basic' | 'collapsable' | 'divider' | 'group' | 'spacer'; 7 | hidden?: (item: TreoNavigationItem) => boolean; 8 | disabled?: boolean; 9 | link?: string; 10 | externalLink?: boolean; 11 | exactMatch?: boolean; 12 | function?: (item: TreoNavigationItem) => void; 13 | classes?: string; 14 | icon?: string; 15 | iconClasses?: string; 16 | badge?: { 17 | title?: string; 18 | style?: 'rectangle' | 'rounded' | 'simple', 19 | background?: string; 20 | color?: string; 21 | }; 22 | children?: TreoNavigationItem[]; 23 | meta?: any; 24 | } 25 | 26 | export type TreoVerticalNavigationAppearance = string; 27 | export type TreoVerticalNavigationMode = 'over' | 'side'; 28 | export type TreoVerticalNavigationPosition = 'left' | 'right'; 29 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/navigation/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/components/navigation/horizontal/horizontal.component'; 2 | export * from '@treo/components/navigation/vertical/vertical.component'; 3 | export * from '@treo/components/navigation/navigation.module'; 4 | export * from '@treo/components/navigation/navigation.service'; 5 | export * from '@treo/components/navigation/navigation.types'; 6 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/navigation/vertical/components/divider/divider.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/components/navigation/vertical/components/spacer/spacer.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/directives/autogrow/autogrow.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TreoAutogrowDirective } from '@treo/directives/autogrow/autogrow.directive'; 3 | 4 | @NgModule({ 5 | declarations: [ 6 | TreoAutogrowDirective 7 | ], 8 | exports : [ 9 | TreoAutogrowDirective 10 | ] 11 | }) 12 | export class TreoAutogrowModule 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/directives/autogrow/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/directives/autogrow/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/directives/autogrow/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/directives/autogrow/autogrow.directive'; 2 | export * from '@treo/directives/autogrow/autogrow.module'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/directives/scrollbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/directives/scrollbar/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/directives/scrollbar/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/directives/scrollbar/scrollbar.directive'; 2 | export * from '@treo/directives/scrollbar/scrollbar.module'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/directives/scrollbar/scrollbar.interfaces.ts: -------------------------------------------------------------------------------- 1 | export class ScrollbarGeometry 2 | { 3 | public x: number; 4 | public y: number; 5 | 6 | public w: number; 7 | public h: number; 8 | 9 | constructor(x: number, y: number, w: number, h: number) 10 | { 11 | this.x = x; 12 | this.y = y; 13 | this.w = w; 14 | this.h = h; 15 | } 16 | } 17 | 18 | export class ScrollbarPosition 19 | { 20 | public x: number | 'start' | 'end'; 21 | public y: number | 'start' | 'end'; 22 | 23 | constructor(x: number | 'start' | 'end', y: number | 'start' | 'end') 24 | { 25 | this.x = x; 26 | this.y = y; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/directives/scrollbar/scrollbar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TreoScrollbarDirective } from '@treo/directives/scrollbar/scrollbar.directive'; 3 | 4 | @NgModule({ 5 | declarations: [ 6 | TreoScrollbarDirective 7 | ], 8 | exports : [ 9 | TreoScrollbarDirective 10 | ] 11 | }) 12 | export class TreoScrollbarModule 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './treo.module'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/lib/mock-api/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/lib/mock-api/mock-api.module'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/lib/mock-api/mock-api.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface TreoMockApi 2 | { 3 | register(): void; 4 | } 5 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/lib/mock-api/mock-api.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 3 | import { TreoMockApiInterceptor } from '@treo/lib/mock-api/mock-api.interceptor'; 4 | import { TreoMockApiService } from '@treo/lib/mock-api/mock-api.service'; 5 | 6 | @NgModule({ 7 | providers: [ 8 | TreoMockApiService, 9 | { 10 | provide : HTTP_INTERCEPTORS, 11 | useClass: TreoMockApiInterceptor, 12 | multi : true 13 | } 14 | ] 15 | }) 16 | export class TreoMockApiModule 17 | { 18 | /** 19 | * forRoot method for setting user configuration 20 | * 21 | * @param mockDataServices 22 | */ 23 | static forRoot(mockDataServices: any[]): ModuleWithProviders 24 | { 25 | return { 26 | ngModule : TreoMockApiModule, 27 | providers: [ 28 | { 29 | provide : APP_INITIALIZER, 30 | deps : mockDataServices, 31 | useFactory: () => () => null, 32 | multi : true 33 | }, 34 | ] 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/lib/mock-api/mock-api.utils.ts: -------------------------------------------------------------------------------- 1 | export class TreoMockApiUtils 2 | { 3 | /** 4 | * Constructor 5 | */ 6 | constructor() 7 | { 8 | 9 | } 10 | 11 | // ----------------------------------------------------------------------------------------------------- 12 | // @ Public methods 13 | // ----------------------------------------------------------------------------------------------------- 14 | 15 | /** 16 | * Generate a globally unique id 17 | */ 18 | static guid(): string 19 | { 20 | /* tslint:disable */ 21 | 22 | let d = new Date().getTime(); 23 | 24 | // Use high-precision timer if available 25 | if ( typeof performance !== 'undefined' && typeof performance.now === 'function' ) 26 | { 27 | d += performance.now(); 28 | } 29 | 30 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 31 | const r = (d + Math.random() * 16) % 16 | 0; 32 | d = Math.floor(d / 16); 33 | return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); 34 | }); 35 | 36 | /* tslint:enable */ 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/pipes/find-by-key/find-by-key.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TreoFindByKeyPipe } from '@treo/pipes/find-by-key/find-by-key.pipe'; 3 | 4 | @NgModule({ 5 | declarations: [ 6 | TreoFindByKeyPipe 7 | ], 8 | exports : [ 9 | TreoFindByKeyPipe 10 | ] 11 | }) 12 | export class TreoFindByKeyPipeModule 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/pipes/find-by-key/find-by-key.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | /** 4 | * Finds an object from given source using the given key - value pairs 5 | */ 6 | @Pipe({ 7 | name: 'treoFindByKey', 8 | pure: false 9 | }) 10 | export class TreoFindByKeyPipe implements PipeTransform 11 | { 12 | /** 13 | * Constructor 14 | */ 15 | constructor() 16 | { 17 | } 18 | 19 | /** 20 | * Transform 21 | * 22 | * @param value A string or an array of strings to find from source 23 | * @param key Key of the object property to look for 24 | * @param source Array of objects to find from 25 | */ 26 | transform(value: string | string[], key: string, source: any[]): any 27 | { 28 | // If the given value is an array of strings... 29 | if ( Array.isArray(value) ) 30 | { 31 | return value.map((item) => { 32 | return source.find((sourceItem) => sourceItem[key] === item); 33 | }); 34 | } 35 | 36 | // If the value is a string... 37 | return source.find(sourceItem => sourceItem[key] === value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/pipes/find-by-key/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/pipes/find-by-key/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/pipes/find-by-key/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/pipes/find-by-key/find-by-key.pipe'; 2 | export * from '@treo/pipes/find-by-key/find-by-key.module'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/config/config.constants.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const TREO_APP_CONFIG = new InjectionToken('Default configuration for the app'); 4 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { TreoConfigService } from '@treo/services/config/config.service'; 3 | import { TREO_APP_CONFIG } from '@treo/services/config/config.constants'; 4 | 5 | @NgModule() 6 | export class TreoConfigModule 7 | { 8 | /** 9 | * Constructor 10 | * 11 | * @param {TreoConfigService} _treoConfigService 12 | */ 13 | constructor( 14 | private _treoConfigService: TreoConfigService 15 | ) 16 | { 17 | } 18 | 19 | /** 20 | * forRoot method for setting user configuration 21 | * 22 | * @param config 23 | */ 24 | static forRoot(config: any): ModuleWithProviders 25 | { 26 | return { 27 | ngModule : TreoConfigModule, 28 | providers: [ 29 | { 30 | provide : TREO_APP_CONFIG, 31 | useValue: config 32 | } 33 | ] 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/services/config/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/config/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/services/config/config.module'; 2 | export * from '@treo/services/config/config.service'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/media-watcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/services/media-watcher/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/media-watcher/media-watcher.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TreoMediaWatcherService } from '@treo/services/media-watcher/media-watcher.service'; 3 | 4 | @NgModule({ 5 | providers: [ 6 | TreoMediaWatcherService 7 | ] 8 | }) 9 | export class TreoMediaWatcherModule 10 | { 11 | /** 12 | * Constructor 13 | * 14 | * @param {TreoMediaWatcherService} _treoMediaWatcherService 15 | */ 16 | constructor( 17 | private _treoMediaWatcherService: TreoMediaWatcherService 18 | ) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/media-watcher/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/services/media-watcher/media-watcher.module'; 2 | export * from '@treo/services/media-watcher/media-watcher.service'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/splash-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/services/splash-screen/public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/splash-screen/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from '@treo/services/splash-screen/splash-screen.module'; 2 | export * from '@treo/services/splash-screen/splash-screen.service'; 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/services/splash-screen/splash-screen.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TreoSplashScreenService } from '@treo/services/splash-screen/splash-screen.service'; 3 | 4 | @NgModule({ 5 | providers: [ 6 | TreoSplashScreenService 7 | ] 8 | }) 9 | export class TreoSplashScreenModule 10 | { 11 | /** 12 | * Constructor 13 | * 14 | * @param {TreoSplashScreenService} _treoSplashScreenService 15 | */ 16 | constructor( 17 | private _treoSplashScreenService: TreoSplashScreenService 18 | ) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/base/_theming.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ Apply Angular Material theme and generate Treo color classes for the theme 3 | // ----------------------------------------------------------------------------------------------------- 4 | @include treo-theme { 5 | 6 | // Generate Angular Material theme 7 | @include angular-material-theme($theme); 8 | 9 | // Generate Treo color classes for the theme 10 | @include treo-color-classes( 11 | ( 12 | primary: map-get($theme, primary), 13 | accent: map-get($theme, accent), 14 | warn: map-get($theme, warn) 15 | ) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/main.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ This file meant to be imported only once! 3 | // Use treo.scss to access to the Angular Material and Treo utilities 4 | // ----------------------------------------------------------------------------------------------------- 5 | 6 | // 1. Utilities 7 | @import 'treo'; 8 | 9 | // 2. Vendors 10 | @import 'vendors/normalize'; 11 | @import 'vendors/angular-material'; 12 | 13 | // 3. Base 14 | @import 'base/preflight'; 15 | @import 'base/typography'; 16 | @import 'base/colors'; 17 | @import 'base/theming'; 18 | 19 | // 4. Layout 20 | @import 'layout/content'; 21 | 22 | // 5. Components 23 | @import 'components/card'; 24 | @import 'components/input'; 25 | @import 'components/table'; 26 | 27 | // 6. Overrides 28 | @import 'overrides/angular-material'; 29 | @import 'overrides/highlightjs'; 30 | @import 'overrides/perfect-scrollbar'; 31 | @import 'overrides/quill'; 32 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/treo.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ Use this file to access to the core Angular Material and Treo utilities 3 | // ----------------------------------------------------------------------------------------------------- 4 | 5 | // 1. Angular Material theming tools 6 | @import '~@angular/material/theming'; 7 | 8 | // 2. Utilities 9 | @import '../tailwind/exported/variables'; 10 | @import 'utilities/theming'; 11 | @import 'utilities/breakpoints'; 12 | @import 'utilities/colors'; 13 | @import 'utilities/elevations'; 14 | @import 'utilities/icons'; 15 | @import 'utilities/keyframes'; 16 | 17 | // 3. Themes definition file that includes $treo-themes map 18 | @import 'src/styles/themes'; 19 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/utilities/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ Mixins 3 | // ----------------------------------------------------------------------------------------------------- 4 | 5 | /// 6 | /// Wrap the mixin content with the given media breakpoint. 7 | /// If breakpoint name does not exist on the breakpoints list, 8 | /// apply the given name as a media rule. 9 | /// 10 | /// @access public 11 | /// @param {String} $breakpoint - Name of the breakpoint or a media rule 12 | /// 13 | @mixin treo-breakpoint($breakpoint) { 14 | 15 | $mediaQuery: map-get($treo-breakpoints, $breakpoint); 16 | 17 | @if ($mediaQuery != null) { 18 | 19 | @media #{$mediaQuery} { 20 | @content 21 | } 22 | } @else { 23 | 24 | @media #{$breakpoint} { 25 | @content 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/utilities/_elevations.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ Mixins 3 | // ----------------------------------------------------------------------------------------------------- 4 | 5 | /// 6 | /// Adds an elevation from pre-defined elevations map. Elevation values are the same 7 | /// as default TailwindCSS elevations to keep things consistent. 8 | /// 9 | /// @access public 10 | /// @param {String} $elevation - The amount of the elevation that the element will have 11 | /// @param {Boolean} $important - Whether to add an !important tag to the shadow rule 12 | /// @param {Color} $color - Color of the shadow 13 | /// 14 | @mixin treo-elevation($elevation: 'default', $important: false, $color: rgb(0, 0, 0)) { 15 | 16 | // Get the shadow value 17 | $shadow: map-get($treo-elevations, $elevation); 18 | 19 | // Throw an error if the shadow does not exist 20 | @if ($shadow == null) { 21 | @error 'Elevation `' + $elevation + '` does not exists!'; 22 | } 23 | 24 | box-shadow: #{$shadow} if($important, !important, null); 25 | } 26 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/utilities/_icons.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ Mixins 3 | // ----------------------------------------------------------------------------------------------------- 4 | 5 | /// 6 | /// Correctly sets the icon size 7 | /// 8 | /// @access public 9 | /// @param {String} $size - Size of the icon (px) 10 | /// @param {Boolean} $important - Set the '!important' tag on the rules 11 | /// 12 | @mixin treo-icon-size($size, $important: false) { 13 | width: #{($size) + 'px'} if($important, !important, null); 14 | height: #{($size) + 'px'} if($important, !important, null); 15 | min-width: #{($size) + 'px'} if($important, !important, null); 16 | min-height: #{($size) + 'px'} if($important, !important, null); 17 | font-size: #{($size) + 'px'} if($important, !important, null); 18 | line-height: #{($size) + 'px'} if($important, !important, null); 19 | 20 | svg { 21 | width: #{($size) + 'px'} if($important, !important, null); 22 | height: #{($size) + 'px'} if($important, !important, null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/utilities/_keyframes.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ Rotation - animation: rotation 8s infinite linear; 3 | // ----------------------------------------------------------------------------------------------------- 4 | @keyframes rotation { 5 | from { 6 | transform: rotate(0deg); 7 | } 8 | to { 9 | transform: rotate(359deg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/utilities/_theming.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ Mixins 3 | // ----------------------------------------------------------------------------------------------------- 4 | 5 | /// 6 | /// Go through the each defined theme and apply it to whatever content this mixin has 7 | /// 8 | /// @access public 9 | /// @param {Boolean} $encapsulated - Should the generated rules compatible with encapsulated components? 10 | /// 11 | @mixin treo-theme($encapsulated: false) { 12 | 13 | @each $class-name, $theme in $treo-themes { 14 | 15 | // Set the theme as global so it can be accessible from outside 16 | $theme: $theme !global; 17 | 18 | // If encapsulated... 19 | @if ($encapsulated) { 20 | 21 | // Do everything inside a host context 22 | :host-context(.#{$class-name}) { 23 | @content; 24 | } 25 | 26 | } @else { 27 | 28 | // Do everything openly 29 | .#{$class-name} { 30 | @content; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/styles/vendors/_angular-material.scss: -------------------------------------------------------------------------------- 1 | // Include core Angular Material styles from '~@angular/material/theming' 2 | @include mat-core(); 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/export.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is being used by injecting custom TailwindCSS variants. 3 | * 4 | * These variants are different because these will not generate any 5 | * CSS rules, but they will generate SCSS variables from your Tailwind 6 | * config file. 7 | * 8 | * The generated output will be used by Treo. 9 | * Do NOT modify or use this file to generate your own variants. 10 | */ 11 | 12 | @variants export-boxShadow, export-colors, export-fontFamily, export-screens {} 13 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/export.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const resolveConfig = require('tailwindcss/resolveConfig'); 6 | const buildMediaQuery = require('tailwindcss/lib/util/buildMediaQuery').default; 7 | 8 | if ( !process.argv[3] || !process.argv[5] ) 9 | { 10 | console.error('Usage: -c [Relative path to Tailwind config file] -o [Relative path to Output file]'); 11 | process.exit(1); 12 | } 13 | 14 | const tailwindConfig = require(path.join(process.cwd(), process.argv[3])); 15 | const output = process.argv[5]; 16 | let outputFileContents = ''; 17 | 18 | // Read screens and build media queries 19 | const screens = resolveConfig(tailwindConfig).theme.screens; 20 | let queries = {}; 21 | Object.keys(screens).forEach((key) => { 22 | queries[key] = buildMediaQuery(screens[key]) 23 | }); 24 | queries = JSON.stringify(queries); 25 | queries = queries.replace(/"/g, '\'').replace(/,/g, ', ').replace(/:/g, ': '); 26 | outputFileContents = `${outputFileContents}export const treoBreakpoints = ${queries};\n`; 27 | 28 | // Write the output file 29 | fs.writeFile(output, outputFileContents, (err) => { 30 | if ( err ) 31 | { 32 | return console.log(err); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/exported/variables.ts: -------------------------------------------------------------------------------- 1 | export const treoBreakpoints = {'xs': '(min-width: 0) and (max-width: 599px)', 'sm': '(min-width: 600px) and (max-width: 959px)', 'md': '(min-width: 960px) and (max-width: 1279px)', 'lg': '(min-width: 1280px) and (max-width: 1439px)', 'xl': '(min-width: 1440px)', 'lt-md': '(max-width: 200px)', 'lt-lg': '(max-width: 1279px)', 'lt-xl': '(max-width: 1439px)', 'gt-xs': '(min-width: 600px)', 'gt-sm': '(min-width: 960px)', 'gt-md': '(min-width: 1280px)'}; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 3 | // Exporter variants 4 | require('./variants/export-box-shadow'), 5 | require('./variants/export-colors'), 6 | require('./variants/export-font-family'), 7 | require('./variants/export-screens'), 8 | 9 | // Variants 10 | require('./variants/dark-light'), 11 | 12 | // Utilities 13 | require('./utilities/color-contrasts'), 14 | require('./utilities/color-combinations'), 15 | require('./utilities/icon-color'), 16 | require('./utilities/icon-size'), 17 | require('./utilities/mirror') 18 | ]; 19 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/plugins/utilities/icon-color.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | const flattenColorPalette = require('tailwindcss/lib/util/flattenColorPalette').default; 3 | const _ = require('lodash'); 4 | 5 | module.exports = plugin(({addUtilities, e, theme, variants}) => { 6 | 7 | const utilities = _.fromPairs( 8 | _.map(flattenColorPalette(theme('iconColor')), (value, modifier) => { 9 | return [ 10 | `.${e(`icon-${modifier}`)}`, 11 | { 12 | [`.mat-icon`]: { 13 | color: value 14 | } 15 | } 16 | ] 17 | }) 18 | ); 19 | 20 | addUtilities(utilities, variants('iconColor')) 21 | }, 22 | { 23 | theme : { 24 | iconColor: theme => theme('colors') 25 | }, 26 | variants: { 27 | iconColor: [] 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/plugins/utilities/mirror.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | 3 | /** 4 | * Adds utility classes for mirroring 5 | */ 6 | module.exports = plugin(({addUtilities, variants}) => { 7 | 8 | const utilities = { 9 | [`.mirror`] : { 10 | transform: `scale(-1, 1)` 11 | }, 12 | [`.mirror-vertical`]: { 13 | transform: `scale(1, -1)` 14 | } 15 | }; 16 | 17 | addUtilities(utilities, variants('mirror')); 18 | }, 19 | { 20 | variants: { 21 | mirror: [] 22 | } 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/plugins/variants/dark-light.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | 3 | /** 4 | * Adds 'dark-light' variants 5 | */ 6 | module.exports = plugin(({addVariant, e}) => { 7 | 8 | const variant = ({modifySelectors, separator}) => { 9 | modifySelectors(({className}) => { 10 | return `[class*="theme-dark"].${e(`dark${separator}${className}`)}, [class*="theme-dark"] .${e(`dark${separator}${className}`)}, [class*="theme-light"].${e(`light${separator}${className}`)}, [class*="theme-light"] .${e(`light${separator}${className}`)}` 11 | }) 12 | }; 13 | 14 | addVariant('dark-light', variant); 15 | }); 16 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/plugins/variants/export-box-shadow.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | const postcss = require('postcss'); 3 | const _ = require('lodash'); 4 | 5 | /** 6 | * Exports 'boxShadow' configuration as an SCSS map 7 | */ 8 | module.exports = plugin(({addVariant, theme}) => { 9 | 10 | const variant = ({container}) => { 11 | 12 | let map = ''; 13 | 14 | _.forEach(theme('boxShadow'), (value, key) => { 15 | map = `${map} '${key}': '${theme('boxShadow.' + key)}',\n`; 16 | }); 17 | 18 | container.append( 19 | postcss.decl({ 20 | prop : '$treo-elevations', 21 | value: `(\n ${map} ) !default` 22 | }) 23 | ); 24 | }; 25 | 26 | addVariant('export-boxShadow', variant); 27 | }); 28 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/plugins/variants/export-font-family.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | const postcss = require('postcss'); 3 | const _ = require('lodash'); 4 | 5 | /** 6 | * Exports 'fontFamily' configuration as an SCSS map 7 | */ 8 | module.exports = plugin(({addVariant, theme}) => { 9 | 10 | const variant = ({container}) => { 11 | 12 | _.forEach(theme('fontFamily'), (value, key) => { 13 | 14 | container.append( 15 | postcss.decl({ 16 | prop : `$treo-font-${key}`, 17 | value: `${value} !default` 18 | }) 19 | ); 20 | }); 21 | 22 | }; 23 | 24 | addVariant('export-fontFamily', variant); 25 | }); 26 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/tailwind/plugins/variants/export-screens.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | const buildMediaQuery = require('tailwindcss/lib/util/buildMediaQuery').default; 3 | const postcss = require('postcss'); 4 | const _ = require('lodash'); 5 | 6 | /** 7 | * Exports 'screens' configuration as an SCSS map 8 | */ 9 | module.exports = plugin(({addVariant, theme}) => { 10 | 11 | const variant = ({container}) => { 12 | 13 | let map = ''; 14 | 15 | _.forEach(theme('screens'), (value, key) => { 16 | map = `${map} ${key}: '${buildMediaQuery(value)}',\n`; 17 | }); 18 | 19 | container.append( 20 | postcss.decl({ 21 | prop : '$treo-breakpoints', 22 | value: `(\n ${map} ) !default` 23 | }) 24 | ); 25 | }; 26 | 27 | addVariant('export-screens', variant); 28 | }); 29 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/treo.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; 3 | import { TreoMediaWatcherModule } from '@treo/services/media-watcher/media-watcher.module'; 4 | import { TreoSplashScreenModule } from '@treo/services/splash-screen/splash-screen.module'; 5 | 6 | @NgModule({ 7 | imports : [ 8 | TreoMediaWatcherModule, 9 | TreoSplashScreenModule 10 | ], 11 | providers: [ 12 | { 13 | // Use the 'fill' appearance on form fields by default 14 | provide : MAT_FORM_FIELD_DEFAULT_OPTIONS, 15 | useValue: { 16 | appearance: 'fill' 17 | } 18 | } 19 | ] 20 | }) 21 | export class TreoModule 22 | { 23 | /** 24 | * Constructor 25 | * 26 | * @param parentModule 27 | */ 28 | constructor( 29 | @Optional() @SkipSelf() parentModule?: TreoModule 30 | ) 31 | { 32 | if ( parentModule ) 33 | { 34 | throw new Error('TreoModule has already been loaded. Import this module in the AppModule only!'); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './public-api'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/@treo/validators/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './validators'; 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/android-icon-144x144.png -------------------------------------------------------------------------------- /webapp/frontend/src/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/android-icon-192x192.png -------------------------------------------------------------------------------- /webapp/frontend/src/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/android-icon-36x36.png -------------------------------------------------------------------------------- /webapp/frontend/src/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/android-icon-48x48.png -------------------------------------------------------------------------------- /webapp/frontend/src/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/android-icon-72x72.png -------------------------------------------------------------------------------- /webapp/frontend/src/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/android-icon-96x96.png -------------------------------------------------------------------------------- /webapp/frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex: 1 1 auto; 4 | width: 100%; 5 | height: 100%; 6 | } -------------------------------------------------------------------------------- /webapp/frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector : 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls : ['./app.component.scss'] 7 | }) 8 | export class AppComponent 9 | { 10 | /** 11 | * Constructor 12 | */ 13 | constructor() 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/config/scrutiny-config.module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; 3 | import {TREO_APP_CONFIG} from '@treo/services/config/config.constants'; 4 | 5 | @NgModule() 6 | export class ScrutinyConfigModule { 7 | /** 8 | * Constructor 9 | * 10 | * @param {ScrutinyConfigService} _scrutinyConfigService 11 | */ 12 | constructor( 13 | private _scrutinyConfigService: ScrutinyConfigService 14 | ) { 15 | } 16 | 17 | /** 18 | * forRoot method for setting user configuration 19 | * 20 | * @param config 21 | */ 22 | static forRoot(config: any): ModuleWithProviders { 23 | return { 24 | ngModule: ScrutinyConfigModule, 25 | providers: [ 26 | { 27 | provide: TREO_APP_CONFIG, 28 | useValue: config 29 | } 30 | ] 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/device-details-response-wrapper.ts: -------------------------------------------------------------------------------- 1 | import {DeviceModel} from 'app/core/models/device-model'; 2 | import {SmartModel} from 'app/core/models/measurements/smart-model'; 3 | import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model'; 4 | 5 | // maps to webapp/backend/pkg/models/device_summary.go 6 | export interface DeviceDetailsResponseWrapper { 7 | success: boolean; 8 | errors?: any[]; 9 | data: { 10 | device: DeviceModel; 11 | smart_results: SmartModel[]; 12 | }, 13 | metadata: { [key: string]: AttributeMetadataModel } | { [key: number]: AttributeMetadataModel }; 14 | } 15 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/device-model.ts: -------------------------------------------------------------------------------- 1 | // maps to webapp/backend/pkg/models/device.go 2 | export interface DeviceModel { 3 | archived?: boolean; 4 | wwn: string; 5 | device_name?: string; 6 | device_uuid?: string; 7 | device_serial_id?: string; 8 | device_label?: string; 9 | 10 | manufacturer: string; 11 | model_name: string; 12 | interface_type: string; 13 | interface_speed: string; 14 | serial_number: string; 15 | firmware: string; 16 | rotational_speed: number; 17 | capacity: number; 18 | form_factor: string; 19 | smart_support: boolean; 20 | device_protocol: string; 21 | device_type: string; 22 | 23 | label: string; 24 | host_id: string; 25 | 26 | device_status: number; 27 | } 28 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/device-summary-model.ts: -------------------------------------------------------------------------------- 1 | import {DeviceModel} from 'app/core/models/device-model'; 2 | import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model'; 3 | 4 | // maps to webapp/backend/pkg/models/device_summary.go 5 | export interface DeviceSummaryModel { 6 | device: DeviceModel; 7 | smart?: SmartSummary; 8 | temp_history?: SmartTemperatureModel[]; 9 | } 10 | 11 | export interface SmartSummary { 12 | collector_date?: string, 13 | temp?: number 14 | power_on_hours?: number 15 | } 16 | 17 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts: -------------------------------------------------------------------------------- 1 | import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; 2 | 3 | // maps to webapp/backend/pkg/models/device_summary.go 4 | export interface DeviceSummaryResponseWrapper { 5 | success: boolean; 6 | errors: any[]; 7 | data: { 8 | summary: { [key: string]: DeviceSummaryModel } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts: -------------------------------------------------------------------------------- 1 | import {SmartTemperatureModel} from './measurements/smart-temperature-model'; 2 | 3 | export interface DeviceSummaryTempResponseWrapper { 4 | success: boolean; 5 | errors: any[]; 6 | data: { 7 | temp_history: { [key: string]: SmartTemperatureModel[]; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts: -------------------------------------------------------------------------------- 1 | // maps to webapp/backend/pkg/models/measurements/smart_ata_attribute.go 2 | // maps to webapp/backend/pkg/models/measurements/smart_nvme_attribute.go 3 | // maps to webapp/backend/pkg/models/measurements/smart_scsi_attribute.go 4 | export interface SmartAttributeModel { 5 | attribute_id: number | string 6 | value: number 7 | thresh: number 8 | worst?: number 9 | raw_value?: number 10 | raw_string?: string 11 | when_failed?: string 12 | 13 | transformed_value: number 14 | status: number 15 | status_reason?: string 16 | failure_rate?: number 17 | 18 | chartData?: any[] 19 | } 20 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/measurements/smart-model.ts: -------------------------------------------------------------------------------- 1 | // maps to webapp/backend/pkg/models/measurements/smart.go 2 | import {SmartAttributeModel} from './smart-attribute-model'; 3 | 4 | export interface SmartModel { 5 | date: string; 6 | device_wwn: string; 7 | device_protocol: string; 8 | 9 | temp: number; 10 | power_on_hours: number; 11 | power_cycle_count: number 12 | attrs: { [key: string]: SmartAttributeModel } 13 | } 14 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts: -------------------------------------------------------------------------------- 1 | // maps to webapp/backend/pkg/models/measurements/smart_temperature.go 2 | export interface SmartTemperatureModel { 3 | date: string; 4 | temp: number; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts: -------------------------------------------------------------------------------- 1 | // map to webapp/backend/pkg/thresholds/ata_attribute_metadata.go 2 | // map to webapp/backend/pkg/thresholds/nvme_attribute_metadata.go 3 | // map to webapp/backend/pkg/thresholds/scsi_attribute_metadata.go 4 | export interface AttributeMetadataModel { 5 | display_name: string 6 | ideal: string 7 | critical: boolean 8 | description: string 9 | 10 | transform_value_unit?: string 11 | observed_thresholds?: any[] 12 | display_type: string 13 | } 14 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/data/mock/device/details/sdf.ts: -------------------------------------------------------------------------------- 1 | export const sdf = { 2 | 'data': { 3 | 'device': { 4 | 'CreatedAt': '2021-06-24T21:17:31.305246-07:00', 5 | 'UpdatedAt': '2021-06-24T21:17:31.305246-07:00', 6 | 'DeletedAt': null, 7 | 'wwn': '0x50014ee20b2a72a9', 8 | 'device_name': 'sdf', 9 | 'manufacturer': 'ATA', 10 | 'model_name': 'WDC_WD60EFRX-68MYMN1', 11 | 'interface_type': 'SCSI', 12 | 'interface_speed': '', 13 | 'serial_number': 'WD-WXL1HXXXXX', 14 | 'firmware': '', 15 | 'rotational_speed': 0, 16 | 'capacity': 6001175126016, 17 | 'form_factor': '', 18 | 'smart_support': false, 19 | 'device_protocol': '', 20 | 'device_type': '', 21 | 'label': '', 22 | 'host_id': '', 23 | 'device_status': 0 24 | }, 25 | 'smart_results': [] 26 | }, 27 | 'metadata': null, 28 | 'success': true 29 | } 30 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/data/mock/index.ts: -------------------------------------------------------------------------------- 1 | import { SummaryMockApi } from 'app/data/mock/summary'; 2 | import { DetailsMockApi } from 'app/data/mock/device/details'; 3 | 4 | export const mockDataServices = [ 5 | SummaryMockApi, 6 | DetailsMockApi, 7 | ]; 8 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.html: -------------------------------------------------------------------------------- 1 |

Archive {{data.title}}?

2 | This will remove the device from Scrutiny dashboard, unless you toggle show archived. Any data about the device 3 | itself will remain untouched. 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.scss -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; 3 | import {DashboardDeviceArchiveDialogService} from 'app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service'; 4 | 5 | @Component({ 6 | selector: 'app-dashboard-device-archive-dialog', 7 | templateUrl: './dashboard-device-archive-dialog.component.html', 8 | styleUrls: ['./dashboard-device-archive-dialog.component.scss'], 9 | }) 10 | export class DashboardDeviceArchiveDialogComponent implements OnInit { 11 | 12 | constructor( 13 | public dialogRef: MatDialogRef, 14 | @Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string}, 15 | private _archiveService: DashboardDeviceArchiveDialogService, 16 | ) { 17 | } 18 | 19 | ngOnInit(): void { 20 | } 21 | 22 | onArchiveClick(): void { 23 | this._archiveService.archiveDevice(this.data.wwn) 24 | .subscribe((data) => { 25 | this.dialogRef.close(data); 26 | }); 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | import {MatButtonModule} from '@angular/material/button'; 4 | import {MatIconModule} from '@angular/material/icon'; 5 | import {SharedModule} from 'app/shared/shared.module'; 6 | import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing'; 7 | import {MatDialogModule} from '@angular/material/dialog'; 8 | import {DashboardDeviceArchiveDialogComponent} from './dashboard-device-archive-dialog.component'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | DashboardDeviceArchiveDialogComponent 13 | ], 14 | imports: [ 15 | RouterModule.forChild([]), 16 | RouterModule.forChild(dashboardRoutes), 17 | MatButtonModule, 18 | MatIconModule, 19 | SharedModule, 20 | MatDialogModule 21 | ], 22 | exports : [ 23 | DashboardDeviceArchiveDialogComponent, 24 | ], 25 | providers : [] 26 | }) 27 | export class DashboardDeviceArchiveDialogModule 28 | { 29 | } 30 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {Observable} from 'rxjs'; 4 | import {getBasePath} from 'app/app.routing'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class DashboardDeviceArchiveDialogService 10 | { 11 | 12 | 13 | /** 14 | * Constructor 15 | * 16 | * @param {HttpClient} _httpClient 17 | */ 18 | constructor( 19 | private _httpClient: HttpClient 20 | ) 21 | { 22 | } 23 | 24 | // ----------------------------------------------------------------------------------------------------- 25 | // @ Public methods 26 | // ----------------------------------------------------------------------------------------------------- 27 | 28 | 29 | archiveDevice(wwn: string): Observable 30 | { 31 | return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {}); 32 | } 33 | 34 | unarchiveDevice(wwn: string): Observable 35 | { 36 | return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {}); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.html: -------------------------------------------------------------------------------- 1 |

Delete {{data.title}}?

2 | This will remove the device and all historical data from Scrutiny. Any data on the device 3 | itself will remain untouched. 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.scss -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; 3 | import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service'; 4 | 5 | @Component({ 6 | selector: 'app-dashboard-device-delete-dialog', 7 | templateUrl: './dashboard-device-delete-dialog.component.html', 8 | styleUrls: ['./dashboard-device-delete-dialog.component.scss'] 9 | }) 10 | export class DashboardDeviceDeleteDialogComponent implements OnInit { 11 | 12 | constructor( 13 | public dialogRef: MatDialogRef, 14 | @Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string}, 15 | private _deleteService: DashboardDeviceDeleteDialogService, 16 | ) { 17 | } 18 | 19 | ngOnInit(): void { 20 | } 21 | 22 | onDeleteClick(): void { 23 | this._deleteService.deleteDevice(this.data.wwn) 24 | .subscribe((data) => { 25 | this.dialogRef.close(data); 26 | }); 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | import {MatButtonModule} from '@angular/material/button'; 4 | import {MatIconModule} from '@angular/material/icon'; 5 | import {SharedModule} from 'app/shared/shared.module'; 6 | import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component' 7 | import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing'; 8 | import {MatDialogModule} from '@angular/material/dialog'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | DashboardDeviceDeleteDialogComponent 13 | ], 14 | imports: [ 15 | RouterModule.forChild([]), 16 | RouterModule.forChild(dashboardRoutes), 17 | MatButtonModule, 18 | MatIconModule, 19 | SharedModule, 20 | MatDialogModule 21 | ], 22 | exports : [ 23 | DashboardDeviceDeleteDialogComponent, 24 | ], 25 | providers : [] 26 | }) 27 | export class DashboardDeviceDeleteDialogModule 28 | { 29 | } 30 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { BehaviorSubject, Observable } from 'rxjs'; 4 | import { tap } from 'rxjs/operators'; 5 | import { getBasePath } from 'app/app.routing'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class DashboardDeviceDeleteDialogService 11 | { 12 | 13 | 14 | /** 15 | * Constructor 16 | * 17 | * @param {HttpClient} _httpClient 18 | */ 19 | constructor( 20 | private _httpClient: HttpClient 21 | ) 22 | { 23 | } 24 | 25 | // ----------------------------------------------------------------------------------------------------- 26 | // @ Public methods 27 | // ----------------------------------------------------------------------------------------------------- 28 | 29 | 30 | deleteDevice(wwn: string): Observable 31 | { 32 | return this._httpClient.delete( `${getBasePath()}/api/device/${wwn}`, {}); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.scss: -------------------------------------------------------------------------------- 1 | .text-disabled{ 2 | opacity: 0.8; 3 | } 4 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | import {MatButtonModule} from '@angular/material/button'; 4 | import {MatIconModule} from '@angular/material/icon'; 5 | import {SharedModule} from 'app/shared/shared.module'; 6 | import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component' 7 | import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing'; 8 | import {MatMenuModule} from '@angular/material/menu'; 9 | import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module'; 10 | import {DashboardDeviceArchiveDialogModule} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.module'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | DashboardDeviceComponent 15 | ], 16 | imports: [ 17 | RouterModule.forChild([]), 18 | RouterModule.forChild(dashboardRoutes), 19 | MatButtonModule, 20 | MatIconModule, 21 | MatMenuModule, 22 | SharedModule, 23 | DashboardDeviceDeleteDialogModule, 24 | DashboardDeviceArchiveDialogModule 25 | ], 26 | exports: [ 27 | DashboardDeviceComponent, 28 | ], 29 | providers: [] 30 | }) 31 | export class DashboardDeviceModule { 32 | } 33 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.scss -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.html: -------------------------------------------------------------------------------- 1 |

Scrutiny Settings

2 | 3 | 4 |
5 |
6 | 7 | Threshold Data 8 | 9 | Scrutiny 10 | Manufacturer 11 | 12 | 13 |
14 | 15 |
16 | 17 | Notifications 18 | 19 | Enabled 20 | Disabled 21 | 22 | 23 |
24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.scss -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DetailSettingsComponent } from './detail-settings.component'; 4 | 5 | describe('DetailSettingsComponent', () => { 6 | let component: DetailSettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DetailSettingsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DetailSettingsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-detail-settings', 5 | templateUrl: './detail-settings.component.html', 6 | styleUrls: ['./detail-settings.component.scss'] 7 | }) 8 | export class DetailSettingsComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/common/search/search.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { Overlay } from '@angular/cdk/overlay'; 4 | import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatFormFieldModule } from '@angular/material/form-field'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { SharedModule } from 'app/shared/shared.module'; 10 | import { SearchComponent } from 'app/layout/common/search/search.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | SearchComponent 15 | ], 16 | imports : [ 17 | RouterModule.forChild([]), 18 | MatAutocompleteModule, 19 | MatButtonModule, 20 | MatFormFieldModule, 21 | MatIconModule, 22 | MatInputModule, 23 | SharedModule 24 | ], 25 | exports : [ 26 | SearchComponent 27 | ], 28 | providers : [ 29 | { 30 | provide : MAT_AUTOCOMPLETE_SCROLL_STRATEGY, 31 | useFactory: (overlay: Overlay) => { 32 | return () => overlay.scrollStrategies.block(); 33 | }, 34 | deps : [Overlay] 35 | } 36 | ] 37 | }) 38 | export class SearchModule 39 | { 40 | } 41 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layout.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layout.component.scss: -------------------------------------------------------------------------------- 1 | layout { 2 | display: flex; 3 | flex: 1 1 auto; 4 | width: 100%; 5 | max-width: 100%; 6 | min-width: 0; 7 | } 8 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TreoDrawerModule } from '@treo/components/drawer'; 3 | import { LayoutComponent } from 'app/layout/layout.component'; 4 | import { EmptyLayoutModule } from 'app/layout/layouts/empty/empty.module'; 5 | import { MaterialLayoutModule } from 'app/layout/layouts/horizontal/material/material.module'; 6 | 7 | import { SharedModule } from 'app/shared/shared.module'; 8 | 9 | const modules = [ 10 | // Empty 11 | EmptyLayoutModule, 12 | 13 | // Horizontal navigation 14 | MaterialLayoutModule, 15 | ]; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | LayoutComponent, 20 | ], 21 | imports : [ 22 | TreoDrawerModule, 23 | SharedModule, 24 | ...modules 25 | ], 26 | exports : [ 27 | ...modules 28 | ] 29 | }) 30 | export class LayoutModule 31 | { 32 | } 33 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layout.types.ts: -------------------------------------------------------------------------------- 1 | export type Layout = 'empty' | 2 | 'centered' | 'enterprise' | 'material' | 'modern' | 3 | 'basic' | 'classic' | 'classy' | 'compact' | 'dense' | 'futuristic' | 'thin'; 4 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layouts/empty/empty.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | 7 | 9 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layouts/empty/empty.component.scss: -------------------------------------------------------------------------------- 1 | @import 'treo'; 2 | 3 | empty-layout { 4 | position: relative; 5 | display: flex; 6 | flex: 1 1 auto; 7 | width: 100%; 8 | 9 | // Container 10 | > .container { 11 | display: flex; 12 | flex-direction: column; 13 | flex: 1 1 auto; 14 | width: 100%; 15 | 16 | // Content 17 | > .content { 18 | display: flex; 19 | flex-direction: column; 20 | flex: 1 0 auto; 21 | 22 | > *:not(router-outlet) { 23 | position: relative; 24 | display: flex; 25 | flex: 1 0 auto; 26 | flex-wrap: wrap; 27 | width: 100%; 28 | min-width: 100%; 29 | } 30 | } 31 | } 32 | } 33 | 34 | // ----------------------------------------------------------------------------------------------------- 35 | // @ Theming 36 | // ----------------------------------------------------------------------------------------------------- 37 | @include treo-theme { 38 | 39 | } 40 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layouts/empty/empty.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | @Component({ 5 | selector : 'empty-layout', 6 | templateUrl : './empty.component.html', 7 | styleUrls : ['./empty.component.scss'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class EmptyLayoutComponent implements OnInit, OnDestroy 11 | { 12 | // Private 13 | private _unsubscribeAll: Subject; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | constructor() 19 | { 20 | // Set the private defaults 21 | this._unsubscribeAll = new Subject(); 22 | } 23 | 24 | // ----------------------------------------------------------------------------------------------------- 25 | // @ Lifecycle hooks 26 | // ----------------------------------------------------------------------------------------------------- 27 | 28 | /** 29 | * On init 30 | */ 31 | ngOnInit(): void 32 | { 33 | 34 | } 35 | 36 | /** 37 | * On destroy 38 | */ 39 | ngOnDestroy(): void 40 | { 41 | // Unsubscribe from all subscriptions 42 | this._unsubscribeAll.next(); 43 | this._unsubscribeAll.complete(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layouts/empty/empty.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { SharedModule } from 'app/shared/shared.module'; 4 | import { EmptyLayoutComponent } from 'app/layout/layouts/empty/empty.component'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | EmptyLayoutComponent 9 | ], 10 | imports : [ 11 | RouterModule, 12 | SharedModule 13 | ], 14 | exports : [ 15 | EmptyLayoutComponent 16 | ] 17 | }) 18 | export class EmptyLayoutModule 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/layout/layouts/horizontal/material/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { RouterModule } from '@angular/router'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatDividerModule } from '@angular/material/divider'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatMenuModule } from '@angular/material/menu'; 8 | import { TreoNavigationModule } from '@treo/components/navigation'; 9 | import { SearchModule } from 'app/layout/common/search/search.module'; 10 | import { SharedModule } from 'app/shared/shared.module'; 11 | import { MaterialLayoutComponent } from 'app/layout/layouts/horizontal/material/material.component'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | MaterialLayoutComponent 16 | ], 17 | imports : [ 18 | HttpClientModule, 19 | RouterModule, 20 | MatButtonModule, 21 | MatDividerModule, 22 | MatIconModule, 23 | MatMenuModule, 24 | TreoNavigationModule, 25 | SearchModule, 26 | SharedModule 27 | ], 28 | exports : [ 29 | MaterialLayoutComponent 30 | ] 31 | }) 32 | export class MaterialLayoutModule 33 | { 34 | } 35 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | @import 'treo'; 2 | 3 | example { 4 | 5 | } 6 | 7 | // ----------------------------------------------------------------------------------------------------- 8 | // @ Theming 9 | // ----------------------------------------------------------------------------------------------------- 10 | @include treo-theme { 11 | 12 | $background: map-get($theme, background); 13 | $foreground: map-get($theme, foreground); 14 | $primary: map-get($theme, primary); 15 | $accent: map-get($theme, accent); 16 | $warn: map-get($theme, warn); 17 | $is-dark: map-get($theme, is-dark); 18 | 19 | example { 20 | 21 | } 22 | } 23 | 24 | .dashboard-placeholder { 25 | align-items: center; 26 | justify-content: center; 27 | padding: 32px; 28 | 29 | img { 30 | max-width:500px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; 3 | import {Observable} from 'rxjs'; 4 | import {DashboardService} from 'app/modules/dashboard/dashboard.service'; 5 | import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class DashboardResolver implements Resolve { 11 | /** 12 | * Constructor 13 | * 14 | * @param {FinanceService} _dashboardService 15 | */ 16 | constructor( 17 | private _dashboardService: DashboardService 18 | ) 19 | { 20 | } 21 | 22 | // ----------------------------------------------------------------------------------------------------- 23 | // @ Public methods 24 | // ----------------------------------------------------------------------------------------------------- 25 | 26 | /** 27 | * Resolver 28 | * 29 | * @param route 30 | * @param state 31 | */ 32 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [p: string]: DeviceSummaryModel }> { 33 | return this._dashboardService.getSummaryData(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/dashboard/dashboard.routing.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | import { DashboardComponent } from 'app/modules/dashboard/dashboard.component'; 3 | import {DashboardResolver} from 'app/modules/dashboard/dashboard.resolvers'; 4 | 5 | export const dashboardRoutes: Route[] = [ 6 | { 7 | path : '', 8 | component: DashboardComponent, 9 | resolve : { 10 | sales: DashboardResolver 11 | } 12 | } 13 | ]; 14 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/detail/detail.component.scss: -------------------------------------------------------------------------------- 1 | @import 'treo'; 2 | 3 | detail { 4 | } 5 | 6 | // ----------------------------------------------------------------------------------------------------- 7 | // @ Theming 8 | // ----------------------------------------------------------------------------------------------------- 9 | @include treo-theme { 10 | 11 | $background: map-get($theme, background); 12 | $foreground: map-get($theme, foreground); 13 | $primary: map-get($theme, primary); 14 | $accent: map-get($theme, accent); 15 | $warn: map-get($theme, warn); 16 | $is-dark: map-get($theme, is-dark); 17 | 18 | detail { 19 | } 20 | 21 | 22 | } 23 | 24 | //table { 25 | // width: 100%; 26 | //} 27 | 28 | $primary: map-get($theme, primary); 29 | $is-dark: map-get($theme, is-dark); 30 | tr.attribute-detail-row { 31 | height: 0; 32 | } 33 | 34 | //tr.attribute-row:not(.attribute-expanded-row):hover { 35 | // @if ($is-dark) { 36 | // background: rgba(0, 0, 0, 0.05); 37 | // } @else { 38 | // background: map-get($primary, 50); 39 | // } 40 | //} 41 | 42 | tr.attribute-row:not(.attribute-expanded-row):active { 43 | background: #efefef; 44 | } 45 | 46 | .attribute-row td { 47 | border-bottom-width: 0; 48 | } 49 | 50 | .attribute-detail { 51 | overflow: hidden; 52 | display: flex; 53 | } 54 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/detail/detail.resolvers.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; 3 | import {Observable} from 'rxjs'; 4 | import {DetailService} from 'app/modules/detail/detail.service'; 5 | import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class DetailResolver implements Resolve { 11 | /** 12 | * Constructor 13 | * 14 | * @param {FinanceService} _detailService 15 | */ 16 | constructor( 17 | private _detailService: DetailService 18 | ) 19 | { 20 | } 21 | 22 | // ----------------------------------------------------------------------------------------------------- 23 | // @ Public methods 24 | // ----------------------------------------------------------------------------------------------------- 25 | 26 | /** 27 | * Resolver 28 | * 29 | * @param route 30 | * @param state 31 | */ 32 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 33 | return this._detailService.getData(route.params.wwn); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/detail/detail.routing.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | import { DetailComponent } from 'app/modules/detail/detail.component'; 3 | import {DetailResolver} from './detail.resolvers'; 4 | 5 | export const detailRoutes: Route[] = [ 6 | { 7 | path : '', 8 | component: DetailComponent, 9 | resolve : { 10 | sales: DetailResolver 11 | } 12 | } 13 | ]; 14 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/detail/detail.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {DetailService} from './detail.service'; 3 | import {of} from 'rxjs'; 4 | import {sda} from 'app/data/mock/device/details/sda' 5 | import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; 6 | 7 | describe('DetailService', () => { 8 | describe('#getData', () => { 9 | let service: DetailService; 10 | let httpClientSpy: jasmine.SpyObj; 11 | 12 | beforeEach(() => { 13 | httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); 14 | service = new DetailService(httpClientSpy); 15 | }); 16 | it('should return getData() (HttpClient called once)', (done: DoneFn) => { 17 | httpClientSpy.get.and.returnValue(of(sda)); 18 | 19 | service.getData('test').subscribe(value => { 20 | expect(value).toBe(sda as DeviceDetailsResponseWrapper); 21 | done(); 22 | }); 23 | expect(httpClientSpy.get.calls.count()) 24 | .withContext('one call') 25 | .toBe(1); 26 | }); 27 | }) 28 | }); 29 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/landing/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 |

Landing Module

6 |

7 | This can be the landing or the welcome page of your application. If you have an SSR (Server Side Rendering) setup, or if you don't need to have Search engine 8 | visibility and optimizations, you can even use this page as your primary landing page. 9 |

10 |

11 | This is a separate "module", it has its own directory and routing setup and also it's completely separated from the actual application. This is also a simple example of 12 | multiple applications setup. You can have different entry points and essentially have different applications within the same codebase. 13 |

14 | 18 | Launch the App 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/landing/home/home.component.scss: -------------------------------------------------------------------------------- 1 | @import 'treo'; 2 | 3 | landing-home { 4 | 5 | } 6 | 7 | // ----------------------------------------------------------------------------------------------------- 8 | // @ Theming 9 | // ----------------------------------------------------------------------------------------------------- 10 | @include treo-theme { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/landing/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector : 'landing-home', 5 | templateUrl : './home.component.html', 6 | styleUrls : ['./home.component.scss'], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | export class LandingHomeComponent 10 | { 11 | /** 12 | * Constructor 13 | */ 14 | constructor() 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/landing/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { SharedModule } from 'app/shared/shared.module'; 5 | import { LandingHomeComponent } from 'app/modules/landing/home/home.component'; 6 | import { landingHomeRoutes } from 'app/modules/landing/home/home.routing'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | LandingHomeComponent 11 | ], 12 | imports : [ 13 | RouterModule.forChild(landingHomeRoutes), 14 | MatButtonModule, 15 | SharedModule 16 | ] 17 | }) 18 | export class LandingHomeModule 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/modules/landing/home/home.routing.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | import { LandingHomeComponent } from 'app/modules/landing/home/home.component'; 3 | 4 | export const landingHomeRoutes: Route[] = [ 5 | { 6 | path : '', 7 | component: LandingHomeComponent 8 | } 9 | ]; 10 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/shared/device-hours.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import humanizeDuration from 'humanize-duration'; 3 | 4 | @Pipe({ name: 'deviceHours' }) 5 | export class DeviceHoursPipe implements PipeTransform { 6 | static format(hoursOfRunTime: number, unit: string, humanizeConfig: object): string { 7 | if (hoursOfRunTime === null) { 8 | return 'Unknown'; 9 | } 10 | if (unit === 'device_hours') { 11 | return `${hoursOfRunTime} hours`; 12 | } 13 | return humanizeDuration(hoursOfRunTime * 60 * 60 * 1000, humanizeConfig); 14 | } 15 | 16 | transform(hoursOfRunTime: number, unit = 'humanize', humanizeConfig: any = {}): string { 17 | return DeviceHoursPipe.format(hoursOfRunTime, unit, humanizeConfig) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/shared/device-sort.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { DeviceSortPipe } from './device-sort.pipe'; 2 | 3 | describe('DeviceSortPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new DeviceSortPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/shared/file-size.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | @Pipe({name: 'fileSize'}) 4 | export class FileSizePipe implements PipeTransform { 5 | 6 | transform(bytes: number = 0, si = false, dp = 1): string { 7 | const thresh = si ? 1000 : 1024; 8 | 9 | if (Math.abs(bytes) < thresh) { 10 | return bytes + ' B'; 11 | } 12 | 13 | const units = si 14 | ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 15 | : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 16 | let u = -1; 17 | const r = 10 ** dp; 18 | 19 | do { 20 | bytes /= thresh; 21 | ++u; 22 | } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); 23 | 24 | 25 | return bytes.toFixed(dp) + ' ' + units[u]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import {FileSizePipe} from './file-size.pipe'; 5 | import { DeviceSortPipe } from './device-sort.pipe'; 6 | import { TemperaturePipe } from './temperature.pipe'; 7 | import { DeviceTitlePipe } from './device-title.pipe'; 8 | import { DeviceStatusPipe } from './device-status.pipe'; 9 | import { DeviceHoursPipe } from './device-hours.pipe'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | FileSizePipe, 14 | DeviceSortPipe, 15 | TemperaturePipe, 16 | DeviceTitlePipe, 17 | DeviceStatusPipe, 18 | DeviceHoursPipe 19 | ], 20 | imports: [ 21 | CommonModule, 22 | FormsModule, 23 | ReactiveFormsModule 24 | ], 25 | exports: [ 26 | CommonModule, 27 | FormsModule, 28 | ReactiveFormsModule, 29 | FileSizePipe, 30 | DeviceSortPipe, 31 | DeviceTitlePipe, 32 | DeviceStatusPipe, 33 | TemperaturePipe, 34 | DeviceHoursPipe 35 | ] 36 | }) 37 | export class SharedModule 38 | { 39 | } 40 | -------------------------------------------------------------------------------- /webapp/frontend/src/app/shared/temperature.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {formatNumber} from '@angular/common'; 3 | 4 | @Pipe({ 5 | name: 'temperature' 6 | }) 7 | export class TemperaturePipe implements PipeTransform { 8 | static celsiusToFahrenheit(celsiusTemp: number): number { 9 | return celsiusTemp * 9/5 + 32; 10 | } 11 | static formatTemperature(temp: number, unit: string, includeUnits: boolean): number|string { 12 | let unitSuffix 13 | switch (unit) { 14 | case 'celsius': 15 | unitSuffix = '°C' 16 | break 17 | case 'fahrenheit': 18 | unitSuffix = '°F' 19 | break 20 | } 21 | if(includeUnits){ 22 | return formatNumber(temp, 'en-US') + unitSuffix 23 | } else { 24 | return formatNumber(temp, 'en-US',) 25 | } 26 | } 27 | 28 | transform(celsiusTemp: number, unit = 'celsius', includeUnits = false): number|string { 29 | let temperature; 30 | switch (unit) { 31 | case 'celsius': 32 | temperature = celsiusTemp; 33 | break 34 | case 'fahrenheit': 35 | temperature = TemperaturePipe.celsiusToFahrenheit(celsiusTemp) 36 | break 37 | } 38 | return TemperaturePipe.formatTemperature(celsiusTemp, unit, includeUnits) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-114x114.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-120x120.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-144x144.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-152x152.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-180x180.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-57x57.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-60x60.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-72x72.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-76x76.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon-precomposed.png -------------------------------------------------------------------------------- /webapp/frontend/src/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/apple-icon.png -------------------------------------------------------------------------------- /webapp/frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-500.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-600.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-700.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-italic.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/ibm-plex-mono/ibm-plex-mono-v12-latin-regular.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Black.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Black.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Bold.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Bold.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-ExtraBold.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-ExtraBold.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Italic.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Italic.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Medium.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Medium.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Regular.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-Regular.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-SemiBold.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/inter/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/inter/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/material-icons/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/material-icons/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/material-icons/MaterialIconsOutlined-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/material-icons/MaterialIconsOutlined-Regular.otf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/material-icons/MaterialIconsRound-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/material-icons/MaterialIconsRound-Regular.otf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/material-icons/MaterialIconsSharp-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/material-icons/MaterialIconsSharp-Regular.otf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/material-icons/MaterialIconsTwoTone-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/material-icons/MaterialIconsTwoTone-Regular.otf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/material-icons/README.md: -------------------------------------------------------------------------------- 1 | The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts: 2 | 3 | ```html 4 | 5 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 19 | 20 | 21 | 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/material-icons/material-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Material Icons'), 6 | local('MaterialIcons-Regular'), 7 | url('MaterialIcons-Regular.ttf') format('truetype'); 8 | } 9 | 10 | .material-icons { 11 | font-family: 'Material Icons'; 12 | font-weight: normal; 13 | font-style: normal; 14 | font-size: 24px; /* Preferred icon size */ 15 | display: inline-block; 16 | line-height: 1; 17 | text-transform: none; 18 | letter-spacing: normal; 19 | word-wrap: normal; 20 | white-space: nowrap; 21 | direction: ltr; 22 | 23 | /* Support for all WebKit browsers. */ 24 | -webkit-font-smoothing: antialiased; 25 | /* Support for Safari and Chrome. */ 26 | text-rendering: optimizeLegibility; 27 | 28 | /* Support for Firefox. */ 29 | -moz-osx-font-smoothing: grayscale; 30 | 31 | /* Support for IE. */ 32 | font-feature-settings: 'liga'; 33 | } 34 | -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-300.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-500.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-700.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-900.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-italic.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.eot -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.ttf -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.woff -------------------------------------------------------------------------------- /webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/fonts/roboto/roboto-v30-latin-regular.woff2 -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/dashboard-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/dashboard-placeholder.png -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/noun_Glasses_775232.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-social.png -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-social.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-social.psd -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-text.png -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-text.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-dark-text.psd -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-dark.png -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-white-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-white-text.png -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-white-text.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-white-text.psd -------------------------------------------------------------------------------- /webapp/frontend/src/assets/images/logo/scrutiny-logo-white.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/assets/images/logo/scrutiny-logo-white.psd -------------------------------------------------------------------------------- /webapp/frontend/src/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | -------------------------------------------------------------------------------- /webapp/frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /webapp/frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /webapp/frontend/src/environments/versions.ts: -------------------------------------------------------------------------------- 1 | // this file is automatically generated by git.version.ts script 2 | export const versionInfo = { 3 | version: 'dev', 4 | }; 5 | -------------------------------------------------------------------------------- /webapp/frontend/src/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/favicon-16x16.png -------------------------------------------------------------------------------- /webapp/frontend/src/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/favicon-32x32.png -------------------------------------------------------------------------------- /webapp/frontend/src/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/favicon-96x96.png -------------------------------------------------------------------------------- /webapp/frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/favicon.ico -------------------------------------------------------------------------------- /webapp/frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { AppModule } from './app/app.module'; 4 | import { environment } from './environments/environment'; 5 | 6 | if ( environment.production ) 7 | { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /webapp/frontend/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": ".\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": ".\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": ".\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": ".\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": ".\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": ".\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /webapp/frontend/src/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/ms-icon-144x144.png -------------------------------------------------------------------------------- /webapp/frontend/src/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/ms-icon-150x150.png -------------------------------------------------------------------------------- /webapp/frontend/src/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/ms-icon-310x310.png -------------------------------------------------------------------------------- /webapp/frontend/src/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/scrutiny/4066c84c8ea8ef7ef2928652c727f79a8481083a/webapp/frontend/src/ms-icon-70x70.png -------------------------------------------------------------------------------- /webapp/frontend/src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ You can use this file to import custom styles. 3 | // 4 | // @ Styles from this file will override anything from 'vendors.scss' file allowing customizations and 5 | // modifications of third party libraries. 6 | // ----------------------------------------------------------------------------------------------------- 7 | 8 | .treo-theme-dark { 9 | .yellow-50 { 10 | background-color: #242b38 !important; 11 | 12 | .mat-icon { 13 | color: #0694a2 !important; 14 | } 15 | 16 | .text-secondary { 17 | color: #0694a2 !important 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webapp/frontend/src/styles/vendors.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------- 2 | // @ You can use this file to import styles from third party libraries. 3 | // 4 | // @ It's important to put them here because anything imported from this file can be overridden by 5 | // Treo which allows having out-of-the-box support for certain libraries. They can also be 6 | // overridden from 'styles.scss' file which allows you to override and make any third party library 7 | // that Treo doesn't support out-of-the-box visually compatible with your application. 8 | // ----------------------------------------------------------------------------------------------------- 9 | 10 | // Perfect scrollbar 11 | @import '~perfect-scrollbar/css/perfect-scrollbar.css'; 12 | 13 | // Quill 14 | @import '~quill/dist/quill.snow.css'; 15 | -------------------------------------------------------------------------------- /webapp/frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 6 | 7 | declare const require: { 8 | context(path: string, deep?: boolean, filter?: RegExp): { 9 | keys(): string[]; 10 | (id: string): T; 11 | }; 12 | }; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | 20 | // Then we find all the tests. 21 | const context = require.context('./', true, /\.spec\.ts$/); 22 | 23 | // And load the modules. 24 | context.keys().map(context); 25 | -------------------------------------------------------------------------------- /webapp/frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "exclude": [ 15 | "src/tailwind/**" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /webapp/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "./src", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ] 22 | }, 23 | "angularCompilerOptions": { 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webapp/frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------