diff --git a/daemon/main/Options.cpp b/daemon/main/Options.cpp index 78cf24db..88392f92 100644 --- a/daemon/main/Options.cpp +++ b/daemon/main/Options.cpp @@ -101,15 +101,14 @@ Options::OptEntry* Options::OptEntries::FindOption(const char* name) return nullptr; } - -const Options::Category* Options::Categories::FindCategory(const char* name, bool searchAliases) const +Options::Category* Options::Categories::FindCategory(const char* name, bool searchAliases) { if (!name) { return nullptr; } - for (const Category& category : *this) + for (Category& category : this) { if (!strcasecmp(category.GetName(), name)) { @@ -119,9 +118,9 @@ const Options::Category* Options::Categories::FindCategory(const char* name, boo if (searchAliases) { - for (const Category& category : *this) + for (Category& category : this) { - for (const CString& alias : *category.GetAliases()) + for (CString& alias : category.GetAliases()) { WildMask mask(alias); if (mask.Match(name)) @@ -135,13 +134,6 @@ const Options::Category* Options::Categories::FindCategory(const char* name, boo return nullptr; } -Options::Category* Options::Categories::FindCategory(const char* name, bool searchAliases) -{ - const Category* result = - static_cast(this)->FindCategory(name, searchAliases); - return const_cast(result); -} - Options::Options(const char* exeName, const char* configFilename, bool noConfig, CmdOptList* commandLineOptions, Extender* extender) { diff --git a/daemon/main/Options.h b/daemon/main/Options.h index f07a34db..2849afa7 100644 --- a/daemon/main/Options.h +++ b/daemon/main/Options.h @@ -496,7 +496,6 @@ class Options Categories* GetCategories() { return &m_categories; } const Categories* GetCategories() const { return &m_categories; } Category* FindCategory(const char* name, bool searchAliases) { return m_categories.FindCategory(name, searchAliases); } - const Category* FindCategory(const char* name, bool searchAliases) const { return m_categories.FindCategory(name, searchAliases); } // Current state void SetServerMode(bool serverMode) { m_serverMode = serverMode; } diff --git a/daemon/main/nzbget.cpp b/daemon/main/nzbget.cpp index 75acfca3..d87a0f7a 100644 --- a/daemon/main/nzbget.cpp +++ b/daemon/main/nzbget.cpp @@ -21,6 +21,7 @@ #include "nzbget.h" + #include "ServerPool.h" #include "Log.h" #include "NzbFile.h" @@ -57,6 +58,7 @@ #include "YEncode.h" #include "ExtensionManager.h" #include "SystemInfo.h" +#include "SystemHealth.h" #ifdef WIN32 #include "WinService.h" @@ -100,6 +102,7 @@ ScriptConfig* g_ScriptConfig; CommandScriptLog* g_CommandScriptLog; ExtensionManager::Manager* g_ExtensionManager; System::SystemInfo* g_SystemInfo; +SystemHealth::Service* g_SystemHealth; #ifdef WIN32 WinConsole* g_WinConsole; @@ -216,6 +219,7 @@ class NZBGet : public Options::Extender std::unique_ptr m_commandScriptLog; std::unique_ptr m_extensionManager; std::unique_ptr m_systemInfo; + std::unique_ptr m_systemHealth; #ifdef WIN32 std::unique_ptr m_winConsole; @@ -285,6 +289,16 @@ void NZBGet::Init() BootConfig(); + m_systemHealth = std::make_unique( + *g_Options, + *g_ServerPool->GetServers(), + *g_FeedCoordinator->GetFeeds(), + m_scheduler->GetTasks()); + g_SystemHealth = m_systemHealth.get(); + + const auto report = m_systemHealth->Diagnose(); + SystemHealth::Log(report); + #ifndef WIN32 mode_t uMask = static_cast(m_options->GetUMask()); if (uMask < 01000) diff --git a/daemon/main/nzbget.h b/daemon/main/nzbget.h index d82c0bd6..429367ba 100644 --- a/daemon/main/nzbget.h +++ b/daemon/main/nzbget.h @@ -156,6 +156,7 @@ #include #include #include +#include #include #include #include diff --git a/daemon/remote/XmlRpc.cpp b/daemon/remote/XmlRpc.cpp index 46fb3022..ad92cc27 100644 --- a/daemon/remote/XmlRpc.cpp +++ b/daemon/remote/XmlRpc.cpp @@ -43,6 +43,7 @@ #include "NetworkSpeedTest.h" #include "Xml.h" #include "Unpack.h" +#include "SystemHealth.h" extern void ExitProc(); extern void Reload(); @@ -133,6 +134,12 @@ class SysInfoXmlCommand: public SafeXmlCommand void Execute() override; }; +class SystemHealthXmlCommand : public SafeXmlCommand +{ +public: + void Execute() override; +}; + class LogXmlCommand: public SafeXmlCommand { public: @@ -437,7 +444,6 @@ class TestDiskSpeedXmlCommand final : public SafeXmlCommand Xml::AddNewNode(structNode, "DurationMS", "double", durationMS.c_str()); xmlAddChild(rootNode, structNode); - std::string result = Xml::Serialize(rootNode); xmlFreeNode(rootNode); @@ -470,7 +476,6 @@ class TestNetworkSpeedXmlCommand final : public SafeXmlCommand Xml::AddNewNode(structNode, "SpeedMbps", "double", speedMbpsStr.c_str()); xmlAddChild(rootNode, structNode); - std::string result = Xml::Serialize(rootNode); xmlFreeNode(rootNode); @@ -783,6 +788,10 @@ std::unique_ptr XmlRpcProcessor::CreateCommand(const char* methodNam { command = std::make_unique(); } + else if (!strcasecmp(methodName, "systemhealth")) + { + command = std::make_unique(); + } else if (!strcasecmp(methodName, "log")) { command = std::make_unique(); @@ -1650,19 +1659,23 @@ void StatusXmlCommand::Execute() postJobCount, postJobCount, urlCount, upTimeSec, downloadTimeSec, BoolToStr(downloadPaused), BoolToStr(downloadPaused), BoolToStr(downloadPaused), BoolToStr(serverStandBy), BoolToStr(postPaused), BoolToStr(scanPaused), BoolToStr(quotaReached), - freeDiskSpaceLo, - freeDiskSpaceHi, - freeDiskSpaceMB, - totalDiskSpaceLo, - totalDiskSpaceHi, + freeDiskSpaceLo, + freeDiskSpaceHi, + freeDiskSpaceMB, + totalDiskSpaceLo, + totalDiskSpaceHi, totalDiskSpaceMB, - freeInterDiskSpaceLo, - freeInterDiskSpaceHi, - freeInterDiskSpaceMB, - totalInterDiskSpaceLo, - totalInterDiskSpaceHi, + freeInterDiskSpaceLo, + freeInterDiskSpaceHi, + freeInterDiskSpaceMB, + totalInterDiskSpaceLo, + totalInterDiskSpaceHi, totalInterDiskSpaceMB, - serverTime, resumeTime, BoolToStr(feedActive), queuedScripts); + serverTime, + resumeTime, + BoolToStr(feedActive), + queuedScripts + ); int index = 0; for (NewsServer* server : g_ServerPool->GetServers()) @@ -1684,6 +1697,15 @@ void SysInfoXmlCommand::Execute() AppendResponse(response.c_str()); } +void SystemHealthXmlCommand::Execute() +{ + const auto report = g_SystemHealth->Diagnose(); + const std::string response = + IsJson() ? SystemHealth::ToJsonStr(report) : SystemHealth::ToXmlStr(report); + + AppendResponse(response.c_str()); +} + // struct[] log(idfrom, entries) void LogXmlCommand::Execute() { @@ -2278,7 +2300,7 @@ void ListGroupsXmlCommand::Execute() const char* ListGroupsXmlCommand::DetectStatus(NzbInfo* nzbInfo) { const char* postStageName[] = { "PP_QUEUED", "LOADING_PARS", "VERIFYING_SOURCES", "REPAIRING", - "VERIFYING_REPAIRED", "RENAMING", "RENAMING", "UNPACKING", "MOVING", "MOVING", "POST_UNPACK_RENAMING", + "VERIFYING_REPAIRED", "RENAMING", "RENAMING", "UNPACKING", "MOVING", "MOVING", "POST_UNPACK_RENAMING", "EXECUTING_SCRIPT", "PP_FINISHED" }; const char* status = nullptr; @@ -3028,7 +3050,7 @@ void LoadExtensionsXmlCommand::Execute() { BuildErrorResponse(3, error.value().c_str()); return; - } + } } AppendResponse(isJson ? "[\n" : "\n"); @@ -3185,7 +3207,7 @@ void SaveConfigXmlCommand::Execute() char* dummy; while ((IsJson() && NextParamAsStr(&dummy) && NextParamAsStr(&name) && NextParamAsStr(&dummy) && NextParamAsStr(&value)) || - (!IsJson() && NextParamAsStr(&name) && NextParamAsStr(&value))) + (!IsJson() && NextParamAsStr(&name) && NextParamAsStr(&value))) { DecodeStr(name); DecodeStr(value); @@ -3670,7 +3692,7 @@ void ServerVolumesXmlCommand::Execute() && NextParamAsBool(&BytesPer[2]) && NextParamAsBool(&BytesPer[3]); - bool articlesPerDays = true; + bool articlesPerDays = true; NextParamAsBool(&articlesPerDays); int index = 0; @@ -3700,11 +3722,11 @@ void ServerVolumesXmlCommand::Execute() serverVolume.GetDaySlot() ); - ServerVolume::VolumeArray* VolumeArrays[] = { + ServerVolume::VolumeArray* VolumeArrays[] = { serverVolume.BytesPerSeconds(), - serverVolume.BytesPerMinutes(), - serverVolume.BytesPerHours(), - serverVolume.BytesPerDays() + serverVolume.BytesPerMinutes(), + serverVolume.BytesPerHours(), + serverVolume.BytesPerDays() }; const char* VolumeNames[] = { "BytesPerSeconds", @@ -3743,12 +3765,12 @@ void ServerVolumesXmlCommand::Execute() { AppendCondResponse(",\n", IsJson()); AppendFmtResponse(IsJson() ? JSON_ARRAY_START : XML_ARRAY_START, "ArticlesPerDays"); - + const auto& articles = serverVolume.GetArticlesPerDays(); for (size_t i = 0; i < articles.size(); ++i) { AppendFmtResponse(IsJson() ? JSON_ARTICLES_ARRAY_ITEM : XML_ARTICLES_ARRAY_ITEM, - articles[i].failed, + articles[i].failed, articles[i].success ); @@ -3868,13 +3890,13 @@ void TestServerXmlCommand::Execute() if (!jsonResult) { BuildErrorResponse(2, "Invalid JSON"); - return; + return; } auto paramsResult = ParseRequestParams(*jsonResult); if (!paramsResult) { BuildErrorResponse(2, "Invalid parameters"); - return; + return; } params = std::move(*paramsResult); } @@ -4042,12 +4064,12 @@ void TestDiskSpeedXmlCommand::Execute() { size_t bufferSizeBytes = writeBufferKiB * 1024; uint64_t maxFileSizeBytes = maxFileSizeGiB * 1024ull * 1024ull * 1024ull; - + Benchmark::DiskBenchmark db; auto [size, time] = db.Run( - dirPath, - bufferSizeBytes, - maxFileSizeBytes, + dirPath, + bufferSizeBytes, + maxFileSizeBytes, std::chrono::seconds(timeoutSec) ); diff --git a/daemon/sources.cmake b/daemon/sources.cmake index 71e88e21..3999afa9 100644 --- a/daemon/sources.cmake +++ b/daemon/sources.cmake @@ -107,6 +107,30 @@ set(SRC ${CMAKE_SOURCE_DIR}/daemon/system/OS.cpp ${CMAKE_SOURCE_DIR}/daemon/system/CPU.cpp ${CMAKE_SOURCE_DIR}/daemon/system/Network.cpp + + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/SystemHealthService.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/Status.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/Validators.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/SectionValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/SectionGroupValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/PathsValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/NewsServerValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/NewsServersValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/SchedulerTasksValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/SchedulerTaskValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/LoggingValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/ExtensionScriptsValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/UnpackValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/SecurityValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/IncomingNzbValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/FeedValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/FeedsValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/DownloadQueueValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/CheckAndRepairValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/CategoryValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/CategoriesValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/ConnectionValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/DisplayValidator.cpp ) set(WIN32_SRC @@ -134,4 +158,5 @@ set(INCLUDES ${INCLUDES} ${CMAKE_SOURCE_DIR}/daemon/remote ${CMAKE_SOURCE_DIR}/daemon/system ${CMAKE_SOURCE_DIR}/daemon/util + ${CMAKE_SOURCE_DIR}/daemon/systemhealth ) diff --git a/daemon/system/SystemInfo.cpp b/daemon/system/SystemInfo.cpp index dd6265f3..6b98c1dd 100644 --- a/daemon/system/SystemInfo.cpp +++ b/daemon/system/SystemInfo.cpp @@ -276,7 +276,7 @@ namespace System return std::nullopt; } - std::optional SystemInfo::GetPythonVersion(const std::string path) const + std::optional SystemInfo::GetPythonVersion(const std::string& path) const { std::string cmd = FileSystem::EscapePathForShell(path) + " --version 2>&1"; { diff --git a/daemon/system/SystemInfo.h b/daemon/system/SystemInfo.h index 3792e75e..bf6dae17 100644 --- a/daemon/system/SystemInfo.h +++ b/daemon/system/SystemInfo.h @@ -1,7 +1,7 @@ /* * This file is part of nzbget. See . * - * Copyright (C) 2024 Denis + * Copyright (C) 2024-2025 Denis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -62,7 +62,7 @@ namespace System std::string GetToolPath(const char* cmd) const; std::string GetUnpackerVersion(const std::string& path, const char* marker) const;\ std::optional FindPython() const; - std::optional GetPythonVersion(const std::string path) const; + std::optional GetPythonVersion(const std::string& path) const; CPU m_cpu; OS m_os; diff --git a/daemon/systemhealth/CategoriesValidator.cpp b/daemon/systemhealth/CategoriesValidator.cpp new file mode 100644 index 00000000..e47cba74 --- /dev/null +++ b/daemon/systemhealth/CategoriesValidator.cpp @@ -0,0 +1,69 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include +#include "CategoriesValidator.h" +#include "CategoryValidator.h" +#include "Options.h" + +namespace SystemHealth::Category +{ +CategoriesValidator::CategoriesValidator(const Options& options, + const Options::Categories& categories) + : SectionGroupValidator(MakeCategoryValidators(options, categories)), + m_options(options), + m_categories(categories) +{ + m_validators.push_back(std::make_unique(categories)); +} + +std::vector> CategoriesValidator::MakeCategoryValidators( + const Options& options, const Options::Categories& categories) const +{ + std::vector> validators; + validators.reserve(categories.size()); + for (size_t i = 0; i < categories.size(); ++i) + { + validators.push_back(std::make_unique(options, categories[i], i + 1)); + } + return validators; +} + +Status DuplicateNamesValidator::Validate() const +{ + if (m_categories.empty()) return Status::Ok(); + + std::set names; + for (const auto& cat : m_categories) + { + std::string_view name = cat.GetName(); + if (name.empty()) continue; + + if (names.count(name)) + { + return Status::Warning("Duplicate category name detected: '" + std::string(name) + "'"); + } + names.insert(name); + } + return Status::Ok(); +} + +} // namespace SystemHealth::Category diff --git a/daemon/systemhealth/CategoriesValidator.h b/daemon/systemhealth/CategoriesValidator.h new file mode 100644 index 00000000..bd2a8b4b --- /dev/null +++ b/daemon/systemhealth/CategoriesValidator.h @@ -0,0 +1,60 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CATEGORIES_VALIDATOR_H +#define CATEGORIES_VALIDATOR_H + +#include "Options.h" +#include "SectionGroupValidator.h" +#include "SectionValidator.h" + +namespace SystemHealth::Category +{ + +class CategoriesValidator final : public SectionGroupValidator +{ +public: + explicit CategoriesValidator(const Options& options, const Options::Categories& categories); + std::string_view GetName() const override { return "Categories"; } + +private: + const Options& m_options; + const Options::Categories& m_categories; + + std::vector> MakeCategoryValidators( + const Options& options, const Options::Categories& categories) const; +}; + +class DuplicateNamesValidator : public Validator +{ +public: + explicit DuplicateNamesValidator(const Options::Categories& categories) + : m_categories(categories) + { + } + std::string_view GetName() const override { return ""; } + Status Validate() const override; + +private: + const Options::Categories& m_categories; +}; + +} // namespace SystemHealth::Category + +#endif diff --git a/daemon/systemhealth/CategoryValidator.cpp b/daemon/systemhealth/CategoryValidator.cpp new file mode 100644 index 00000000..d9edec15 --- /dev/null +++ b/daemon/systemhealth/CategoryValidator.cpp @@ -0,0 +1,65 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "CategoryValidator.h" +#include "Options.h" + +namespace SystemHealth::Category +{ + +CategoryValidator::CategoryValidator(const Options& options, const Options::Category& category, + size_t id) + : m_options(options), m_category(category), m_name("Category" + std::to_string(id)) +{ + m_validators.reserve(2); + m_validators.push_back(std::make_unique(category)); + m_validators.push_back(std::make_unique(options, category)); +} + +Status NameValidator::Validate() const +{ + std::string_view name = m_category.GetName(); + if (name.empty()) + { + return Status::Warning("Category name is empty. Categories should have a name"); + } + return Status::Ok(); +} + +Status UnpackValidator::Validate() const +{ + bool globalUnpack = m_options.GetUnpack(); + bool catUnpack = m_category.GetUnpack(); + + if (globalUnpack && !catUnpack) + { + return Status::Info( + "Unpack is disabled. Files will remain packed after download"); + } + + if (!globalUnpack && catUnpack) + { + return Status::Info("Global Unpack is disabled, so this category setting has no effect"); + } + + return Status::Ok(); +} +} // namespace SystemHealth::Category diff --git a/daemon/systemhealth/CategoryValidator.h b/daemon/systemhealth/CategoryValidator.h new file mode 100644 index 00000000..91067bd3 --- /dev/null +++ b/daemon/systemhealth/CategoryValidator.h @@ -0,0 +1,71 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CATEGORY_VALIDATOR_H +#define CATEGORY_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::Category +{ +class CategoryValidator final : public SectionValidator +{ +public: + explicit CategoryValidator(const Options& options, const Options::Category& category, + size_t id); + std::string_view GetName() const override { return m_name; } + +private: + const Options& m_options; + const Options::Category& m_category; + const std::string m_name; +}; + +class NameValidator : public Validator +{ +public: + explicit NameValidator(const Options::Category& category) : m_category(category) {} + + std::string_view GetName() const override { return "Name"; } + Status Validate() const override; + +private: + const Options::Category& m_category; +}; + +class UnpackValidator : public Validator +{ +public: + UnpackValidator(const Options& options, const Options::Category& category) + : m_options(options), m_category(category) + { + } + + std::string_view GetName() const override { return "Unpack"; } + Status Validate() const override; + +private: + const Options& m_options; + const Options::Category& m_category; +}; + +} // namespace SystemHealth::Category + +#endif diff --git a/daemon/systemhealth/CheckAndRepairValidator.cpp b/daemon/systemhealth/CheckAndRepairValidator.cpp new file mode 100644 index 00000000..4df415b2 --- /dev/null +++ b/daemon/systemhealth/CheckAndRepairValidator.cpp @@ -0,0 +1,236 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "CheckAndRepairValidator.h" +#include "Options.h" +#include "Validators.h" + +namespace SystemHealth::CheckAndRepair +{ +CheckAndRepairValidator::CheckAndRepairValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(16); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status CrcCheckValidator::Validate() const +{ + if (!m_options.GetCrcCheck()) + { + return Status::Info("Normally, '" + std::string(Options::CRCCHECK) + + "' should be enabled to better detect download errors and for " + "quick par-verification"); + } + return Status::Ok(); +} + +Status ParCheckValidator::Validate() const +{ + if (!m_options.GetParRepair()) return Status::Ok(); + + const auto parCheck = m_options.GetParCheck(); + switch (parCheck) + { + case Options::EParCheck::pcAuto: + return Status::Ok(); + + case Options::EParCheck::pcAlways: + return Status::Info("'" + std::string(Options::PARCHECK) + + "' is set to 'Always'. " + "Verification runs on every download, even undamaged ones. " + "Use 'Auto' to skip unnecessary checks and save CPU"); + + case Options::EParCheck::pcForce: + return Status::Info( + "'" + std::string(Options::PARCHECK) + + "' is set to 'Force'. " + "All PAR2 files are downloaded immediately, wasting bandwidth and CPU. " + "'Auto' is recommended"); + + case Options::EParCheck::pcManual: + return Status::Warning( + "Automatic repair is disabled. " + "Damaged downloads will require manual intervention"); + } + return Status::Ok(); +} + +Status ParRepairValidator::Validate() const +{ + if (!m_options.GetParRepair()) + { + return Status::Warning("'" + std::string(Options::PARREPAIR) + + "' is off. Corrupted files won't be automatically repaired"); + } + return Status::Ok(); +} + +Status ParScanValidator::Validate() const { return Status::Ok(); } + +Status ParQuickValidator::Validate() const +{ + if (!m_options.GetParQuick() && !m_options.GetParRepair()) return Status::Ok(); + if (!m_options.GetParQuick()) + { + return Status::Info( + "'" + std::string(Options::PARQUICK) + + "' is off. Files will be verified by reading from disk, which is slower"); + } + + return Status::Ok(); +} + +Status ParBufferValidator::Validate() const +{ + if (!m_options.GetParRepair()) return Status::Ok(); + + Status s = CheckPositiveNum(Options::PARBUFFER, m_options.GetParBuffer()); + if (!s.IsOk()) return s; + + const int buffer = m_options.GetParBuffer(); + if (buffer < 250) + { + return Status::Info( + "'" + std::string(Options::PARBUFFER) + "' is set to " + std::to_string(buffer) + + " MB. " + "Increasing to 250 MB or higher is recommended to speed up verification and repair"); + } + + return Status::Ok(); +} + +Status ParThreadsValidator::Validate() const +{ + if (!m_options.GetParRepair()) return Status::Ok(); + + Status s = CheckPositiveNum(Options::PARTHREADS, m_options.GetParThreads()); + if (!s.IsOk()) return s; + + const size_t threads = static_cast(m_options.GetParThreads()); + if (threads == 0) return Status::Ok(); + + const size_t hwThreads = std::thread::hardware_concurrency(); + if (hwThreads > 0 && threads > hwThreads) + { + return Status::Warning("'" + std::string(Options::PARTHREADS) + "' (" + + std::to_string(threads) + ") exceeds the number of CPU cores (" + + std::to_string(hwThreads) + + "). " + "This may slow down repair due to excessive context switching"); + } + + return Status::Ok(); +} + +Status ParIgnoreExtValidator::Validate() const { return Status::Ok(); } + +Status ParRenameValidator::Validate() const +{ + if (!m_options.GetParRename()) + { + return Status::Info(std::string(Options::PARRENAME) + + " is off. Original file names won't be restored from par2-files"); + } + return Status::Ok(); +} + +Status RarRenameValidator::Validate() const +{ + if (!m_options.GetRarRename()) + return Status::Info("'" + std::string(Options::RARRENAME) + + "' is off. Original file names won't be restored from rar-files"); + return Status::Ok(); +} + +Status DirectRenameValidator::Validate() const { return Status::Ok(); } + +Status HardLinkingValidator::Validate() const +{ + if (!m_options.GetHardLinking()) return Status::Ok(); + if (!m_options.GetDirectRename()) + { + return Status::Warning("'" + std::string(Options::HARDLINKING) + "' is enabled, but '" + + std::string(Options::DIRECTRENAME) + + "' is disabled. Hard linking will not be performed"); + } + + return Status::Ok(); +} + +Status HardLinkingIgnoreExtValidator::Validate() const { return Status::Ok(); } + +Status HealthCheckValidator::Validate() const +{ + const auto healthCheck = m_options.GetHealthCheck(); + switch (healthCheck) + { + case Options::EHealthCheck::hcDelete: + case Options::EHealthCheck::hcPark: + case Options::EHealthCheck::hcNone: + return Status::Ok(); + case Options::EHealthCheck::hcPause: + return Status::Warning("'" + std::string(Options::HEALTHCHECK) + + "' is set to 'Pause' you will need to manually move another " + "duplicate from history to queue"); + + default: + return Status::Ok(); + } +} + +Status ParTimeLimitValidator::Validate() const +{ + if (!m_options.GetParRepair()) return Status::Ok(); + + Status s = CheckPositiveNum(Options::PARTIMELIMIT, m_options.GetParTimeLimit()); + if (!s.IsOk()) return s; + + const int limit = m_options.GetParTimeLimit(); + if (limit == 0) return Status::Ok(); + if (limit < 5) + { + return Status::Info("'" + std::string(Options::PARTIMELIMIT) + "' is set to " + + std::to_string(limit) + + " minutes. " + "Large repairs often take longer, risking premature cancellation"); + } + + return Status::Ok(); +} + +Status ParPauseQueueValidator::Validate() const { return Status::Ok(); } + +} // namespace SystemHealth::CheckAndRepair diff --git a/daemon/systemhealth/CheckAndRepairValidator.h b/daemon/systemhealth/CheckAndRepairValidator.h new file mode 100644 index 00000000..330129fb --- /dev/null +++ b/daemon/systemhealth/CheckAndRepairValidator.h @@ -0,0 +1,217 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CHECK_AND_REPAIR_VALIDATOR_H +#define CHECK_AND_REPAIR_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::CheckAndRepair +{ +class CheckAndRepairValidator final : public SectionValidator +{ +public: + explicit CheckAndRepairValidator(const Options& options); + std::string_view GetName() const override { return "CheckAndRepair"; } + +private: + const Options& m_options; +}; + +// Individual validators +class CrcCheckValidator : public Validator +{ +public: + explicit CrcCheckValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CRCCHECK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParCheckValidator : public Validator +{ +public: + explicit ParCheckValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARCHECK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParRepairValidator : public Validator +{ +public: + explicit ParRepairValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARREPAIR; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParScanValidator : public Validator +{ +public: + explicit ParScanValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARSCAN; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParQuickValidator : public Validator +{ +public: + explicit ParQuickValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARQUICK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParBufferValidator : public Validator +{ +public: + explicit ParBufferValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARBUFFER; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParThreadsValidator : public Validator +{ +public: + explicit ParThreadsValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARTHREADS; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParIgnoreExtValidator : public Validator +{ +public: + explicit ParIgnoreExtValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARIGNOREEXT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParTimeLimitValidator : public Validator +{ +public: + explicit ParTimeLimitValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARTIMELIMIT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParRenameValidator : public Validator +{ +public: + explicit ParRenameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARRENAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RarRenameValidator : public Validator +{ +public: + explicit RarRenameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::RARRENAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class DirectRenameValidator : public Validator +{ +public: + explicit DirectRenameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::DIRECTRENAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class HardLinkingValidator : public Validator +{ +public: + explicit HardLinkingValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::HARDLINKING; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class HardLinkingIgnoreExtValidator : public Validator +{ +public: + explicit HardLinkingIgnoreExtValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::HARDLINKINGIGNOREEXT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class HealthCheckValidator : public Validator +{ +public: + explicit HealthCheckValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::HEALTHCHECK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ParPauseQueueValidator : public Validator +{ +public: + explicit ParPauseQueueValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PARPAUSEQUEUE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +} // namespace SystemHealth::CheckAndRepair + +#endif diff --git a/daemon/systemhealth/ConnectionValidator.cpp b/daemon/systemhealth/ConnectionValidator.cpp new file mode 100644 index 00000000..e28fdffe --- /dev/null +++ b/daemon/systemhealth/ConnectionValidator.cpp @@ -0,0 +1,215 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "ConnectionValidator.h" +#include "Validators.h" + +namespace SystemHealth::Connection +{ +ConnectionValidator::ConnectionValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(14); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status ArticleRetriesValidator::Validate() const +{ + int val = m_options.GetArticleRetries(); + + if (val < 0 || val > 99) + { + return Status::Error("'" + std::string(Options::ARTICLERETRIES) + + "' must be between 0 and 99"); + } + + if (val < 3) + { + return Status::Warning( + "'" + std::string(Options::ARTICLERETRIES) + "' is set to " + std::to_string(val) + + ". " + "This is very low; temporary connection drops may cause permanent download failures"); + } + + return Status::Ok(); +} + +Status ArticleIntervalValidator::Validate() const +{ + return CheckPositiveNum(Options::ARTICLEINTERVAL, m_options.GetArticleInterval()); +} + +Status ArticleTimeoutValidator::Validate() const +{ + int val = m_options.GetArticleTimeout(); + Status s = CheckPositiveNum(Options::ARTICLETIMEOUT, val); + if (!s.IsOk()) return s; + + if (val < 30) + { + return Status::Warning( + "'" + std::string(Options::ARTICLETIMEOUT) + "' is set to " + std::to_string(val) + + " seconds. " + "Slow server responses may cause valid connections to be dropped prematurely"); + } + + return Status::Ok(); +} + +Status ArticleReadChunkSizeValidator::Validate() const +{ + int val = m_options.GetArticleReadChunkSize(); + Status s = CheckPositiveNum(Options::ARTICLEREADCHUNKSIZE, val); + if (!s.IsOk()) return s; + + if (val < 4) + { + return Status::Warning( + "'" + std::string(Options::ARTICLEREADCHUNKSIZE) + "' is set to " + + std::to_string(val) + + " KB. " + "This is very low and may reduce download speed due to network overhead"); + } + + if (val > 10240) + { + return Status::Info( + "'" + std::string(Options::ARTICLEREADCHUNKSIZE) + "' is set to " + + std::to_string(val) + + " KB. " + "This buffer is allocated per connection; ensure sufficient system RAM is available"); + } + + return Status::Ok(); +} + +Status UrlRetriesValidator::Validate() const +{ + int val = m_options.GetUrlRetries(); + if (val < 0 || val > 99) + return Status::Error("'" + std::string(Options::URLRETRIES) + "' must be between 0 and 99"); + + return Status::Ok(); +} + +Status UrlIntervalValidator::Validate() const +{ + return CheckPositiveNum(Options::URLINTERVAL, m_options.GetUrlInterval()); +} + +Status UrlTimeoutValidator::Validate() const +{ + int val = m_options.GetUrlTimeout(); + Status s = CheckPositiveNum(Options::URLTIMEOUT, val); + if (!s.IsOk()) return s; + + if (val < 5) + { + return Status::Warning( + "'" + std::string(Options::URLTIMEOUT) + "' is set to " + std::to_string(val) + + " seconds. " + "RSS feeds and external URL fetches may fail if the remote server is slow"); + } + + return Status::Ok(); +} + +Status RemoteTimeoutValidator::Validate() const +{ + return CheckPositiveNum(Options::REMOTETIMEOUT, m_options.GetRemoteTimeout()); +} + +Status DownloadRateValidator::Validate() const +{ + int val = m_options.GetDownloadRate(); + Status s = CheckPositiveNum(Options::DOWNLOADRATE, val); + if (!s.IsOk()) return s; + + if (val == 0) return Status::Ok(); + + return Status::Warning("Global download speed is restricted to " + std::to_string(val) + + " KB/s by '" + std::string(Options::DOWNLOADRATE) + "'"); +} + +Status UrlConnectionsValidator::Validate() const +{ + int val = m_options.GetUrlConnections(); + if (val < 0 || val > 999) + { + return Status::Error("'" + std::string(Options::URLCONNECTIONS) + + "' must be between 0 and 999"); + } + return Status::Ok(); +} + +Status UrlForceValidator::Validate() const { return Status::Ok(); } + +Status MonthlyQuotaValidator::Validate() const +{ + int val = m_options.GetMonthlyQuota(); + Status s = CheckPositiveNum(Options::MONTHLYQUOTA, val); + if (!s.IsOk()) return s; + + if (val == 0) return Status::Ok(); // no quota + + return Status::Info("'" + std::string(Options::MONTHLYQUOTA) + "' is active (" + + std::to_string(val) + + " MB). " + "Downloads will pause if this limit is reached"); +} + +Status QuotaStartDayValidator::Validate() const +{ + int day = m_options.GetQuotaStartDay(); + if (day < 1 || day > 31) + return Status::Error("'" + std::string(Options::QUOTASTARTDAY) + + "' must be between 1 and 31"); + + return Status::Ok(); +} + +Status DailyQuotaValidator::Validate() const +{ + int val = m_options.GetDailyQuota(); + Status s = CheckPositiveNum(Options::DAILYQUOTA, val); + if (!s.IsOk()) return s; + + if (val == 0) return Status::Ok(); + + return Status::Info("'" + std::string(Options::DAILYQUOTA) + "' is active (" + + std::to_string(val) + + " MB). " + "Downloads will pause if this limit is reached"); +} + +} // namespace SystemHealth::Connection diff --git a/daemon/systemhealth/ConnectionValidator.h b/daemon/systemhealth/ConnectionValidator.h new file mode 100644 index 00000000..50549b12 --- /dev/null +++ b/daemon/systemhealth/ConnectionValidator.h @@ -0,0 +1,194 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CONNECTION_VALIDATOR_H +#define CONNECTION_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::Connection +{ +class ConnectionValidator final : public SectionValidator +{ +public: + explicit ConnectionValidator(const Options& options); + std::string_view GetName() const override { return "Connection"; } + +private: + const Options& m_options; +}; + +class ArticleRetriesValidator : public Validator +{ +public: + explicit ArticleRetriesValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ARTICLERETRIES; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ArticleIntervalValidator : public Validator +{ +public: + explicit ArticleIntervalValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ARTICLEINTERVAL; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ArticleTimeoutValidator : public Validator +{ +public: + explicit ArticleTimeoutValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ARTICLETIMEOUT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ArticleReadChunkSizeValidator : public Validator +{ +public: + explicit ArticleReadChunkSizeValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ARTICLEREADCHUNKSIZE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UrlRetriesValidator : public Validator +{ +public: + explicit UrlRetriesValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::URLRETRIES; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UrlIntervalValidator : public Validator +{ +public: + explicit UrlIntervalValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::URLINTERVAL; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UrlTimeoutValidator : public Validator +{ +public: + explicit UrlTimeoutValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::URLTIMEOUT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RemoteTimeoutValidator : public Validator +{ +public: + explicit RemoteTimeoutValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::REMOTETIMEOUT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class DownloadRateValidator : public Validator +{ +public: + explicit DownloadRateValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::DOWNLOADRATE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UrlConnectionsValidator : public Validator +{ +public: + explicit UrlConnectionsValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::URLCONNECTIONS; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UrlForceValidator : public Validator +{ +public: + explicit UrlForceValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::URLFORCE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class MonthlyQuotaValidator : public Validator +{ +public: + explicit MonthlyQuotaValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::MONTHLYQUOTA; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class QuotaStartDayValidator : public Validator +{ +public: + explicit QuotaStartDayValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::QUOTASTARTDAY; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class DailyQuotaValidator : public Validator +{ +public: + explicit DailyQuotaValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::DAILYQUOTA; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +} // namespace SystemHealth::Connection + +#endif diff --git a/daemon/systemhealth/DisplayValidator.cpp b/daemon/systemhealth/DisplayValidator.cpp new file mode 100644 index 00000000..9ce50c52 --- /dev/null +++ b/daemon/systemhealth/DisplayValidator.cpp @@ -0,0 +1,91 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "DisplayValidator.h" +#include "Status.h" + +namespace SystemHealth::Display +{ + +DisplayValidator::DisplayValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(5); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status OutputModeValidator::Validate() const +{ + auto mode = m_options.GetOutputMode(); + + if (mode == Options::EOutputMode::omLoggable || mode == Options::EOutputMode::omColored || + mode == Options::EOutputMode::omNCurses) + { + return Status::Ok(); + } + + return Status::Error("'" + std::string(Options::OUTPUTMODE) + "' has an invalid value"); +} + +Status UpdateIntervalValidator::Validate() const +{ + int v = m_options.GetUpdateInterval(); + if (v == 0) return Status::Ok(); + if (v < 25) + return Status::Error("'" + std::string(Options::UPDATEINTERVAL) + "' must be >= 25 ms."); + return Status::Ok(); +} + +Status CursesNzbNameValidator::Validate() const +{ + if (m_options.GetOutputMode() != Options::EOutputMode::omNCurses && + m_options.GetCursesNzbName()) + { + return Status::Info("'" + std::string(Options::CURSESNZBNAME) + + "' applies only when OutputMode=curses"); + } + return Status::Ok(); +} + +Status CursesGroupValidator::Validate() const +{ + if (m_options.GetOutputMode() != Options::EOutputMode::omNCurses && m_options.GetCursesGroup()) + { + return Status::Info("'" + std::string(Options::CURSESGROUP) + + "' applies only when OutputMode=curses"); + } + return Status::Ok(); +} + +Status CursesTimeValidator::Validate() const +{ + if (m_options.GetOutputMode() != Options::EOutputMode::omNCurses && m_options.GetCursesTime()) + { + return Status::Info("'" + std::string(Options::CURSESTIME) + + "' applies only when OutputMode=curses"); + } + return Status::Ok(); +} + +} // namespace SystemHealth::Display diff --git a/daemon/systemhealth/DisplayValidator.h b/daemon/systemhealth/DisplayValidator.h new file mode 100644 index 00000000..64e6c35e --- /dev/null +++ b/daemon/systemhealth/DisplayValidator.h @@ -0,0 +1,96 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DISPLAY_VALIDATOR_H +#define DISPLAY_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::Display +{ + +class DisplayValidator final : public SectionValidator +{ +public: + explicit DisplayValidator(const Options& options); + std::string_view GetName() const override { return "Display"; } + +private: + const Options& m_options; +}; + +class OutputModeValidator final : public Validator +{ +public: + explicit OutputModeValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::OUTPUTMODE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UpdateIntervalValidator final : public Validator +{ +public: + explicit UpdateIntervalValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::UPDATEINTERVAL; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class CursesNzbNameValidator final : public Validator +{ +public: + explicit CursesNzbNameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CURSESNZBNAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class CursesGroupValidator final : public Validator +{ +public: + explicit CursesGroupValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CURSESGROUP; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class CursesTimeValidator final : public Validator +{ +public: + explicit CursesTimeValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CURSESTIME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +} // namespace SystemHealth::Display + +#endif diff --git a/daemon/systemhealth/DownloadQueueValidator.cpp b/daemon/systemhealth/DownloadQueueValidator.cpp new file mode 100644 index 00000000..8e7c8c57 --- /dev/null +++ b/daemon/systemhealth/DownloadQueueValidator.cpp @@ -0,0 +1,279 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#include "nzbget.h" + +#include "DownloadQueueValidator.h" +#include "Options.h" +#include "Validators.h" + +namespace SystemHealth::DownloadQueue +{ +DownloadQueueValidator::DownloadQueueValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(17); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status FlushQueueValidator::Validate() const +{ + if (m_options.GetFlushQueue() && m_options.GetSkipWrite()) + { + return Status::Warning("'" + std::string(Options::FLUSHQUEUE) + "' is enabled while '" + + std::string(Options::SKIPWRITE) + + "' is enabled: flushed data may not be written to disk"); + } + return Status::Ok(); +} + +Status ContinuePartialValidator::Validate() const +{ + if (m_options.GetContinuePartial()) + return Status::Info("Disabling '" + std::string(Options::CONTINUEPARTIAL) + + "' may slightly reduce disk access and is recommended on fast " + "connections"); + + return Status::Ok(); +} + +Status PropagationDelayValidator::Validate() const +{ + return CheckPositiveNum(Options::PROPAGATIONDELAY, m_options.GetPropagationDelay()); +} + +Status ArticleCacheValidator::Validate() const +{ + int val = m_options.GetArticleCache(); + Status s = CheckPositiveNum(Options::ARTICLECACHE, val); + if (!s.IsOk()) return s; + + if (val == 0) + return Status::Warning( + "'" + std::string(Options::ARTICLECACHE) + + "' is disabled. Enabling it is recommended to reduce disk fragmentation"); + + // Check for 32-bit architecture limit (1900 MB) + if (sizeof(void*) == 4 && val > 1900) + return Status::Error("'" + std::string(Options::ARTICLECACHE) + + "' cannot exceed 1900 MB on 32-bit systems"); + + if (m_options.GetDirectWrite()) + { + if (val < 50) + { + return Status::Warning("It's recommended to set '" + + std::string(Options::ARTICLECACHE) + "' at least 50MB"); + } + else + { + return Status::Ok(); + } + } + else if (val < 200) + { + return Status::Warning( + "A cache under 200 MB is likely too small to hold complete files, " + "forcing writes to the temporary directory and degrading performance"); + } + + return Status::Ok(); +} + +Status DirectWriteValidator::Validate() const +{ + if (!m_options.GetDirectWrite() && !m_options.GetRawArticle()) + return Status::Warning( + "'" + std::string(Options::DIRECTWRITE) + + "' is disabled. " + "Articles will be written to the temporary directory first, then copied to the " + "destination. " + "Enabling this option is usually recommended to reduce disk I/O usage"); + + return Status::Ok(); +} + +Status WriteBufferValidator::Validate() const +{ + int val = m_options.GetWriteBuffer(); + Status s = CheckPositiveNum(Options::WRITEBUFFER, val); + if (!s.IsOk()) return s; + + if (val == 0) + { + return Status::Warning( + "'" + std::string(Options::WRITEBUFFER) + + "' is set to '0'. " + "This uses the default system buffer, which is often too small and inefficient"); + } + + if (val < 1024) + { + return Status::Warning( + "'" + std::string(Options::WRITEBUFFER) + + "' is very low. " + "At least 1024 KB is recommended for systems with sufficient memory"); + } + + // Warn if buffer is excessively large (e.g., > 100MB per connection) + if (val > 102400) + { + return Status::Warning( + "'" + std::string(Options::WRITEBUFFER) + + "' is very large (>100MB). This is per-connection and may exhaust memory"); + } + return Status::Ok(); +} + +Status FileNamingValidator::Validate() const +{ + const auto val = m_options.GetFileNaming(); + switch (val) + { + case Options::nfAuto: + return Status::Ok(); + + case Options::EFileNaming::nfNzb: + return Status::Info("'" + std::string(Options::FILENAMING) + + "' is set to 'Nzb'. " + "Files will be named strictly based on the NZB content. " + "Obfuscated releases often result in meaningless filenames with " + "this setting. 'Auto' is recommended"); + + case Options::EFileNaming::nfArticle: + return Status::Info("'" + std::string(Options::FILENAMING) + + "' is set to 'Article'. " + "Files will be named using subject headers from the articles. " + "If headers are malformed or missing, filenames will be incorrect. " + "'Auto' is recommended"); + + default: + return Status::Ok(); + } +} + +Status RenameAfterUnpackValidator::Validate() const { return Status::Ok(); } + +Status RenameIgnoreExtValidator::Validate() const { return Status::Ok(); } + +Status ReorderFilesValidator::Validate() const { return Status::Ok(); } + +Status PostStrategyValidator::Validate() const +{ + const auto strategy = m_options.GetPostStrategy(); + switch (strategy) + { + case Options::EPostStrategy::ppBalanced: + return Status::Ok(); + + case Options::EPostStrategy::ppSequential: + return Status::Info("'" + std::string(Options::POSTSTRATEGY) + + "' is set to 'Sequential'. " + "Safe for low-end hardware, but may be slower."); + + case Options::EPostStrategy::ppAggressive: + return Status::Info( + "'" + std::string(Options::POSTSTRATEGY) + + "' is set to 'Aggressive'. " + "Ensure you have a multi-core CPU and fast storage (SSD) to prevent bottlenecks"); + + case Options::EPostStrategy::ppRocket: + return Status::Info("'" + std::string(Options::POSTSTRATEGY) + + "' is set to 'Rocket'. " + "This requires high-end hardware (NVMe SSD, many CPU cores)"); + + default: + return Status::Ok(); + } +} + +Status DiskSpaceValidator::Validate() const +{ + int val = m_options.GetDiskSpace(); + Status s = CheckPositiveNum(Options::DISKSPACE, val); + if (!s.IsOk()) return s; + + // 0 means disabled, which is fine. + // If enabled but very low (e.g. < 50MB), it might be too late to pause effectively. + if (val > 0 && val < 50) + { + return Status::Warning("'" + std::string(Options::DISKSPACE) + + "' is set very low (<50MB). Downloads may fill disk before pausing"); + } + return Status::Ok(); +} + +Status NzbCleanupDiskValidator::Validate() const +{ + if (!m_options.GetNzbCleanupDisk()) + return Status::Info( + "'" + std::string(Options::NZBCLEANUPDISK) + + "' is disabled. " + "Source NZB files will remain in the incoming directory after processing"); + + return Status::Ok(); +} + +Status KeepHistoryValidator::Validate() const +{ + return CheckPositiveNum(Options::KEEPHISTORY, m_options.GetKeepHistory()); +} + +Status FeedHistoryValidator::Validate() const +{ + return CheckPositiveNum(Options::FEEDHISTORY, m_options.GetFeedHistory()); +} + +Status SkipWriteValidator::Validate() const +{ + if (m_options.GetSkipWrite()) + { + return Status::Warning("'" + std::string(Options::SKIPWRITE) + + "' is enabled: downloaded data will NOT be saved to disk"); + } + return Status::Ok(); +} + +Status RawArticleValidator::Validate() const +{ + if (m_options.GetRawArticle()) + { + return Status::Warning( + "'" + std::string(Options::RAWARTICLE) + + "' is enabled: articles will be saved in raw format (unusable for normal files)"); + } + return Status::Ok(); +} +} // namespace SystemHealth::DownloadQueue diff --git a/daemon/systemhealth/DownloadQueueValidator.h b/daemon/systemhealth/DownloadQueueValidator.h new file mode 100644 index 00000000..7fc9c0d5 --- /dev/null +++ b/daemon/systemhealth/DownloadQueueValidator.h @@ -0,0 +1,227 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DOWNLOAD_QUEUE_VALIDATOR_H +#define DOWNLOAD_QUEUE_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::DownloadQueue +{ +class DownloadQueueValidator final : public SectionValidator +{ +public: + explicit DownloadQueueValidator(const Options& options); + std::string_view GetName() const override { return "DownloadQueue"; } + +private: + const Options& m_options; +}; + +class FlushQueueValidator : public Validator +{ +public: + explicit FlushQueueValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::FLUSHQUEUE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ContinuePartialValidator : public Validator +{ +public: + explicit ContinuePartialValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CONTINUEPARTIAL; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class PropagationDelayValidator : public Validator +{ +public: + explicit PropagationDelayValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::PROPAGATIONDELAY; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ArticleCacheValidator : public Validator +{ +public: + explicit ArticleCacheValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ARTICLECACHE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class DirectWriteValidator : public Validator +{ +public: + explicit DirectWriteValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::DIRECTWRITE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class WriteBufferValidator : public Validator +{ +public: + explicit WriteBufferValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::WRITEBUFFER; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class FileNamingValidator : public Validator +{ +public: + explicit FileNamingValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::FILENAMING; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RenameAfterUnpackValidator : public Validator +{ +public: + explicit RenameAfterUnpackValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::RENAMEAFTERUNPACK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RenameIgnoreExtValidator : public Validator +{ +public: + explicit RenameIgnoreExtValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::RENAMEIGNOREEXT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ReorderFilesValidator : public Validator +{ +public: + explicit ReorderFilesValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::REORDERFILES; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class PostStrategyValidator : public Validator +{ +public: + explicit PostStrategyValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::POSTSTRATEGY; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class DiskSpaceValidator : public Validator +{ +public: + explicit DiskSpaceValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::DISKSPACE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class NzbCleanupDiskValidator : public Validator +{ +public: + explicit NzbCleanupDiskValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::NZBCLEANUPDISK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class KeepHistoryValidator : public Validator +{ +public: + explicit KeepHistoryValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::KEEPHISTORY; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class FeedHistoryValidator : public Validator +{ +public: + explicit FeedHistoryValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::FEEDHISTORY; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class SkipWriteValidator : public Validator +{ +public: + explicit SkipWriteValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SKIPWRITE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RawArticleValidator : public Validator +{ +public: + explicit RawArticleValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::RAWARTICLE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +} // namespace SystemHealth::DownloadQueue + +#endif diff --git a/daemon/systemhealth/ExtensionScriptsValidator.cpp b/daemon/systemhealth/ExtensionScriptsValidator.cpp new file mode 100644 index 00000000..8342ebf2 --- /dev/null +++ b/daemon/systemhealth/ExtensionScriptsValidator.cpp @@ -0,0 +1,133 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Status.h" +#include "nzbget.h" + +#include "ExtensionScriptsValidator.h" +#include "Validators.h" +#include "Options.h" +#include "Util.h" + +namespace SystemHealth::ExtensionScripts +{ +ExtensionScriptsValidator::ExtensionScriptsValidator(const Options& options, + const ExtensionManager::Manager& manager) + : m_options(options), m_extensionManager(manager) +{ + m_validators.reserve(5); + m_validators.push_back(std::make_unique(options, manager)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status ExtensionListValidator::Validate() const +{ + std::string_view extensions = m_options.GetExtensions(); + if (extensions.empty()) return Status::Ok(); + + std::string message; + Tokenizer tokDir(extensions.data(), ",;"); + while (const char* scriptName = tokDir.Next()) + { + const auto extension = + m_extensionManager.FindIf([&](const auto ext) + { + std::string_view name = ext->GetName(); + return name == scriptName; + }); + if (!extension) + { + if (!message.empty()) message += "; "; + message += std::string("'") + scriptName + "' doesn't exist"; + continue; + } + + const auto exists = File::Exists(extension.value()->GetEntry()); + if (!exists.IsOk()) + { + if (!message.empty()) message += "; "; + message += exists.GetMessage() + " "; + } + } + + if (message.empty()) return Status::Ok(); + + return Status::Warning(std::move(message)); +} + +Status ScriptPauseQueueValidator::Validate() const +{ + return Status::Ok(); +} + +Status ShellOverrideValidator::Validate() const +{ + std::string_view path = m_options.GetShellOverride(); + if (path.empty()) return Status::Ok(); + + std::string message; + Tokenizer tok(path.data(), ",;"); + while (char* shellover = tok.Next()) + { + char* shellcmd = strchr(shellover, '='); + if (shellcmd) + { + *shellcmd = '\0'; + ++shellcmd; + const auto exists = File::Exists(shellcmd); + if (!exists.IsOk()) + { + if (!message.empty()) message += "; "; + message += exists.GetMessage() + " "; + continue; + } + const auto exe = File::Executable(shellcmd); + if (!exe.IsOk()) + { + if (!message.empty()) message += "; "; + message += exe.GetMessage() + " "; + } + } + } + + if (message.empty()) return Status::Ok(); + + return Status::Warning(std::move(message)); +} + +Status EventIntervalValidator::Validate() const +{ + int val = m_options.GetEventInterval(); + if (val < -1) + { + return Status::Error("'" + std::string(Options::EVENTINTERVAL) + + "' cannot be less than -1"); + } + + return Status::Ok(); +} + +Status ScriptOrderValidator::Validate() const +{ + return Status::Ok(); +} +} // namespace SystemHealth::ExtensionScripts diff --git a/daemon/systemhealth/ExtensionScriptsValidator.h b/daemon/systemhealth/ExtensionScriptsValidator.h new file mode 100644 index 00000000..c318ba16 --- /dev/null +++ b/daemon/systemhealth/ExtensionScriptsValidator.h @@ -0,0 +1,105 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EXTENSION_SCRIPTS_VALIDATOR_H +#define EXTENSION_SCRIPTS_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" +#include "Validators.h" +#include "ExtensionManager.h" +#include + +namespace SystemHealth::ExtensionScripts +{ +class ExtensionScriptsValidator final : public SectionValidator +{ +public: + explicit ExtensionScriptsValidator(const Options& options, + const ExtensionManager::Manager& manager); + std::string_view GetName() const override { return "ExtensionScripts"; } + +private: + const Options& m_options; + const ExtensionManager::Manager& m_extensionManager; +}; + +class ExtensionListValidator final : public Validator +{ +public: + explicit ExtensionListValidator(const Options& options, + const ExtensionManager::Manager& manager) + : m_options(options), m_extensionManager(manager) + { + } + std::string_view GetName() const override { return Options::EXTENSIONS; } + Status Validate() const override; + +private: + const Options& m_options; + const ExtensionManager::Manager& m_extensionManager; +}; + +class ScriptOrderValidator : public Validator +{ +public: + explicit ScriptOrderValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SCRIPTORDER; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ScriptPauseQueueValidator : public Validator +{ +public: + explicit ScriptPauseQueueValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SCRIPTPAUSEQUEUE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ShellOverrideValidator : public Validator +{ +public: + explicit ShellOverrideValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SHELLOVERRIDE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class EventIntervalValidator : public Validator +{ +public: + explicit EventIntervalValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::EVENTINTERVAL; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +} // namespace SystemHealth::ExtensionScripts + +#endif diff --git a/daemon/systemhealth/FeedValidator.cpp b/daemon/systemhealth/FeedValidator.cpp new file mode 100644 index 00000000..50388e0d --- /dev/null +++ b/daemon/systemhealth/FeedValidator.cpp @@ -0,0 +1,91 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "FeedValidator.h" +#include "FeedInfo.h" +#include "Options.h" + +namespace SystemHealth::Feeds +{ + +FeedValidator::FeedValidator(const FeedInfo& feed, const Options& options) + : m_feed(feed), m_options(options), m_name("Feed" + std::to_string(feed.GetId())) +{ + m_validators.reserve(6); + m_validators.push_back(std::make_unique(feed)); + m_validators.push_back(std::make_unique(feed)); + m_validators.push_back(std::make_unique(feed)); + m_validators.push_back(std::make_unique(feed)); + m_validators.push_back(std::make_unique(feed)); + m_validators.push_back(std::make_unique(feed, options)); +} + +Status NameValidator::Validate() const +{ + std::string_view name = m_feed.GetName(); + if (name.empty()) + { + return Status::Info("Name is recommended for clearer logs and troubleshooting"); + } + return Status::Ok(); +} + +Status UrlValidator::Validate() const +{ + std::string_view url = m_feed.GetUrl(); + if (url.empty()) + { + return Status::Warning("URL is required"); + } + + if (url.find("http://") == std::string_view::npos && + url.find("https://") == std::string_view::npos) + { + return Status::Warning("URL does not start with 'http://' or 'https://'"); + } + + return Status::Ok(); +} + +Status IntervalValidator::Validate() const +{ + int interval = m_feed.GetInterval(); + const auto check = CheckPositiveNum("Interval", interval); + if (!check.IsOk()) return check; + + if (interval == 0) + { + return Status::Info("Automatic check is disabled"); + } + + return Status::Ok(); +} + +Status FilterValidator::Validate() const { return Status::Ok(); } + +Status ScriptsValidator::Validate() const { return Status::Ok(); } + +Status CategoryValidator::Validate() const +{ + return Status::Ok(); +} + +} // namespace SystemHealth::Feeds diff --git a/daemon/systemhealth/FeedValidator.h b/daemon/systemhealth/FeedValidator.h new file mode 100644 index 00000000..a175e6ab --- /dev/null +++ b/daemon/systemhealth/FeedValidator.h @@ -0,0 +1,117 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FEED_VALIDATOR_H +#define FEED_VALIDATOR_H + +#include "SectionValidator.h" +#include "FeedInfo.h" +#include "Validators.h" +#include "Options.h" + +namespace SystemHealth::Feeds +{ + +class FeedValidator final : public SectionValidator +{ +public: + FeedValidator(const FeedInfo& feed, const Options& options); + std::string_view GetName() const override { return m_name; } + +private: + const FeedInfo& m_feed; + const Options& m_options; + const std::string m_name; +}; + +class NameValidator final : public Validator +{ +public: + explicit NameValidator(const FeedInfo& feed) : m_feed(feed) {} + std::string_view GetName() const override { return "Name"; } + Status Validate() const override; + +private: + const FeedInfo& m_feed; +}; + +class UrlValidator final : public Validator +{ +public: + explicit UrlValidator(const FeedInfo& feed) : m_feed(feed) {} + std::string_view GetName() const override { return "URL"; } + Status Validate() const override; + +private: + const FeedInfo& m_feed; +}; + +class IntervalValidator final : public Validator +{ +public: + explicit IntervalValidator(const FeedInfo& feed) : m_feed(feed) {} + std::string_view GetName() const override { return "Interval"; } + Status Validate() const override; + +private: + const FeedInfo& m_feed; +}; + +class FilterValidator final : public Validator +{ +public: + explicit FilterValidator(const FeedInfo& feed) : m_feed(feed) {} + std::string_view GetName() const override { return "Filter"; } + Status Validate() const override; + +private: + const FeedInfo& m_feed; +}; + +class ScriptsValidator final : public Validator +{ +public: + ScriptsValidator(const FeedInfo& feed) : m_feed(feed) {} + + std::string_view GetName() const override { return "Extensions"; } + Status Validate() const override; + +private: + const FeedInfo& m_feed; +}; + +class CategoryValidator final : public Validator +{ +public: + CategoryValidator(const FeedInfo& feed, const Options& options) + : m_feed(feed), m_options(options) + { + } + + std::string_view GetName() const override { return "Category"; } + Status Validate() const override; + +private: + const FeedInfo& m_feed; + const Options& m_options; +}; + +} // namespace SystemHealth::Feeds + +#endif diff --git a/daemon/systemhealth/FeedsValidator.cpp b/daemon/systemhealth/FeedsValidator.cpp new file mode 100644 index 00000000..f232034f --- /dev/null +++ b/daemon/systemhealth/FeedsValidator.cpp @@ -0,0 +1,71 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "FeedsValidator.h" +#include "FeedValidator.h" + +namespace SystemHealth::Feeds +{ +FeedsValidator::FeedsValidator(const ::Feeds& feeds, const Options& options) + : SectionGroupValidator(MakeFeedValidators(feeds)), m_feeds(feeds), m_options(options) +{ + m_validators.reserve(1); + m_validators.push_back(std::make_unique(feeds)); +} +std::vector> FeedsValidator::MakeFeedValidators( + const ::Feeds& feeds) const +{ + std::vector> validators; + validators.reserve(feeds.size()); + for (const auto& feed : feeds) + { + validators.push_back(std::make_unique(*feed, m_options)); + } + return validators; +} + +Status DuplicateFeedsValidator::Validate() const +{ + if (m_feeds.empty()) return Status::Ok(); + + std::map seenUrls; + + for (const auto& feed : m_feeds) + { + std::string_view url = feed->GetUrl(); + if (url.empty()) continue; + + auto it = seenUrls.find(url); + if (it != seenUrls.end()) + { + std::string_view firstFeedName = it->second; + std::string_view currentFeedName = feed->GetName(); + + return Status::Warning("Feeds '" + std::string(firstFeedName) + "' and '" + + std::string(currentFeedName) + "' have the same URL"); + } + + seenUrls.emplace(url, feed->GetName()); + } + + return Status::Ok(); +} +} // namespace SystemHealth::Feeds diff --git a/daemon/systemhealth/FeedsValidator.h b/daemon/systemhealth/FeedsValidator.h new file mode 100644 index 00000000..80ac86a2 --- /dev/null +++ b/daemon/systemhealth/FeedsValidator.h @@ -0,0 +1,59 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FEEDS_VALIDATOR_H +#define FEEDS_VALIDATOR_H + +#include +#include "FeedInfo.h" +#include "Options.h" +#include "SectionGroupValidator.h" + +namespace SystemHealth::Feeds +{ + +class FeedsValidator final : public SectionGroupValidator +{ +public: + explicit FeedsValidator(const ::Feeds& feeds, const Options& options); + std::string_view GetName() const override { return "Feeds"; } + +private: + const ::Feeds& m_feeds; + const Options& m_options; + std::vector> MakeFeedValidators(const ::Feeds& feeds) const; +}; + +class DuplicateFeedsValidator : public Validator +{ +public: + explicit DuplicateFeedsValidator(const ::Feeds& feeds) + : m_feeds(feeds) + { + } + std::string_view GetName() const override { return ""; } + Status Validate() const override; + +private: + const ::Feeds& m_feeds; +}; + +} // namespace SystemHealth::Feeds + +#endif diff --git a/daemon/systemhealth/IncomingNzbValidator.cpp b/daemon/systemhealth/IncomingNzbValidator.cpp new file mode 100644 index 00000000..2e419da9 --- /dev/null +++ b/daemon/systemhealth/IncomingNzbValidator.cpp @@ -0,0 +1,99 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "IncomingNzbValidator.h" + +namespace SystemHealth::IncomingNzb +{ +IncomingNzbValidator::IncomingNzbValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(4); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status AppendCategoryDirValidator::Validate() const +{ + bool append = m_options.GetAppendCategoryDir(); + if (append) return Status::Info("Category subdirectories will be created automatically"); + + return Status::Ok(); +} + +Status NzbDirIntervalValidator::Validate() const +{ + int interval = m_options.GetNzbDirInterval(); + Status s = CheckPositiveNum(Options::NZBDIRINTERVAL, interval); + if (!s.IsOk()) return s; + + if (interval == 0) return Status::Warning("Automatic NZB directory scanning is disabled"); + + if (interval < 3) + { + std::stringstream ss; + ss << "'" << Options::NZBDIRINTERVAL << "' is set to " << interval << " seconds. " + << "Very frequent scanning may cause high CPU/Disk usage"; + return Status::Warning(ss.str()); + } + + return Status::Ok(); +} + +Status NzbDirFileAgeValidator::Validate() const +{ + int age = m_options.GetNzbDirFileAge(); + Status s = CheckPositiveNum(Options::NZBDIRFILEAGE, age); + if (!s.IsOk()) return s; + + if (age > 3600) // 1 Hour + { + std::stringstream ss; + ss << "'" << Options::NZBDIRFILEAGE << "' is set to " << age << " seconds (> 1 hour). " + << "New NZB files will not be picked up until they are at least this old"; + return Status::Warning(ss.str()); + } + + return Status::Ok(); +} + +Status DupeCheckValidator::Validate() const +{ + bool dupeCheck = m_options.GetDupeCheck(); + auto healthCheckMode = m_options.GetHealthCheck(); + if (!dupeCheck) return Status::Ok(); + + if (healthCheckMode == Options::EHealthCheck::hcPause) + { + const auto healthCheckStr = std::string(Options::HEALTHCHECK); + return Status::Info( + "'" + std::string(Options::DUPECHECK) + "' is enabled while '" + healthCheckStr + + "' is set to 'Pause'. " + "This configuration is not recommended as it requires manual intervention " + "to unpause backup downloads if the primary one fails. " + "Consider using 'Delete', 'Park', or 'None' for '" + + healthCheckStr + "'"); + } + + return Status::Ok(); +} +} // namespace SystemHealth::IncomingNzb diff --git a/daemon/systemhealth/IncomingNzbValidator.h b/daemon/systemhealth/IncomingNzbValidator.h new file mode 100644 index 00000000..6872fe0b --- /dev/null +++ b/daemon/systemhealth/IncomingNzbValidator.h @@ -0,0 +1,84 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCOMING_NZB_VALIDATOR_H +#define INCOMING_NZB_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::IncomingNzb +{ +class IncomingNzbValidator final : public SectionValidator +{ +public: + explicit IncomingNzbValidator(const Options& options); + std::string_view GetName() const override { return "IncomingNzbs"; } + +private: + const Options& m_options; +}; + +class AppendCategoryDirValidator final : public Validator +{ +public: + explicit AppendCategoryDirValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::APPENDCATEGORYDIR; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class NzbDirIntervalValidator final : public Validator +{ +public: + explicit NzbDirIntervalValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::NZBDIRINTERVAL; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class NzbDirFileAgeValidator final : public Validator +{ +public: + explicit NzbDirFileAgeValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::NZBDIRFILEAGE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class DupeCheckValidator final : public Validator +{ +public: + explicit DupeCheckValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::DUPECHECK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +} // namespace SystemHealth::IncomingNzb + +#endif diff --git a/daemon/systemhealth/LoggingValidator.cpp b/daemon/systemhealth/LoggingValidator.cpp new file mode 100644 index 00000000..40fc95a2 --- /dev/null +++ b/daemon/systemhealth/LoggingValidator.cpp @@ -0,0 +1,109 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "LoggingValidator.h" +#include "Options.h" +#include "Validators.h" + +namespace SystemHealth::Logging +{ +LoggingValidator::LoggingValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(7); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status WriteLogValidator::Validate() const +{ + const auto writeLog = m_options.GetWriteLog(); + switch (writeLog) + { + case Options::EWriteLog::wlNone: + return Status::Info( + "Logging is disabled. Logging is recommended for " + "effective debugging and troubleshooting"); + case Options::EWriteLog::wlAppend: + return Status::Warning("'" + std::string(Options::WRITELOG) + + "' is set to 'Append'. The log file may grow indefinitely"); + case Options::EWriteLog::wlReset: + case Options::EWriteLog::wlRotate: + return Status::Ok(); + } + return Status::Ok(); +} + +Status RotateLogValidator::Validate() const +{ + if (m_options.GetWriteLog() != Options::EWriteLog::wlRotate) return Status::Ok(); + + Status s = CheckPositiveNum(Options::ROTATELOG, m_options.GetRotateLog()); + if (!s.IsOk()) return s; + + int rotateLog = m_options.GetRotateLog(); + if (rotateLog > 365) + return Status::Warning("'" + std::string(Options::ROTATELOG) + + "' is very high (>365). This may consume significant disk space"); + + return Status::Ok(); +} +Status LogBufferValidator::Validate() const +{ + Status s = CheckPositiveNum(Options::LOGBUFFER, m_options.GetLogBuffer()); + if (!s.IsOk()) return s; + + if (m_options.GetLogBuffer() < 100) + return Status::Info("'" + std::string(Options::LOGBUFFER) + + "' is very low. You might miss recent messages in the web UI"); + return Status::Ok(); +} + +Status CrashDumpValidator::Validate() const +{ +#ifdef __linux__ + if (m_options.GetCrashDump()) + return Status::Info( + "'" + std::string(Options::CRASHDUMP) + + "' is enabled. Memory dumps may contain sensitive data (passwords, keys)"); +#endif + + return Status::Ok(); +} + +Status TimeCorrectionValidator::Validate() const +{ + int val = m_options.GetTimeCorrection(); + + // Config says: -24..+24 are hours, others are minutes. + // If someone sets 10000 (minutes), that's ~7 days offset, likely a mistake. + if (std::abs(val) > 1440 && std::abs(val) < 1000000) // 1440 mins = 24 hours + return Status::Warning("'" + std::string(Options::TIMECORRECTION) + + "' value is very large (interpreted as minutes)"); + + return Status::Ok(); +} + +} // namespace SystemHealth::Logging \ No newline at end of file diff --git a/daemon/systemhealth/LoggingValidator.h b/daemon/systemhealth/LoggingValidator.h new file mode 100644 index 00000000..67933957 --- /dev/null +++ b/daemon/systemhealth/LoggingValidator.h @@ -0,0 +1,118 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LOGGING_VALIDATOR_H +#define LOGGING_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::Logging +{ + +class LoggingValidator final : public SectionValidator +{ +public: + explicit LoggingValidator(const Options& options); + std::string_view GetName() const override { return "Logging"; } + +private: + const Options& m_options; +}; + +class WriteLogValidator final : public Validator +{ +public: + explicit WriteLogValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::WRITELOG; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RotateLogValidator final : public Validator +{ +public: + explicit RotateLogValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ROTATELOG; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class LogBufferValidator final : public Validator +{ +public: + explicit LogBufferValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::LOGBUFFER; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class NzbLogValidator final : public Validator +{ +public: + explicit NzbLogValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::NZBLOG; } + Status Validate() const override { return Status::Ok(); } + +private: + const Options& m_options; +}; + +class CrashTraceValidator final : public Validator +{ +public: + explicit CrashTraceValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CRASHTRACE; } + Status Validate() const override { return Status::Ok(); } + +private: + const Options& m_options; +}; + +class CrashDumpValidator final : public Validator +{ +public: + explicit CrashDumpValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CRASHDUMP; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class TimeCorrectionValidator final : public Validator +{ +public: + explicit TimeCorrectionValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::TIMECORRECTION; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +} // namespace SystemHealth::Logging + +#endif diff --git a/daemon/systemhealth/NewsServerValidator.cpp b/daemon/systemhealth/NewsServerValidator.cpp new file mode 100644 index 00000000..22ca2f4e --- /dev/null +++ b/daemon/systemhealth/NewsServerValidator.cpp @@ -0,0 +1,258 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "NewsServerValidator.h" +#include "Options.h" + +namespace SystemHealth::NewsServer +{ +NewsServerValidator::NewsServerValidator(const ::NewsServer& server) + : m_server(server), m_name("Server" + std::to_string(server.GetId())) +{ + m_validators.reserve(16); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); + m_validators.push_back(std::make_unique(server)); +} + +Status ServerActiveValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Warning("Server is disabled"); + + return Status::Ok(); +} + +Status ServerNameValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + std::string_view name = m_server.GetName(); + if (name.empty()) + return Status::Info("Name is recommended for clearer logs and troubleshooting"); + + return Status::Ok(); +} + +Status ServerLevelValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + const auto level = m_server.GetLevel(); + if (level < 0 || level > 99) return Status::Error("Level must be between 0 and 99"); + + return Status::Ok(); +} + +Status ServerOptionalValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + if (m_server.GetOptional() && m_server.GetLevel() == 0) + { + return Status::Warning( + "Server is marked as optional but assigned to level 0; this may affect primary " + "server selection"); + } + return Status::Ok(); +} + +Status ServerGroupValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + const auto group = m_server.GetGroup(); + if (group < 0 || group > 99) return Status::Error("Group must be between 0 and 99"); + + return Status::Ok(); +} + +Status ServerHostValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + return RequiredOption(GetName(), m_server.GetHost()) + .And([&]() { return Network::ValidHostname(m_server.GetHost()); }); +} + +Status ServerPortValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + return Network::ValidPort(m_server.GetPort()) + .And( + [&]() + { + bool tls = m_server.GetTls(); + int port = m_server.GetPort(); + + if (tls && (port != 443 && port != 563)) + return Status::Info("Consider using a standard encrypted port (443 or 563)"); + + if (!tls && (port != 80 && port != 119)) + return Status::Info("Consider using a standard unencrypted port (80 or 119)"); + + return Status::Ok(); + }); +} + +Status ServerUsernameValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + std::string_view username = m_server.GetUser(); + if (username.empty()) + { + return Status::Warning("Username is set to empty"); + } + return Status::Ok(); +} + +Status ServerPasswordValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + return CheckPassword(m_server.GetPassword()); +} + +Status ServerEncryptionValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + if (!m_server.GetTls()) + { + return Status::Warning( + "TLS is disabled. " + "Communication with this server will not be encrypted"); + } + + return Status::Ok(); +} + +Status ServerJoinGroupValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + int join = m_server.GetJoinGroup(); + if (join < 0 || join > 99) + { + return Status::Error("JoinGroup must be between 0 and 99"); + } + return Status::Ok(); +} + +Status ServerCipherValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + std::string_view cipher = m_server.GetCipher(); + if (cipher.empty()) return Status::Ok(); + + bool tls = m_server.GetTls(); + if (!tls) return Status::Warning("Cipher specified but TLS is disabled"); + + return Status::Info("Using custom cipher suite"); +} + +Status ServerConnectionsValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + int maxConnections = m_server.GetMaxConnections(); + if (maxConnections == 0) + return Status::Warning("'Connections' is set to '0'. The Server is disabled"); + + if (maxConnections < 0 || maxConnections > 999) + return Status::Error("'Connections' value is invalid. It must be between 0 and 999"); + + if (maxConnections < 8) + return Status::Warning("A low number of connections may impact download performance"); + + return Status::Ok(); +} + +Status ServerRetentionValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + int retention = m_server.GetRetention(); + if (retention > 0 && retention < 100) + { + std::stringstream ss; + ss << "Retention is set to " << retention << " days. " + << "This low value may cause downloads to fail"; + return Status::Warning(ss.str()); + } + + if (retention > 10000) // ~27 years + { + std::stringstream ss; + ss << "Retention (" << retention << ") is unrealistically high " + << "and will be limited by the server's actual retention"; + return Status::Info(ss.str()); + } + + return Status::Ok(); +} + +Status ServerCertVerificationValidator::Validate() const +{ + if (!m_server.GetActive()) return Status::Ok(); + + bool tls = m_server.GetTls(); + auto certLevel = m_server.GetCertVerificationLevel(); + if (!tls) return Status::Ok(); + + if (certLevel == Options::ECertVerifLevel::cvNone) + return Status::Warning( + "Certificate verification is 'None', " + "making the TLS connection insecure"); + + return Status::Ok(); +} + +Status ServerIpVersionValidator::Validate() const +{ + // Auto: 0, + // V4: 4, + // V6: 6 + int ipVersion = m_server.GetIpVersion(); + if (ipVersion == 0 || ipVersion == 4) + return Status::Ok(); + + if (ipVersion == 6) + return Status::Info("Using IPv6 - ensure your network supports IPv6"); + + return Status::Error("Invalid value. Available options are: Auto, IpV4, IpV6"); +} + +} // namespace SystemHealth::NewsServer diff --git a/daemon/systemhealth/NewsServerValidator.h b/daemon/systemhealth/NewsServerValidator.h new file mode 100644 index 00000000..8ca85c36 --- /dev/null +++ b/daemon/systemhealth/NewsServerValidator.h @@ -0,0 +1,233 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NEWS_SERVER_VALIDATOR_H +#define NEWS_SERVER_VALIDATOR_H + +#include "SectionValidator.h" +#include "NewsServer.h" + +namespace SystemHealth::NewsServer +{ +class NewsServerValidator final : public SectionValidator +{ +public: + explicit NewsServerValidator(const ::NewsServer& server); + std::string_view GetName() const override { return m_name; } + +private: + const ::NewsServer& m_server; + const std::string m_name; +}; + +class ServerActiveValidator final : public Validator +{ +public: + explicit ServerActiveValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Active"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerNameValidator final : public Validator +{ +public: + explicit ServerNameValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Name"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerLevelValidator final : public Validator +{ +public: + explicit ServerLevelValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Level"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerOptionalValidator final : public Validator +{ +public: + explicit ServerOptionalValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Optional"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerGroupValidator final : public Validator +{ +public: + explicit ServerGroupValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Group"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerHostValidator final : public Validator +{ +public: + explicit ServerHostValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Host"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerPortValidator final : public Validator +{ +public: + explicit ServerPortValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Port"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerUsernameValidator final : public Validator +{ +public: + explicit ServerUsernameValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Username"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerPasswordValidator final : public Validator +{ +public: + explicit ServerPasswordValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Password"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerEncryptionValidator final : public Validator +{ +public: + explicit ServerEncryptionValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Encryption"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerJoinGroupValidator final : public Validator +{ +public: + explicit ServerJoinGroupValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "JoinGroup"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerCipherValidator final : public Validator +{ +public: + explicit ServerCipherValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Cipher"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerConnectionsValidator final : public Validator +{ +public: + explicit ServerConnectionsValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Connections"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerRetentionValidator final : public Validator +{ +public: + explicit ServerRetentionValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "Retention"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerCertVerificationValidator final : public Validator +{ +public: + explicit ServerCertVerificationValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "CertVerification"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +class ServerIpVersionValidator final : public Validator +{ +public: + explicit ServerIpVersionValidator(const ::NewsServer& server) : m_server(server) {} + + std::string_view GetName() const override { return "IpVersion"; } + Status Validate() const override; + +private: + const ::NewsServer& m_server; +}; + +} // namespace SystemHealth::NewsServer + +#endif diff --git a/daemon/systemhealth/NewsServersValidator.cpp b/daemon/systemhealth/NewsServersValidator.cpp new file mode 100644 index 00000000..da21be74 --- /dev/null +++ b/daemon/systemhealth/NewsServersValidator.cpp @@ -0,0 +1,77 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "NewsServersValidator.h" +#include "Validators.h" + +namespace SystemHealth::NewsServers +{ + +NewsServersValidator::NewsServersValidator(const Servers& servers) + : SectionGroupValidator(MakeServerValidators(servers)), m_servers(servers) +{ + m_validators.reserve(3); + m_validators.push_back(std::make_unique(servers)); + m_validators.push_back(std::make_unique(servers)); + m_validators.push_back(std::make_unique(servers)); +} + +std::vector> NewsServersValidator::MakeServerValidators( + const Servers& servers) const +{ + std::vector> validators; + validators.reserve(servers.size()); + for (const auto& server : servers) + { + validators.push_back(std::make_unique(*server)); + } + return validators; +} + +Status ServersConfiguredValidator::Validate() const +{ + if (m_servers.empty() || + (m_servers.size() == 1 && m_servers.front()->GetHost() == DEFAULT_SERVER_HOST)) + return Status::Error("No news servers are configured"); + + return Status::Ok(); +} + +Status AnyServerActiveValidator::Validate() const +{ + auto anyActive = + std::any_of(m_servers.cbegin(), m_servers.cend(), [](const auto& server) + { return server->GetActive() && server->GetMaxConnections() > 0; }); + if (!anyActive) return Status::Error("At least one news server must be active"); + + return Status::Ok(); +} + +Status AnyPrimaryServerExistsValidator::Validate() const +{ + auto anyLevelZero = std::any_of(m_servers.cbegin(), m_servers.cend(), + [](const auto& server) { return server->GetLevel() == 0; }); + if (!anyLevelZero) return Status::Error("No servers are configured for level 0"); + + return Status::Ok(); +} + +} // namespace SystemHealth::NewsServers diff --git a/daemon/systemhealth/NewsServersValidator.h b/daemon/systemhealth/NewsServersValidator.h new file mode 100644 index 00000000..29deb575 --- /dev/null +++ b/daemon/systemhealth/NewsServersValidator.h @@ -0,0 +1,84 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NEWS_SERVERS_VALIDATOR_H +#define NEWS_SERVERS_VALIDATOR_H + +#include "Options.h" +#include "NewsServerValidator.h" +#include "Connection.h" +#include "SectionGroupValidator.h" +#include "SectionValidator.h" +#include "Validators.h" + +namespace SystemHealth::NewsServers +{ + +class NewsServersValidator final : public SectionGroupValidator +{ +public: + explicit NewsServersValidator(const Servers& servers); + std::string_view GetName() const override { return "NewsServers"; } + +private: + const Servers& m_servers; + std::vector> MakeServerValidators( + const Servers& servers) const; +}; + +class ServersConfiguredValidator final : public Validator +{ +public: + explicit ServersConfiguredValidator(const Servers& servers) : m_servers(servers) {} + + std::string_view GetName() const override { return ""; } + Status Validate() const override; + +private: + const Servers& m_servers; + static constexpr std::string_view DEFAULT_SERVER_HOST = "my.newsserver.com"; +}; + +class AnyServerActiveValidator final : public Validator +{ +public: + explicit AnyServerActiveValidator(const Servers& servers) : m_servers(servers) {} + + std::string_view GetName() const override { return ""; } + Status Validate() const override; + +private: + const Servers& m_servers; +}; + +class AnyPrimaryServerExistsValidator final : public Validator +{ +public: + explicit AnyPrimaryServerExistsValidator(const Servers& servers) : m_servers(servers) {} + + std::string_view GetName() const override { return ""; } + Status Validate() const override; + +private: + const Servers& m_servers; +}; + +} // namespace SystemHealth::NewsServers + +#endif diff --git a/daemon/systemhealth/PathsValidator.cpp b/daemon/systemhealth/PathsValidator.cpp new file mode 100644 index 00000000..ce25e602 --- /dev/null +++ b/daemon/systemhealth/PathsValidator.cpp @@ -0,0 +1,327 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Options.h" +#include "Status.h" +#include "Validators.h" +#include "PathsValidator.h" + +namespace SystemHealth::Paths +{ +PathsValidator::PathsValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(13); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +#ifndef _WIN32 + m_validators.push_back(std::make_unique(options)); +#endif +} + +Status MainDirValidator::Validate() const { return Validate(m_options.GetMainDirPath()); } + +Status MainDirValidator::Validate(const boost::filesystem::path& path) +{ + return RequiredPathOption(Options::MAINDIR, path) + .And(&Directory::Exists, path) + .And(&Directory::Writable, path); +} + +Status DestDirValidator::Validate() const +{ + return Validate(m_options.GetDestDirPath()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetDestDirPath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}}); + }); +} + +Status DestDirValidator::Validate(const boost::filesystem::path& path) +{ + return RequiredPathOption(Options::DESTDIR, path) + .And(&Directory::Exists, path) + .And(&Directory::Writable, path); +} + +Status InterDirValidator::Validate() const +{ + return Validate(m_options.GetInterDirPath()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetInterDirPath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::DESTDIR, m_options.GetDestDirPath()}}); + }); +} + +Status InterDirValidator::Validate(const boost::filesystem::path& path) +{ + if (path.empty()) + return Status::Warning( + "'" + std::string(Options::INTERDIR) + + "' is set to empty which is not recommended for optimal unpack performance"); + + return Directory::Exists(path).And(&Directory::Writable, path); +} + +Status NzbDirValidator::Validate() const +{ + return Validate(m_options.GetNzbDirPath()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetNzbDirPath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::DESTDIR, m_options.GetDestDirPath()}, + {Options::INTERDIR, m_options.GetInterDirPath()}}); + }); +} + +Status NzbDirValidator::Validate(const boost::filesystem::path& path) +{ + return RequiredPathOption(Options::NZBDIR, path) + .And(&Directory::Exists, path) + .And(&Directory::Writable, path); +} + +Status QueueDirValidator::Validate() const +{ + return Validate(m_options.GetQueueDirPath()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetQueueDirPath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::DESTDIR, m_options.GetDestDirPath()}, + {Options::INTERDIR, m_options.GetInterDirPath()}, + {Options::NZBDIR, m_options.GetNzbDirPath()}}); + }); +} + +Status QueueDirValidator::Validate(const boost::filesystem::path& path) +{ + return RequiredPathOption(Options::QUEUEDIR, path) + .And(&Directory::Exists, path) + .And(&Directory::Writable, path); +} + +Status WebDirValidator::Validate() const +{ + const auto& path = m_options.GetWebDirPath(); + return Validate(path).And( + [&]() + { + if (path.empty()) return Status::Ok(); + return UniquePath(GetName(), m_options.GetWebDirPath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::DESTDIR, m_options.GetDestDirPath()}, + {Options::INTERDIR, m_options.GetInterDirPath()}, + {Options::NZBDIR, m_options.GetNzbDirPath()}, + {Options::QUEUEDIR, m_options.GetQueueDirPath()}}); + }); +} + +Status WebDirValidator::Validate(const boost::filesystem::path& path) +{ + if (path.empty()) return Status::Ok(); + return Directory::Exists(path).And(&Directory::Readable, path); +} + +Status TempDirValidator::Validate() const +{ + return Validate(m_options.GetTempDirPath()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetTempDirPath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::DESTDIR, m_options.GetDestDirPath()}, + {Options::INTERDIR, m_options.GetInterDirPath()}, + {Options::NZBDIR, m_options.GetNzbDirPath()}, + {Options::QUEUEDIR, m_options.GetQueueDirPath()}, + {Options::WEBDIR, m_options.GetWebDirPath()}}); + }); +} + +Status TempDirValidator::Validate(const boost::filesystem::path& path) +{ + return RequiredPathOption(Options::TEMPDIR, path) + .And(&Directory::Exists, path) + .And(&Directory::Writable, path); +} + +Status ScriptDirValidator::Validate() const +{ + const auto& paths = m_options.GetScriptDirPaths(); + if (paths.empty()) + { + return Status::Error("'" + std::string(Options::SCRIPTDIR) + + "' is required and cannot be empty"); + } + + for (const auto& dir : paths) + { + auto status = Validate(dir).And( + [&]() + { + return UniquePath(GetName(), dir, + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::DESTDIR, m_options.GetDestDirPath()}, + {Options::INTERDIR, m_options.GetInterDirPath()}, + {Options::NZBDIR, m_options.GetNzbDirPath()}, + {Options::QUEUEDIR, m_options.GetQueueDirPath()}, + {Options::WEBDIR, m_options.GetWebDirPath()}, + {Options::TEMPDIR, m_options.GetTempDirPath()}}); + }); + if (!status.IsOk()) + { + return status; + } + } + + return Status::Ok(); +} + +Status ScriptDirValidator::Validate(const boost::filesystem::path& path) +{ + return RequiredPathOption(Options::SCRIPTDIR, path) + .And(&Directory::Exists, path) + .And(&Directory::Writable, path); +} + +Status ConfigTemplateValidator::Validate() const { return Status::Ok(); } + +Status LogFileValidator::Validate() const +{ + return Validate(m_options.GetLogFilePath(), m_options.GetWriteLog()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetLogFilePath(), + {{Options::CONFIGTEMPLATE, m_options.GetConfigTemplatePath()}, + {Options::CONFIGFILE, m_options.GetConfigFilePath()}}); + }); +} + +Status LogFileValidator::Validate(const boost::filesystem::path& path, Options::EWriteLog writeLog) +{ + if (writeLog == Options::EWriteLog::wlNone) return Status::Ok(); + + if (path.empty()) + { + return Status::Error("Logging is enabled, but '" + std::string(Options::LOGFILE) + + "' is set to empty"); + } + + if (writeLog == Options::EWriteLog::wlRotate && path.has_parent_path()) + { + const auto&& parent = path.parent_path(); + return Directory::Exists(parent).And(&Directory::Writable, parent); + } + + return File::Exists(path).And(&File::Writable, path); +} + +Status CertStoreValidator::Validate() const +{ + return Validate(m_options.GetCertStore(), m_options.GetCertCheck()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetCertStorePath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::DESTDIR, m_options.GetDestDirPath()}, + {Options::INTERDIR, m_options.GetInterDirPath()}, + {Options::NZBDIR, m_options.GetNzbDirPath()}, + {Options::QUEUEDIR, m_options.GetQueueDirPath()}, + {Options::WEBDIR, m_options.GetWebDirPath()}, + {Options::TEMPDIR, m_options.GetTempDirPath()}, + {Options::CONFIGTEMPLATE, m_options.GetConfigTemplatePath()}, + {Options::LOGFILE, m_options.GetLogFilePath()}, + {Options::CONFIGFILE, m_options.GetConfigFilePath()}}); + }); +} + +Status CertStoreValidator::Validate(const boost::filesystem::path& path, bool certCheck) +{ + if (path.empty() && !certCheck) return Status::Ok(); + + if (path.empty() && certCheck) + return Status::Error("'" + std::string(Options::CERTCHECK) + + "' requires proper configuration of option '" + + std::string(Options::CERTSTORE) + "'"); + + const auto file = File::Exists(path); + if (!file.IsError()) return File::Readable(path); + + const auto dir = Directory::Exists(path); + if (!dir.IsError()) return Directory::Readable(path); + + return Status::Error("'" + std::string(Options::CERTSTORE) + "' must be a file or a directory"); +} + +Status RequiredDirValidator::Validate() const { return Status::Ok(); } + +#ifndef _WIN32 +Status LockFileValidator::Validate() const +{ + return Validate(m_options.GetLockFilePath(), m_options.GetDaemonMode()) + .And( + [&]() + { + return UniquePath(GetName(), m_options.GetLockFilePath(), + {{Options::MAINDIR, m_options.GetMainDirPath()}, + {Options::CONFIGTEMPLATE, m_options.GetConfigTemplatePath()}, + {Options::LOGFILE, m_options.GetLogFilePath()}, + {Options::CERTSTORE, m_options.GetCertStorePath()}, + {Options::CONFIGFILE, m_options.GetConfigFilePath()}}); + }); +} + +Status LockFileValidator::Validate(const boost::filesystem::path& path, bool daemonMode) +{ + if (path.empty() && daemonMode) + { + return Status::Warning( + "'" + std::string(Options::LOCKFILE) + + "' is set to empty. The check for another running instance is disabled"); + } + + if (daemonMode) + { + return File::Exists(path); + } + + return Status::Ok(); +} +#endif +} // namespace SystemHealth::Paths diff --git a/daemon/systemhealth/PathsValidator.h b/daemon/systemhealth/PathsValidator.h new file mode 100644 index 00000000..16bd7af4 --- /dev/null +++ b/daemon/systemhealth/PathsValidator.h @@ -0,0 +1,208 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PATHS_VALIDATOR_H +#define PATHS_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::Paths +{ + +class PathsValidator final : public SectionValidator +{ +public: + explicit PathsValidator(const Options& options); + std::string_view GetName() const override { return "Paths"; } + +private: + const Options& m_options; +}; + +class MainDirValidator final : public Validator +{ +public: + explicit MainDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::MAINDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class DestDirValidator final : public Validator +{ +public: + explicit DestDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::DESTDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class InterDirValidator final : public Validator +{ +public: + explicit InterDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::INTERDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class NzbDirValidator final : public Validator +{ +public: + explicit NzbDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::NZBDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class QueueDirValidator final : public Validator +{ +public: + explicit QueueDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::QUEUEDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class WebDirValidator final : public Validator +{ +public: + explicit WebDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::WEBDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class TempDirValidator final : public Validator +{ +public: + explicit TempDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::TEMPDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class ScriptDirValidator final : public Validator +{ +public: + explicit ScriptDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::SCRIPTDIR; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path); + +private: + const Options& m_options; +}; + +class ConfigTemplateValidator final : public Validator +{ +public: + explicit ConfigTemplateValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::CONFIGTEMPLATE; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class LogFileValidator final : public Validator +{ +public: + explicit LogFileValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::LOGFILE; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path, Options::EWriteLog writeLog); + +private: + const Options& m_options; +}; + +class CertStoreValidator final : public Validator +{ +public: + explicit CertStoreValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::CERTSTORE; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path, bool certCheck); + +private: + const Options& m_options; +}; + +class RequiredDirValidator final : public Validator +{ +public: + explicit RequiredDirValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::REQUIREDDIR; } + Status Validate() const override; + +private: + const Options& m_options; +}; +#ifndef _WIN32 +class LockFileValidator final : public Validator +{ +public: + explicit LockFileValidator(const Options& options) : m_options(options) {} + + std::string_view GetName() const override { return Options::LOCKFILE; } + Status Validate() const override; + static Status Validate(const boost::filesystem::path& path, bool daemonMode); + +private: + const Options& m_options; +}; +#endif +} // namespace SystemHealth::Paths + +#endif diff --git a/daemon/systemhealth/SchedulerTaskValidator.cpp b/daemon/systemhealth/SchedulerTaskValidator.cpp new file mode 100644 index 00000000..5121c995 --- /dev/null +++ b/daemon/systemhealth/SchedulerTaskValidator.cpp @@ -0,0 +1,72 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "SchedulerTaskValidator.h" + +namespace SystemHealth::Scheduler +{ +SchedulerTaskValidator::SchedulerTaskValidator(const ::Scheduler::Task& task) + : m_task(task), m_name("Task" + std::to_string(m_task.GetId())) +{ + m_validators.reserve(4); + m_validators.push_back(std::make_unique(task)); + m_validators.push_back(std::make_unique(task)); + m_validators.push_back(std::make_unique(task)); + m_validators.push_back(std::make_unique(task)); +} + +Status TimeValidator::Validate() const +{ + int h = m_task.GetHours(); + int m = m_task.GetMinutes(); + + // Assumption: -1 might represent a wildcard '*' in your internal logic + // If not, remove the (< -1) check. + if (h < -1 || h > 23) + { + return Status::Error("Invalid hour: " + std::to_string(h)); + } + + if (m < 0 || m > 59) + { + return Status::Error("Invalid minute: " + std::to_string(m)); + } + + return Status::Ok(); +} + +Status WeekDaysValidator::Validate() const +{ + int bits = m_task.GetWeeDaysBits(); + + if (bits < 0 || bits > 127) // Bits 1-7 (1<<0 to 1<<6) = max 127 + { + return Status::Error("Invalid weekdays configuration"); + } + + return Status::Ok(); +} + +Status ParamValidator::Validate() const { return Status::Ok(); } + +Status CommandValidator::Validate() const { return Status::Ok(); } + +} // namespace SystemHealth::Scheduler diff --git a/daemon/systemhealth/SchedulerTaskValidator.h b/daemon/systemhealth/SchedulerTaskValidator.h new file mode 100644 index 00000000..78481b1d --- /dev/null +++ b/daemon/systemhealth/SchedulerTaskValidator.h @@ -0,0 +1,86 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SCHEDULER_TASK_VALIDATOR_H +#define SCHEDULER_TASK_VALIDATOR_H + +#include "SectionValidator.h" +#include "Scheduler.h" +#include "Validators.h" + +namespace SystemHealth::Scheduler +{ +class SchedulerTaskValidator final : public SectionValidator +{ +public: + explicit SchedulerTaskValidator(const ::Scheduler::Task& task); + std::string_view GetName() const override { return m_name; } + +private: + const ::Scheduler::Task& m_task; + const std::string m_name; +}; + +class TimeValidator final : public Validator +{ +public: + explicit TimeValidator(const ::Scheduler::Task& task) : m_task(task) {} + std::string_view GetName() const override { return "Time"; } + Status Validate() const override; + +private: + const ::Scheduler::Task& m_task; +}; + +class WeekDaysValidator final : public Validator +{ +public: + explicit WeekDaysValidator(const ::Scheduler::Task& task) : m_task(task) {} + std::string_view GetName() const override { return "WeekDays"; } + Status Validate() const override; + +private: + const ::Scheduler::Task& m_task; +}; + +class ParamValidator final : public Validator +{ +public: + explicit ParamValidator(const ::Scheduler::Task& task) : m_task(task) {} + std::string_view GetName() const override { return "Param"; } + Status Validate() const override; + +private: + const ::Scheduler::Task& m_task; +}; + +class CommandValidator final : public Validator +{ +public: + explicit CommandValidator(const ::Scheduler::Task& task) : m_task(task) {} + std::string_view GetName() const override { return "Command"; } + Status Validate() const override; + +private: + const ::Scheduler::Task& m_task; +}; + +} // namespace SystemHealth::SchedulerTask + +#endif diff --git a/daemon/systemhealth/SchedulerTasksValidator.cpp b/daemon/systemhealth/SchedulerTasksValidator.cpp new file mode 100644 index 00000000..a004af4e --- /dev/null +++ b/daemon/systemhealth/SchedulerTasksValidator.cpp @@ -0,0 +1,29 @@ +/* + * This file is part of nzbget. See . + */ + +#include "nzbget.h" + +#include "SchedulerTasksValidator.h" +#include "SchedulerTaskValidator.h" + +namespace SystemHealth::Scheduler +{ +SchedulerTasksValidator::SchedulerTasksValidator(const ::Scheduler::TaskList& tasks) + : SectionGroupValidator(MakeTaskValidators(tasks)), m_tasks(tasks) +{ +} + +std::vector> SchedulerTasksValidator::MakeTaskValidators( + const ::Scheduler::TaskList& tasks) const +{ + std::vector> validators; + validators.reserve(tasks.size()); + for (const auto& task : tasks) + { + validators.push_back(std::make_unique(*task)); + } + return validators; +} + +} // namespace SystemHealth::SchedulerTask diff --git a/daemon/systemhealth/SchedulerTasksValidator.h b/daemon/systemhealth/SchedulerTasksValidator.h new file mode 100644 index 00000000..2136fc67 --- /dev/null +++ b/daemon/systemhealth/SchedulerTasksValidator.h @@ -0,0 +1,29 @@ +/* + * This file is part of nzbget. See . + */ + +#ifndef SCHEDULER_TASKS_VALIDATOR_H +#define SCHEDULER_TASKS_VALIDATOR_H + +#include "SectionGroupValidator.h" +#include "SectionValidator.h" +#include "Scheduler.h" + +namespace SystemHealth::Scheduler +{ +class SchedulerTasksValidator final : public SectionGroupValidator +{ +public: + explicit SchedulerTasksValidator(const ::Scheduler::TaskList& tasks); + std::string_view GetName() const override { return "Scheduler"; } + +private: + const ::Scheduler::TaskList& m_tasks; + + std::vector> MakeTaskValidators( + const ::Scheduler::TaskList& tasks) const; +}; + +} // namespace SystemHealth::SchedulerTask + +#endif diff --git a/daemon/systemhealth/SectionGroupValidator.cpp b/daemon/systemhealth/SectionGroupValidator.cpp new file mode 100644 index 00000000..9db46e20 --- /dev/null +++ b/daemon/systemhealth/SectionGroupValidator.cpp @@ -0,0 +1,55 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "SectionGroupValidator.h" + +namespace SystemHealth +{ +SectionGroupValidator::SectionGroupValidator( + std::vector> sections) + : m_sections(std::move(sections)) +{ +} + +SectionReport SectionGroupValidator::Validate() const +{ + SectionReport report; + report.name = GetName(); + report.issues.reserve(m_validators.size()); + + for (const auto& section : m_sections) + { + auto sectionReport = section->Validate(); + report.subsections.push_back( + {std::move(sectionReport.name), std::move(sectionReport.options)}); + } + + for (const auto& validator : m_validators) + { + Status status = validator->Validate(); + if (!status.IsOk()) + report.issues.push_back(std::move(status)); + } + + return report; +} + +} // namespace SystemHealth diff --git a/daemon/systemhealth/SectionGroupValidator.h b/daemon/systemhealth/SectionGroupValidator.h new file mode 100644 index 00000000..00d7ac00 --- /dev/null +++ b/daemon/systemhealth/SectionGroupValidator.h @@ -0,0 +1,40 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SECTION_GROUP_VALIDATOR_H +#define SECTION_GROUP_VALIDATOR_H + +#include "SectionValidator.h" + +namespace SystemHealth +{ + +class SectionGroupValidator : public SectionValidator +{ +public: + explicit SectionGroupValidator(std::vector> sections); + SectionReport Validate() const override; + +protected: + std::vector> m_sections; +}; + +} // namespace SystemHealth + +#endif diff --git a/daemon/systemhealth/SectionValidator.cpp b/daemon/systemhealth/SectionValidator.cpp new file mode 100644 index 00000000..9e0445e8 --- /dev/null +++ b/daemon/systemhealth/SectionValidator.cpp @@ -0,0 +1,180 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "SectionValidator.h" +#include "Json.h" +#include "Xml.h" + +namespace SystemHealth +{ + +SectionValidator::~SectionValidator() = default; + +SectionReport SectionValidator::Validate() const +{ + SectionReport report; + report.name = GetName(); + report.options.reserve(m_validators.size()); + + for (const auto& validator : m_validators) + { + auto status = validator->Validate(); + if (!status.IsOk()) + report.options.push_back({std::string(validator->GetName()), std::move(status)}); + } + + return report; +} + +Json::JsonObject ToJson(const OptionStatus& status) +{ + Json::JsonObject json; + json["Name"] = status.name; + json["Severity"] = SeverityToStr(status.status.GetSeverity()); + json["Message"] = status.status.GetMessage(); + return json; +} + +Json::JsonObject ToJson(const SubsectionReport& report) +{ + Json::JsonObject json; + Json::JsonArray optionsArrayJson; + + for (const auto& option : report.options) + { + optionsArrayJson.push_back(ToJson(option)); + } + + json["Name"] = report.name; + json["Options"] = std::move(optionsArrayJson); + + return json; +} + +Json::JsonObject ToJson(const SectionReport& report) +{ + Json::JsonObject json; + Json::JsonObject reportJson; + Json::JsonArray issuesArrayJson; + Json::JsonArray optionsArrayJson; + Json::JsonArray sectionsArrayJson; + + for (const auto& issue : report.issues) + { + issuesArrayJson.push_back(ToJson(issue)); + } + + for (const auto& option : report.options) + { + optionsArrayJson.push_back(ToJson(option)); + } + + for (const auto& section : report.subsections) + { + sectionsArrayJson.push_back(ToJson(section)); + } + + json["Name"] = report.name; + json["Issues"] = std::move(issuesArrayJson); + json["Options"] = std::move(optionsArrayJson); + json["Subsections"] = std::move(sectionsArrayJson); + + return json; +} + +// +Xml::XmlNodePtr ToXml(const OptionStatus& status) +{ + xmlNodePtr structNode = Xml::CreateStructNode(); + + const auto severity = SeverityToStr(status.status.GetSeverity()); + Xml::AddNewNode(structNode, "Name", "string", status.name.c_str()); + Xml::AddNewNode(structNode, "Severity", "string", severity.data()); + Xml::AddNewNode(structNode, "Message", "string", status.status.GetMessage().c_str()); + + return structNode->parent; +} + +// +// ... +// ... +// +Xml::XmlNodePtr ToXml(const SubsectionReport& report) +{ + xmlNodePtr structNode = Xml::CreateStructNode(); + + Xml::AddNewNode(structNode, "Name", "string", report.name.c_str()); + + std::vector optionsNodes; + for (const auto& option : report.options) + { + optionsNodes.push_back(ToXml(option)); + } + Xml::AddArrayNode(structNode, "Options", optionsNodes); + + return structNode->parent; +} + +//
+// ... +// ... +// ... +// ... +//
+Xml::XmlNodePtr ToXml(const SectionReport& report) +{ + xmlNodePtr structNode = Xml::CreateStructNode(); + + Xml::AddNewNode(structNode, "Name", "string", report.name.c_str()); + + std::vector issueNodes; + for (const auto& issue : report.issues) + { + xmlNodePtr sNode = Xml::CreateStructNode(); + const auto severity = SeverityToStr(issue.GetSeverity()); + Xml::AddNewNode(sNode, "Severity", "string", severity.data()); + Xml::AddNewNode(sNode, "Message", "string", issue.GetMessage().c_str()); + issueNodes.push_back(sNode->parent); + } + Xml::AddArrayNode(structNode, "Issues", issueNodes); + + std::vector optionNodes; + for (const auto& option : report.options) + { + optionNodes.push_back(ToXml(option)); + } + Xml::AddArrayNode(structNode, "Options", optionNodes); + + std::vector subNodes; + for (const auto& sub : report.subsections) + { + subNodes.push_back(ToXml(sub)); + } + Xml::AddArrayNode(structNode, "Subsections", subNodes); + + return structNode->parent; +} + +} // namespace SystemHealth diff --git a/daemon/systemhealth/SectionValidator.h b/daemon/systemhealth/SectionValidator.h new file mode 100644 index 00000000..619e6879 --- /dev/null +++ b/daemon/systemhealth/SectionValidator.h @@ -0,0 +1,73 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SECTION_VALIDATOR_H +#define SECTION_VALIDATOR_H + +#include +#include +#include +#include +#include "Validators.h" +#include "Status.h" + +namespace SystemHealth +{ +struct OptionStatus +{ + std::string name; + Status status; +}; + +struct SubsectionReport +{ + std::string name; + std::vector options; +}; + +struct SectionReport +{ + std::string name; + std::vector issues; + std::vector options; + std::vector subsections; +}; + +class SectionValidator +{ +public: + virtual ~SectionValidator(); + virtual std::string_view GetName() const = 0; + virtual SectionReport Validate() const; + +protected: + std::vector> m_validators; +}; + +Json::JsonObject ToJson(const OptionStatus& status); +Json::JsonObject ToJson(const SubsectionReport& report); +Json::JsonObject ToJson(const SectionReport& report); + +Xml::XmlNodePtr ToXml(const OptionStatus& status); +Xml::XmlNodePtr ToXml(const SubsectionReport& report); +Xml::XmlNodePtr ToXml(const SectionReport& report); + +} // namespace SystemHealth + +#endif diff --git a/daemon/systemhealth/SecurityValidator.cpp b/daemon/systemhealth/SecurityValidator.cpp new file mode 100644 index 00000000..7c707304 --- /dev/null +++ b/daemon/systemhealth/SecurityValidator.cpp @@ -0,0 +1,324 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "Options.h" +#include "Status.h" +#include "SecurityValidator.h" + +namespace SystemHealth::Security +{ + +SecurityValidator::SecurityValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(16); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + +#ifndef _WIN32 + if (options.GetDaemonMode()) + { + m_validators.push_back(std::make_unique(options)); + } + m_validators.push_back(std::make_unique(options)); +#endif +} + +Status ControlIpValidator::Validate() const +{ + Status s = RequiredOption(Options::CONTROLIP, m_options.GetControlIp()); + if (!s.IsOk()) return s; + + std::string_view ip = m_options.GetControlIp(); + if (ip == "0.0.0.0") + { + return Status::Info("'" + std::string(Options::CONTROLIP) + + "' is '0.0.0.0', allowing connections from any IP. Client mode will " + "default to 127.0.0.1 to connect"); + } + + if (ip == "127.0.0.1") return Status::Ok(); + if (!ip.empty() && (ip[0] == '/' || ip[0] == '.')) + + { +#ifdef _WIN32 + return Status::Error("Using Unix domain sockets is not supported on Windows"); +#else + + return Status::Info("'" + std::string(Options::CONTROLIP) + + "' is set to a path, activating Unix domain socket mode"); +#endif + } + + return Status::Ok(); +} + +Status SecureKeyValidator::Validate() const +{ + if (!m_options.GetSecureControl()) return Status::Ok(); + + std::string_view keyFile = m_options.GetSecureKey(); + if (m_options.GetSecureControl() && keyFile.empty()) + { + return Status::Warning("'" + std::string(Options::SECURECONTROL) + "' is enabled but '" + + std::string(Options::SECUREKEY) + "' is empty"); + } + + Status s = File::Exists(m_options.GetSecureKey()); + + if (!s.IsOk()) return s; + + return File::Readable(m_options.GetSecureKey()); +} + +Status SecureCertValidator::Validate() const +{ + if (!m_options.GetSecureControl()) return Status::Ok(); + + std::string_view certFile = m_options.GetSecureCert(); + if (m_options.GetSecureControl() && certFile.empty()) + { + return Status::Warning("'" + std::string(Options::SECURECONTROL) + "' is enabled but '" + + std::string(Options::SECURECERT) + "' is empty"); + } + + if (!m_options.GetSecureControl()) return Status::Ok(); + + Status s = File::Exists(m_options.GetSecureCert()); + + if (!s.IsOk()) return s; + + return File::Readable(m_options.GetSecureCert()); +} + +Status ControlPortValidator::Validate() const +{ + Status s = Network::ValidPort(m_options.GetControlPort()); + if (!s.IsOk()) return s; + + int port = m_options.GetControlPort(); + if (port < 1024) + { + return Status::Warning("'" + std::string(Options::CONTROLPORT) + + "' is below 1024 and may require root privileges"); + } + return Status::Ok(); +} + +Status ControlUsernameValidator::Validate() const +{ + Status s = RequiredOption(Options::CONTROLUSERNAME, m_options.GetControlUsername()); + if (!s.IsOk()) return s; + + std::string_view username = m_options.GetControlUsername(); + if (username == "nzbget") + return Status::Info("Using default username 'nzbget' is not recommended for security"); + + return Status::Ok(); +} + +Status ControlPasswordValidator::Validate() const +{ + Status s = CheckPassword(m_options.GetControlPassword()); + if (!s.IsOk()) return s; + + std::string_view password = m_options.GetControlPassword(); + if (password == "tegbzn6789") + return Status::Info("Using default password is not recommended for security"); + + return Status::Ok(); +} + +Status AddUsernameValidator::Validate() const +{ + std::string_view addUser = m_options.GetAddUsername(); + std::string_view controlUser = m_options.GetControlUsername(); + if (!addUser.empty() && controlUser == "nzbget") + { + return Status::Warning("'" + std::string(Options::ADDUSERNAME) + "' is enabled while '" + + std::string(Options::CONTROLUSERNAME) + + "' is still set to the default 'nzbget'"); + } + + return Status::Ok(); +} + +Status AddPasswordValidator::Validate() const +{ + std::string_view addUser = m_options.GetAddUsername(); + std::string_view addPass = m_options.GetAddPassword(); + if (!addUser.empty() && addPass.empty()) + { + return Status::Warning("'" + std::string(Options::ADDUSERNAME) + "' is enabled, but '" + + std::string(Options::ADDPASSWORD) + + "' is empty. This allows add-only access without a password"); + } + + if (addUser.empty() && !addPass.empty()) + { + return Status::Info("'" + std::string(Options::ADDPASSWORD) + "' is set, but '" + + std::string(Options::ADDUSERNAME) + + "' is empty. The add-user is currently disabled"); + } + + return Status::Ok(); +} + +Status RestrictedUsernameValidator::Validate() const + +{ + std::string_view restrictedUser = m_options.GetRestrictedUsername(); + + std::string_view controlUser = m_options.GetControlUsername(); + + if (!restrictedUser.empty() && controlUser == "nzbget") + { + return Status::Warning("'" + std::string(Options::RESTRICTEDUSERNAME) + + "' is enabled while '" + std::string(Options::CONTROLUSERNAME) + + "' is still set to the default 'nzbget'"); + } + + return Status::Ok(); +} + +Status RestrictedPasswordValidator::Validate() const +{ + std::string_view restrictedUser = m_options.GetRestrictedUsername(); + std::string_view restrictedPass = m_options.GetRestrictedPassword(); + if (!restrictedUser.empty() && restrictedPass.empty()) + { + return Status::Warning("'" + std::string(Options::RESTRICTEDUSERNAME) + + "' is enabled, but '" + std::string(Options::RESTRICTEDPASSWORD) + + "' is empty. This allows restricted access without a password"); + } + + if (restrictedUser.empty() && !restrictedPass.empty()) + { + return Status::Info("'" + std::string(Options::RESTRICTEDPASSWORD) + "' is set, but '" + + std::string(Options::RESTRICTEDUSERNAME) + + "' is empty. The restricted user is currently disabled"); + } + + return Status::Ok(); +} + +Status SecurePortValidator::Validate() const +{ + if (!m_options.GetSecureControl()) return Status::Ok(); + + Status s = Network::ValidPort(m_options.GetSecurePort()); + if (!s.IsOk()) return s; + + int securePort = m_options.GetSecurePort(); + int controlPort = m_options.GetControlPort(); + + if (securePort == controlPort) + { + return Status::Error("'" + std::string(Options::SECUREPORT) + "' cannot be the same as '" + + std::string(Options::CONTROLPORT) + "'"); + } + return Status::Ok(); +} + +Status AuthorizedIPValidator::Validate() const { return Status::Ok(); } + +Status UpdateCheckValidator::Validate() const { return Status::Ok(); } + +Status FormAuthValidator::Validate() const +{ + if (!m_options.GetFormAuth()) return Status::Ok(); + + if (!m_options.GetSecureControl()) + { + return Status::Warning("'" + std::string(Options::FORMAUTH) + "' is enabled but '" + + std::string(Options::SECURECONTROL) + + "' is disabled. Form " + "credentials may be transmitted in plaintext"); + } + + const bool hasAdd = Util::EmptyStr(m_options.GetAddUsername()); + const bool hasRestricted = Util::EmptyStr(m_options.GetRestrictedUsername()); + if (hasAdd && hasRestricted) + { + return Status::Warning( + "'" + std::string(Options::FORMAUTH) + + "' is enabled but no form users are configured. Users cannot log in via forms"); + } + + return Status::Ok(); +} + +Status SecureControlValidator::Validate() const +{ + if (m_options.GetSecureControl() && m_options.GetSecurePort() == 0) + { + return Status::Error("'" + std::string(Options::SECURECONTROL) + "' is enabled but '" + + std::string(Options::SECUREPORT) + "' is invalid"); + } + return Status::Ok(); +} + +Status CertCheckValidator::Validate() const +{ + if (!m_options.GetCertCheck() && !m_options.GetCertStorePath().empty()) + { + return Status::Warning("'" + std::string(Options::CERTCHECK) + + "' is disabled. Connections to news servers may be insecure"); + } + return Status::Ok(); +} + +#ifndef _WIN32 +Status DaemonUsernameValidator::Validate() const +{ + if (!m_options.GetDaemonMode()) return Status::Ok(); + std::string_view daemonUser = m_options.GetDaemonUsername(); + if (daemonUser == "root") + return Status::Warning("'" + std::string(Options::DAEMONUSERNAME) + + "' is set to 'root', consider using a non-privileged user"); + + return Status::Ok(); +} + +Status UmaskValidator::Validate() const +{ + int umask = m_options.GetUMask(); + if (umask == 0) + return Status::Warning("'" + std::string(Options::UMASK) + + "' is set to 0, files will be created with full permissions"); + return Status::Ok(); +} +#endif + +} // namespace SystemHealth::Security diff --git a/daemon/systemhealth/SecurityValidator.h b/daemon/systemhealth/SecurityValidator.h new file mode 100644 index 00000000..fdb1fcbe --- /dev/null +++ b/daemon/systemhealth/SecurityValidator.h @@ -0,0 +1,242 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SECURITY_VALIDATOR_H +#define SECURITY_VALIDATOR_H + +#include "SectionValidator.h" +#include "Validators.h" +#include "Options.h" + +namespace SystemHealth::Security +{ + +class SecurityValidator final : public SectionValidator +{ +public: + explicit SecurityValidator(const Options& options); + std::string_view GetName() const override { return "Security"; } + +private: + const Options& m_options; +}; + +class ControlIpValidator final : public Validator +{ +public: + explicit ControlIpValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CONTROLIP; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ControlPortValidator final : public Validator +{ +public: + explicit ControlPortValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CONTROLPORT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ControlUsernameValidator final : public Validator +{ +public: + explicit ControlUsernameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CONTROLUSERNAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class ControlPasswordValidator final : public Validator +{ +public: + explicit ControlPasswordValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CONTROLPASSWORD; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class SecureCertValidator final : public Validator +{ +public: + explicit SecureCertValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SECURECERT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class SecureKeyValidator final : public Validator +{ +public: + explicit SecureKeyValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SECUREKEY; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RestrictedUsernameValidator final : public Validator +{ +public: + explicit RestrictedUsernameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::RESTRICTEDUSERNAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class RestrictedPasswordValidator final : public Validator +{ +public: + explicit RestrictedPasswordValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::RESTRICTEDPASSWORD; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class AddUsernameValidator final : public Validator +{ +public: + explicit AddUsernameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ADDUSERNAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class AddPasswordValidator final : public Validator +{ +public: + explicit AddPasswordValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::ADDPASSWORD; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class AuthorizedIPValidator final : public Validator +{ +public: + explicit AuthorizedIPValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::AUTHORIZEDIP; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class FormAuthValidator final : public Validator +{ +public: + explicit FormAuthValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::FORMAUTH; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class SecureControlValidator final : public Validator +{ +public: + explicit SecureControlValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SECURECONTROL; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class SecurePortValidator final : public Validator +{ +public: + explicit SecurePortValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::SECUREPORT; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class CertCheckValidator final : public Validator +{ +public: + explicit CertCheckValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::CERTCHECK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UpdateCheckValidator final : public Validator +{ +public: + explicit UpdateCheckValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::UPDATECHECK; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +#ifndef _WIN32 +class DaemonUsernameValidator final : public Validator +{ +public: + explicit DaemonUsernameValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::DAEMONUSERNAME; } + Status Validate() const override; + +private: + const Options& m_options; +}; + +class UmaskValidator final : public Validator +{ +public: + explicit UmaskValidator(const Options& options) : m_options(options) {} + std::string_view GetName() const override { return Options::UMASK; } + Status Validate() const override; + +private: + const Options& m_options; +}; +#endif + +} // namespace SystemHealth::Security + +#endif diff --git a/daemon/systemhealth/Status.cpp b/daemon/systemhealth/Status.cpp new file mode 100644 index 00000000..f6655796 --- /dev/null +++ b/daemon/systemhealth/Status.cpp @@ -0,0 +1,66 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Status.h" + +namespace SystemHealth +{ +namespace +{ +const xmlChar* XmlLiteral(const char* literal) { return reinterpret_cast(literal); } +} // namespace + +Json::JsonObject ToJson(const Status& status) +{ + Json::JsonObject json; + + json["Severity"] = SeverityToStr(status.GetSeverity()); + json["Message"] = status.GetMessage(); + + return json; +} + +Xml::XmlNodePtr ToXml(const Status& status) +{ + std::string_view severity = SeverityToStr(status.GetSeverity()); + const std::string& message = status.GetMessage(); + + xmlNodePtr node = Xml::CreateStructNode(); + Xml::AddNewNode(node, "Severity", "string", severity.data()); + Xml::AddNewNode(node, "Message", "string", message.c_str()); + + return node; +} + +std::string_view SeverityToStr(Severity severity) +{ + switch (severity) + { + case Severity::Ok: + return "Ok"; + case Severity::Info: + return "Info"; + case Severity::Warning: + return "Warning"; + case Severity::Error: + return "Error"; + } + return "Ok"; +} +} // namespace SystemHealth diff --git a/daemon/systemhealth/Status.h b/daemon/systemhealth/Status.h new file mode 100644 index 00000000..3ba93965 --- /dev/null +++ b/daemon/systemhealth/Status.h @@ -0,0 +1,85 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef STATUS_H +#define STATUS_H + +#include +#include + +#include "Json.h" +#include "Xml.h" + +namespace SystemHealth +{ +enum class Severity +{ + Ok, + Info, + Warning, + Error +}; + +class Status final +{ +public: + Status() = delete; + + static Status Ok() { return Status(Severity::Ok, ""); } + static Status Info(std::string message) { return Status(Severity::Info, std::move(message)); } + static Status Warning(std::string message) + { + return Status(Severity::Warning, std::move(message)); + } + static Status Error(std::string message) { return Status(Severity::Error, std::move(message)); } + + template + Status And(Fn&& nextCheck, Args&&... args) const + { + if (IsOk()) return std::invoke(std::forward(nextCheck), std::forward(args)...); + return *this; + } + + bool IsOk() const { return m_severity == Severity::Ok; } + bool IsInfo() const { return m_severity == Severity::Info; } + bool IsWarning() const { return m_severity == Severity::Warning; } + bool IsError() const { return m_severity == Severity::Error; } + + Severity GetSeverity() const { return m_severity; } + + const std::string& GetMessage() const { return m_message; } + +private: + Status(Severity severity, std::string message) + : m_message(std::move(message)), m_severity(severity) + { + } + + std::string m_message; + Severity m_severity = Severity::Ok; +}; + +std::string_view SeverityToStr(Severity severity); + +Json::JsonObject ToJson(const Status& status); +Xml::XmlNodePtr ToXml(const Status& status); + +} // namespace SystemHealth + +#endif diff --git a/daemon/systemhealth/SystemHealth.h b/daemon/systemhealth/SystemHealth.h new file mode 100644 index 00000000..2a0dc464 --- /dev/null +++ b/daemon/systemhealth/SystemHealth.h @@ -0,0 +1,26 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SYSTEM_HEALTH_H +#define SYSTEM_HEALTH_H + +#include "SystemHealthService.h" +#include "Status.h" + +#endif diff --git a/daemon/systemhealth/SystemHealthService.cpp b/daemon/systemhealth/SystemHealthService.cpp new file mode 100644 index 00000000..0843fb12 --- /dev/null +++ b/daemon/systemhealth/SystemHealthService.cpp @@ -0,0 +1,262 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "ExtensionManager.h" +#include "SystemHealthService.h" +#include "PathsValidator.h" +#include "IncomingNzbValidator.h" +#include "SchedulerTasksValidator.h" +#include "NewsServersValidator.h" +#include "LoggingValidator.h" +#include "ExtensionScriptsValidator.h" +#include "ConnectionValidator.h" +#include "DownloadQueueValidator.h" +#include "SecurityValidator.h" +#include "CheckAndRepairValidator.h" +#include "FeedsValidator.h" +#include "CategoriesValidator.h" +#include "UnpackValidator.h" +#include "DisplayValidator.h" +#include "Status.h" +#include "Json.h" +#include "Xml.h" + +namespace SystemHealth +{ +Alert::Alert(Severity severity, std::string category, std::string source, std::string message) + : m_severity(severity) + , m_category(std::move(category)) + , m_source(std::move(source)) + , m_message(std::move(message)) + , m_timestamp{std::chrono::system_clock::now()} +{} + +Service::Service(const Options& options, const Servers& servers, const ::Feeds& feeds, + const ::Scheduler::TaskList& tasks) + : m_options(options), m_servers(servers), m_feeds(feeds), m_tasks(tasks) +{ + m_validators.reserve(12); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_servers)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique( + m_options, *g_ExtensionManager)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_options)); + m_validators.push_back(std::make_unique(m_tasks)); + m_validators.push_back( + std::make_unique(m_options, *m_options.GetCategories())); + m_validators.push_back(std::make_unique(m_feeds, m_options)); +} + +HealthReport Service::Diagnose() const +{ + HealthReport report; + report.sections.reserve(m_validators.size()); + + { + std::lock_guard lock(m_mutex); + report.alerts = m_alerts; + } + + for (const auto& validator : m_validators) + { + report.sections.push_back(validator->Validate()); + } + + return report; +} + +void Service::ReportAlert(Alert alert) +{ + std::lock_guard lock(m_mutex); + + auto it = std::find_if( + m_alerts.begin(), m_alerts.end(), [&](const Alert& existing) + { return existing.GetSource() == alert.GetSource() && existing.GetCategory() == alert.GetCategory(); }); + + if (alert.GetSeverity() == Severity::Ok) + { + if (it != m_alerts.end()) + { + m_alerts.erase(it); + } + return; + } + + if (it != m_alerts.end()) + { + *it = std::move(alert); + } + else + { + m_alerts.push_back(std::move(alert)); + } +} + +void Log(const HealthReport& report) +{ + for (const auto& alert : report.alerts) Log(alert); + for (const auto& sectionReport : report.sections) Log(sectionReport); +} + +void Log(const SectionReport& report) +{ + for (const auto& issue : report.issues) + { + if (issue.IsError()) + error("%s", issue.GetMessage().c_str()); + else if (issue.IsWarning()) + warn("%s", issue.GetMessage().c_str()); + else if (issue.IsInfo()) + info("%s", issue.GetMessage().c_str()); + } + + for (const auto& [optName, status] : report.options) + { + if (status.IsError()) + error("[%s][%s]: %s", report.name.c_str(), optName.c_str(), status.GetMessage().c_str()); + else if (status.IsWarning()) + warn("[%s][%s]: %s", report.name.c_str(), optName.c_str(), status.GetMessage().c_str()); + else if (status.IsInfo()) + info("[%s][%s]: %s", report.name.c_str(), optName.c_str(), status.GetMessage().c_str()); + } + + for (const auto& section : report.subsections) + { + for (const auto& [optName, status] : section.options) + { + if (status.IsError()) + error("[%s][%s.%s]: %s", report.name.c_str(), section.name.c_str(), optName.c_str(), + status.GetMessage().c_str()); + else if (status.IsWarning()) + warn("[%s][%s.%s]: %s", report.name.c_str(), section.name.c_str(), optName.c_str(), + status.GetMessage().c_str()); + else if (status.IsInfo()) + info("[%s][%s.%s]: %s", report.name.c_str(), section.name.c_str(), optName.c_str(), + status.GetMessage().c_str()); + } + } +} + +void Log(const Alert& alert) +{ + if (alert.GetSeverity() == Severity::Error) + error("[%s][%s]: %s", alert.GetCategory().c_str(), alert.GetSource().c_str(), + alert.GetMessage().c_str()); + else if (alert.GetSeverity() == Severity::Warning) + warn("[%s][%s]: %s", alert.GetCategory().c_str(), alert.GetSource().c_str(), + alert.GetMessage().c_str()); + else if (alert.GetSeverity() == Severity::Info) + info("[%s][%s]: %s", alert.GetCategory().c_str(), alert.GetSource().c_str(), + alert.GetMessage().c_str()); +} + +Json::JsonObject ToJson(const Alert& alert) +{ + Json::JsonObject alertJson; + alertJson["Source"] = alert.GetSource(); + alertJson["Category"] = alert.GetCategory(); + alertJson["Severity"] = SeverityToStr(alert.GetSeverity()); + alertJson["Message"] = alert.GetMessage(); + alertJson["Timestamp"] = alert.GetTimestamp().time_since_epoch().count(); + + return alertJson; +} + +std::string ToJsonStr(const HealthReport& report) +{ + Json::JsonObject reportJson; + Json::JsonArray sectionsArrayJson; + Json::JsonArray alertsArrayJson; + + for (const auto& alert : report.alerts) + { + alertsArrayJson.push_back(ToJson(alert)); + } + + for (const auto& section : report.sections) + { + sectionsArrayJson.push_back(ToJson(section)); + } + + reportJson["Alerts"] = std::move(alertsArrayJson); + reportJson["Sections"] = std::move(sectionsArrayJson); + + return Json::serialize(reportJson); +} + +// +// ... +// ... +// ... +// ... +// 123456789 +// +Xml::XmlNodePtr ToXml(const Alert& alert) +{ + xmlNodePtr structNode = Xml::CreateStructNode(); + + Xml::AddNewNode(structNode, "Source", "string", alert.GetSource().c_str()); + Xml::AddNewNode(structNode, "Category", "string", alert.GetCategory().c_str()); + const auto severity = SeverityToStr(alert.GetSeverity()); + Xml::AddNewNode(structNode, "Severity", "string", severity.data()); + Xml::AddNewNode(structNode, "Message", "string", alert.GetMessage().c_str()); + + auto ticks = std::to_string(alert.GetTimestamp().time_since_epoch().count()); + Xml::AddNewNode(structNode, "Timestamp", "i8", ticks.c_str()); + + return structNode->parent; +} + +std::string ToXmlStr(const HealthReport& report) +{ + xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0"); + xmlNodePtr structNode = Xml::CreateStructNode(); + xmlDocSetRootElement(doc, structNode->parent); + + std::vector alertNodes; + for (const auto& alert : report.alerts) + { + alertNodes.push_back(ToXml(alert)); + } + Xml::AddArrayNode(structNode, "Alerts", alertNodes); + + std::vector sectionNodes; + for (const auto& section : report.sections) + { + sectionNodes.push_back(ToXml(section)); + } + Xml::AddArrayNode(structNode, "Sections", sectionNodes); + + std::string result = Xml::Serialize(structNode->parent); + + xmlFreeDoc(doc); + + return result; +} + +} // namespace SystemHealth diff --git a/daemon/systemhealth/SystemHealthService.h b/daemon/systemhealth/SystemHealthService.h new file mode 100644 index 00000000..32ac106e --- /dev/null +++ b/daemon/systemhealth/SystemHealthService.h @@ -0,0 +1,101 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SYSTEM_HEALTH_SERVICE_H +#define SYSTEM_HEALTH_SERVICE_H + +#include +#include +#include +#include +#include +#include +#include "SectionValidator.h" +#include "Options.h" +#include "NewsServer.h" +#include "FeedInfo.h" +#include "Scheduler.h" +#include "Status.h" + +namespace SystemHealth +{ +using Timestamp = std::chrono::system_clock::time_point; + +class Alert final +{ +public: + Alert() = delete; + Alert(Severity severity, std::string category, std::string source, std::string message); + + Severity GetSeverity() const { return m_severity; } + const std::string& GetCategory() const { return m_category; } + const std::string& GetSource() const { return m_source; } + const std::string& GetMessage() const { return m_message; } + const Timestamp& GetTimestamp() const { return m_timestamp; } + +private: + Severity m_severity; + std::string m_category; + std::string m_source; + std::string m_message; + Timestamp m_timestamp; +}; + +struct HealthReport +{ + std::vector alerts; + std::vector sections; +}; + +class Service +{ +public: + Service(const Options& options, const Servers& servers, const Feeds& feeds, + const ::Scheduler::TaskList& tasks); + HealthReport Diagnose() const; + + void ReportAlert(Alert alert); + +private: + const Options& m_options; + const Servers& m_servers; + const Feeds& m_feeds; + const ::Scheduler::TaskList& m_tasks; + + std::vector> m_validators; + std::vector m_alerts; + + mutable std::mutex m_mutex; +}; + +void Log(const HealthReport& report); +void Log(const SectionReport& sectionReport); +void Log(const Alert& alert); + +Json::JsonObject ToJson(const Alert& alert); +Xml::XmlNodePtr ToXml(const Alert& alert); + +std::string ToJsonStr(const HealthReport& report); +std::string ToXmlStr(const HealthReport& report); + +} // namespace SystemHealth + +extern SystemHealth::Service* g_SystemHealth; + +#endif diff --git a/daemon/systemhealth/UnpackValidator.cpp b/daemon/systemhealth/UnpackValidator.cpp new file mode 100644 index 00000000..07eeb9e5 --- /dev/null +++ b/daemon/systemhealth/UnpackValidator.cpp @@ -0,0 +1,166 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include "SectionValidator.h" +#include "UnpackValidator.h" +#include "Options.h" +#include "Validators.h" + +namespace SystemHealth::Unpack +{ + +UnpackValidator::UnpackValidator(const Options& options) : m_options(options) +{ + m_validators.reserve(10); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); + m_validators.push_back(std::make_unique(options)); +} + +Status UnpackEnabledValidator::Validate() const +{ + if (!m_options->GetUnpack()) + { + return Status::Warning("Unpacking is disabled - archives will not be extracted"); + } + return Status::Ok(); +} + +Status DirectUnpackValidator::Validate() const +{ + bool directUnpack = m_options->GetDirectUnpack(); + bool unpack = m_options->GetUnpack(); + + if (directUnpack && !unpack) + { + return Status::Warning("'" + std::string(Options::DIRECTUNPACK) + "' is enabled, but '" + + std::string(Options::UNPACK) + + "' is disabled. Direct unpacking will not work"); + } + + return Status::Ok(); +} + +Status UseTempUnpackDirValidator::Validate() const { return Status::Ok(); } + +Status UnpackCleanupDiskValidator::Validate() const +{ + if (!m_options->GetUnpack()) + { + return Status::Info( + "'" + std::string(Options::UNPACKCLEANUPDISK) + + "' is configured but global Unpack is disabled; cleanup will not occur"); + } + return Status::Ok(); +} + +Status UnpackPauseQueueValidator::Validate() const +{ + if (m_options->GetUnpackPauseQueue() && !m_options->GetUnpack()) + { + return Status::Info("'" + std::string(Options::UNPACKPAUSEQUEUE) + + "' has no effect because Unpack is disabled"); + } + return Status::Ok(); +} + +Status UnrarCmdValidator::Validate() const +{ + if (!m_options->GetUnpack()) return Status::Ok(); + + if (m_options->GetUnrarPath().empty()) + { + return Status::Warning("'" + std::string(Options::UNRARCMD) + + "' is not configured. RAR archives cannot be unpacked"); + } + + const auto exists = File::Exists(m_options->GetUnrarPath()); + if (!exists.IsOk()) return exists; + + const auto exe = File::Executable(m_options->GetUnrarPath()); + if (!exe.IsOk()) return exe; + + return Status::Ok(); +} + +Status SevenZipCmdValidator::Validate() const +{ + if (!m_options->GetUnpack()) return Status::Ok(); + if (m_options->GetSevenZipPath().empty()) + { + return Status::Warning("'" + std::string(Options::SEVENZIPCMD) + + "' is not configured. " + "This prevents unpacking 7z archives and installing extensions"); + } + + const auto exists = File::Exists(m_options->GetSevenZipPath()); + if (!exists.IsOk()) return exists; + + const auto exe = File::Executable(m_options->GetSevenZipPath()); + if (!exe.IsOk()) return exe; + + return Status::Ok(); +} + +Status ExtensionCleanupValidator::Validate() const +{ + std::string_view ext = m_options->GetExtCleanupDisk(); + if (ext.empty()) return Status::Ok(); + if (ext.size() > 512) + { + return Status::Warning("'" + std::string(Options::EXTCLEANUPDISK) + + "' value is unusually long"); + } + return Status::Ok(); +} + +Status UnpackPassFileValidator::Validate() const +{ + // Note: We check this even if Unpack is disabled, because the user might + // enable Unpack later and forget the file path is invalid. + std::string_view unpackPassFile = m_options->GetUnpackPassFile(); + if (!unpackPassFile.empty()) + { + return File::Exists(unpackPassFile); + } + + return Status::Ok(); +} + +Status UnpackIgnoreExtValidator::Validate() const +{ + std::string_view val = m_options->GetUnpackIgnoreExt(); + if (val.empty()) return Status::Ok(); + if (val.size() > 512) + { + return Status::Warning("'" + std::string(Options::UNPACKIGNOREEXT) + "' is unusually long"); + } + return Status::Ok(); +} + +} // namespace SystemHealth::Unpack diff --git a/daemon/systemhealth/UnpackValidator.h b/daemon/systemhealth/UnpackValidator.h new file mode 100644 index 00000000..52f0dad1 --- /dev/null +++ b/daemon/systemhealth/UnpackValidator.h @@ -0,0 +1,150 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef UNPACK_VALIDATOR_H +#define UNPACK_VALIDATOR_H + +#include "SectionValidator.h" +#include "Options.h" + +namespace SystemHealth::Unpack +{ +class UnpackValidator final : public SectionValidator +{ +public: + explicit UnpackValidator(const Options& options); + std::string_view GetName() const override { return "Unpack"; } + +private: + const Options& m_options; +}; + +class UnpackEnabledValidator final : public Validator +{ +public: + explicit UnpackEnabledValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::UNPACK; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class DirectUnpackValidator final : public Validator +{ +public: + explicit DirectUnpackValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::DIRECTUNPACK; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class UseTempUnpackDirValidator final : public Validator +{ +public: + explicit UseTempUnpackDirValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::USETEMPUNPACKDIR; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class UnpackCleanupDiskValidator final : public Validator +{ +public: + explicit UnpackCleanupDiskValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::UNPACKCLEANUPDISK; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class UnpackPauseQueueValidator final : public Validator +{ +public: + explicit UnpackPauseQueueValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::UNPACKPAUSEQUEUE; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class UnrarCmdValidator final : public Validator +{ +public: + explicit UnrarCmdValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::UNRARCMD; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class SevenZipCmdValidator final : public Validator +{ +public: + explicit SevenZipCmdValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::SEVENZIPCMD; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class ExtensionCleanupValidator final : public Validator +{ +public: + explicit ExtensionCleanupValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::EXTCLEANUPDISK; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class UnpackPassFileValidator final : public Validator +{ +public: + explicit UnpackPassFileValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::UNPACKPASSFILE; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +class UnpackIgnoreExtValidator final : public Validator +{ +public: + explicit UnpackIgnoreExtValidator(const Options& options) : m_options(&options) {} + std::string_view GetName() const override { return Options::UNPACKIGNOREEXT; } + Status Validate() const override; + +private: + const Options* m_options; +}; + +} // namespace SystemHealth::Unpack + +#endif diff --git a/daemon/systemhealth/Validators.cpp b/daemon/systemhealth/Validators.cpp new file mode 100644 index 00000000..cc0673cf --- /dev/null +++ b/daemon/systemhealth/Validators.cpp @@ -0,0 +1,314 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include +#include "Validators.h" + +namespace fs = boost::filesystem; +using namespace boost::system; + +namespace SystemHealth +{ +Validator::~Validator() = default; + +Status RequiredOption(std::string_view name, std::string_view value) +{ + if (value.empty()) + { + return Status::Error("'" + std::string(name) + + "' is required and cannot be empty"); + } + + return Status::Ok(); +} + +Status RequiredPathOption(std::string_view name, const boost::filesystem::path& value) +{ + if (value.empty()) + { + return Status::Error("'" + std::string(name) + + "' is required and cannot be empty"); + } + + return Status::Ok(); +} + +Status UniquePath( + std::string_view name, const boost::filesystem::path& path, + const std::vector>& other) +{ + if (path.empty()) return Status::Ok(); + + const auto found = std::find_if(other.cbegin(), other.cend(), + [&](const auto& pair) { return pair.second == path; }); + + if (found == other.cend()) return Status::Ok(); + + return Status::Warning("'" + std::string(name) + "' and '" + std::string(found->first) + + "' are identical that can lead to unexpected behavior"); +} + +Status CheckPassword(std::string_view password) +{ + if (password.empty()) + { + return Status::Warning("Password is set to empty"); + } + + if (password.length() < 8) + { + return Status::Info("Password is too short (recommended minimum 8 characters)"); + } + + return Status::Ok(); +} + +Status CheckPositiveNum(std::string_view name, int value) +{ + if (value < 0) return Status::Error("'" + std::string(name) + "' option must not be negative"); + return Status::Ok(); +} + +namespace File +{ + +Status Exists(const fs::path& path) +{ + error_code ec; + bool exists = fs::exists(path, ec); + if (ec && ec != errc::no_such_file_or_directory) + { + std::stringstream ss; + ss << "Failed to check " << path << ": " << ec.message(); + return Status::Error(ss.str()); + } + + if (!exists) + { + std::stringstream ss; + ss << path << " file doesn't exist"; + return Status::Error(ss.str()); + } + + bool isFile = fs::is_regular_file(path, ec); + if (ec) + { + std::stringstream ss; + ss << "Failed to check type of " << path << ": " << ec.message(); + return Status::Error(ss.str()); + } + + if (!isFile) + { + std::stringstream ss; + ss << path << " exists but is not a regular file"; + return Status::Error(ss.str()); + } + + return Status::Ok(); +} + +Status Readable(const fs::path& path) +{ + std::ifstream file(path.c_str()); + if (file.is_open()) + { + return Status::Ok(); + } + + std::stringstream ss; + ss << "Failed to read " << path << ": " << std::strerror(errno); + return Status::Error(ss.str()); +} + +Status Writable(const fs::path& path) +{ + std::ofstream file(path.c_str(), std::ios::app); + if (file.is_open()) + { + return Status::Ok(); + } + + std::stringstream ss; + ss << "Failed to write to " << path << ": " << std::strerror(errno); + return Status::Error(ss.str()); +} + +Status Executable(const fs::path& path) +{ +#ifdef _WIN32 + if (!path.has_extension()) + { + std::stringstream ss; + ss << path << " is not executable: missing file extension"; + return Status::Error(ss.str()); + } + + std::string ext = path.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (ext == ".exe" || ext == ".bat" || ext == ".cmd" || ext == ".com") + { + return Status::Ok(); + } + + std::stringstream ss; + ss << path << " is not executable: invalid extension (" << ext << ")"; + return Status::Error(ss.str()); + +#else + if (access(path.c_str(), X_OK) == 0) + { + return Status::Ok(); + } + + std::stringstream ss; + ss << path << " is not executable: " << std::strerror(errno); + return Status::Error(ss.str()); +#endif +} + +} // namespace File + +namespace Directory +{ + +Status Exists(const fs::path& path) +{ + error_code ec; + bool exists = fs::exists(path, ec); + if (ec && ec != errc::no_such_file_or_directory) + { + std::stringstream ss; + ss << "Failed to check " << path << ": " << ec.message(); + return Status::Error(ss.str()); + } + + if (!exists) + { + std::stringstream ss; + ss << path << " directory does not exist"; + return Status::Error(ss.str()); + } + + bool isDir = fs::is_directory(path, ec); + if (ec) + { + std::stringstream ss; + ss << "Failed to check type of " << path << ": " << ec.message(); + return Status::Error(ss.str()); + } + + if (!isDir) + { + std::stringstream ss; + ss << path << " is not a directory"; + return Status::Error(ss.str()); + } + + return Status::Ok(); +} + +Status Readable(const fs::path& path) +{ + error_code ec; + fs::directory_iterator it(path, ec); + if (ec) + { + std::stringstream ss; + ss << "Failed to read " << path << " directory: " << ec.message(); + return Status::Error(ss.str()); + } + + return Status::Ok(); +} + +Status Writable(const fs::path& path) +{ + const fs::path testPath = path / "nzbget_write_test.tmp"; + { + std::ofstream testFile(testPath.c_str()); + if (!testFile.is_open()) + { + std::stringstream ss; + ss << "Failed to write to " << path << ": " << std::strerror(errno); + return Status::Error(ss.str()); + } + + testFile << "Write test"; + if (testFile.fail()) + { + testFile.close(); + std::stringstream ss; + ss << "Failed to write content to " << path << ": " << std::strerror(errno); + + error_code ignore; + fs::remove(testPath, ignore); + return Status::Error(ss.str()); + } + } + + error_code ec; + fs::remove(testPath, ec); + if (ec) + { + std::stringstream ss; + ss << "Writable check failed during cleanup for " << path << ": " << ec.message(); + return Status::Warning(ss.str()); + } + + return Status::Ok(); +} + +} // namespace Directory + +namespace Network +{ +Status ValidHostname(std::string_view hostname) +{ + if (hostname.empty()) + { + return Status::Error("Hostname cannot be empty"); + } + + if (hostname.length() > 253) + { + return Status::Error("Hostname is too long (max 253 characters)"); + } + + for (char c : hostname) + { + if (!std::isalnum(c) && c != '.' && c != '-' && c != '_') + { + return Status::Error("Hostname contains invalid characters"); + } + } + + return Status::Ok(); +} + +Status ValidPort(int port) +{ + if (port < 1 || port > 65535) return Status::Error("Port must be between 1 and 65535"); + + return Status::Ok(); +} +} // namespace Network +} // namespace SystemHealth diff --git a/daemon/systemhealth/Validators.h b/daemon/systemhealth/Validators.h new file mode 100644 index 00000000..c426a6cc --- /dev/null +++ b/daemon/systemhealth/Validators.h @@ -0,0 +1,69 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef VALIDATORS_H +#define VALIDATORS_H + +#include +#include +#include +#include +#include +#include "Status.h" + +namespace SystemHealth +{ +class Validator +{ +public: + virtual ~Validator(); + virtual std::string_view GetName() const = 0; + virtual Status Validate() const = 0; +}; + +Status RequiredOption(std::string_view name, std::string_view value); +Status RequiredPathOption(std::string_view name, const boost::filesystem::path& value); +Status UniquePath( + std::string_view name, const boost::filesystem::path& path, + const std::vector>& other); +Status CheckPassword(std::string_view password); +Status CheckPositiveNum(std::string_view name, int value); + +namespace File +{ +Status Exists(const boost::filesystem::path& path); +Status Readable(const boost::filesystem::path& path); +Status Writable(const boost::filesystem::path& path); +Status Executable(const boost::filesystem::path& path); +} // namespace File + +namespace Directory +{ +Status Exists(const boost::filesystem::path& path); +Status Readable(const boost::filesystem::path& path); +Status Writable(const boost::filesystem::path& path); +} // namespace Directory + +namespace Network +{ +Status ValidHostname(std::string_view hostname); +Status ValidPort(int port); +} // namespace Network +} // namespace SystemHealth + +#endif diff --git a/daemon/util/Xml.cpp b/daemon/util/Xml.cpp index 758e6607..5395d147 100644 --- a/daemon/util/Xml.cpp +++ b/daemon/util/Xml.cpp @@ -1,7 +1,7 @@ /* * This file is part of nzbget. See . * - * Copyright (C) 2023-2024 Denis + * Copyright (C) 2023-2025 Denis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,37 +21,65 @@ #include "Xml.h" -namespace Xml { - std::string Serialize(const xmlNodePtr rootNode) - { - std::string result; - - xmlBufferPtr buffer = xmlBufferCreate(); - if (buffer == nullptr) { - return result; - } +namespace Xml +{ +std::string Serialize(const xmlNodePtr rootNode) +{ + std::string result; - int size = xmlNodeDump(buffer, rootNode->doc, rootNode, 0, 0); - if (size > 0) { - result = std::string(reinterpret_cast(buffer->content), size); - } - - xmlBufferFree(buffer); + xmlBufferPtr buffer = xmlBufferCreate(); + if (buffer == nullptr) + { return result; } - void AddNewNode(xmlNodePtr rootNode, const char* name, const char* type, const char* value) + int size = xmlNodeDump(buffer, rootNode->doc, rootNode, 0, 0); + if (size > 0) { - xmlNodePtr memberNode = xmlNewNode(nullptr, BAD_CAST "member"); - xmlNodePtr valueNode = xmlNewNode(nullptr, BAD_CAST "value"); - xmlNewChild(memberNode, nullptr, BAD_CAST "name", BAD_CAST name); - xmlNewChild(valueNode, nullptr, BAD_CAST type, BAD_CAST value); - xmlAddChild(memberNode, valueNode); - xmlAddChild(rootNode, memberNode); + result = std::string(reinterpret_cast(buffer->content), size); } - const char* BoolToStr(bool value) noexcept + xmlBufferFree(buffer); + return result; +} + +void AddNewNode(xmlNodePtr rootNode, const char* name, const char* type, const char* value) +{ + xmlNodePtr memberNode = xmlNewNode(nullptr, BAD_CAST "member"); + xmlNodePtr valueNode = xmlNewNode(nullptr, BAD_CAST "value"); + xmlNewChild(memberNode, nullptr, BAD_CAST "name", BAD_CAST name); + xmlNewChild(valueNode, nullptr, BAD_CAST type, BAD_CAST value); + xmlAddChild(memberNode, valueNode); + xmlAddChild(rootNode, memberNode); +} + +xmlNodePtr CreateStructNode() +{ + xmlNodePtr valueNode = xmlNewNode(nullptr, BAD_CAST "value"); + xmlNodePtr structNode = xmlNewNode(nullptr, BAD_CAST "struct"); + xmlAddChild(valueNode, structNode); + return structNode; +} + +void AddArrayNode(xmlNodePtr structNode, const char* name, std::vector children) +{ + xmlNodePtr memberNode = xmlNewNode(nullptr, BAD_CAST "member"); + xmlNewChild(memberNode, nullptr, BAD_CAST "name", BAD_CAST name); + + xmlNodePtr valueNode = xmlNewNode(nullptr, BAD_CAST "value"); + xmlNodePtr arrayNode = xmlNewNode(nullptr, BAD_CAST "array"); + xmlNodePtr dataNode = xmlNewNode(nullptr, BAD_CAST "data"); + + for (xmlNodePtr child : children) { - return value ? "1" : "0"; + xmlAddChild(dataNode, child); } + + xmlAddChild(arrayNode, dataNode); + xmlAddChild(valueNode, arrayNode); + xmlAddChild(memberNode, valueNode); + xmlAddChild(structNode, memberNode); } + +const char* BoolToStr(bool value) noexcept { return value ? "1" : "0"; } +} // namespace Xml diff --git a/daemon/util/Xml.h b/daemon/util/Xml.h index ba0ff099..da804385 100644 --- a/daemon/util/Xml.h +++ b/daemon/util/Xml.h @@ -1,7 +1,7 @@ /* * This file is part of nzbget. See . * - * Copyright (C) 2023-2024 Denis + * Copyright (C) 2023-2025 Denis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,9 +25,12 @@ namespace Xml { - std::string Serialize(const xmlNodePtr rootNode); - void AddNewNode(xmlNodePtr rootNode, const char* name, const char* type, const char* value); - const char* BoolToStr(bool value) noexcept; -} +using XmlNodePtr = xmlNodePtr; +std::string Serialize(const xmlNodePtr rootNode); +void AddNewNode(xmlNodePtr rootNode, const char* name, const char* type, const char* value); +xmlNodePtr CreateStructNode(); +void AddArrayNode(xmlNodePtr structNode, const char* name, std::vector children); +const char* BoolToStr(bool value) noexcept; +} // namespace Xml #endif diff --git a/docs/api/API.md b/docs/api/API.md index 9f4111f7..a9414467 100644 --- a/docs/api/API.md +++ b/docs/api/API.md @@ -58,6 +58,7 @@ If HTTP basic authentication is somewhat problematic the username/password can a - [status](STATUS.md) - [sysinfo](SYSINFO.md) +- [systemhealth](SYSTEMHEALTH.md) - [log](LOG.md) - [writelog](WRITELOG.md) - [loadlog](LOADLOG.md) diff --git a/docs/api/SYSTEMHEALTH.md b/docs/api/SYSTEMHEALTH.md new file mode 100644 index 00000000..176203e6 --- /dev/null +++ b/docs/api/SYSTEMHEALTH.md @@ -0,0 +1,106 @@ +## API-method `systemhealth` + +## Since +`v26.0` + +### Status +**Experimental (Not recommended for use)** + +### Signature +``` c++ +struct systemhealth(); +``` + +### Description +Performs a diagnostic validation of the running configuration. The result is a structured report grouping findings by section, option and subsection. + +### Return value +This method returns a structure with the following fields: + +- **Status** `(String)` - Overall health severity: **Ok**, **Info**, **Warning**, **Error**. + +- **Alerts** `(struct[])` - Flat list of problematic configuration options across all sections. + - **Name** `(String)` - Option name (e.g. `ControlPassword`, `MainDir`). + - **Status** `(struct)` - The status object with `Severity` and `Message`. + +- **Sections** `(struct[])` - Detailed per-section reports. Each `SectionReport` contains: + - **Name** `(string)` - Section name (e.g. `Paths`, `Unpack`). + - **Alerts** `(struct[])` - General alerts for the section. + - **Options** `(struct[])` - Per-option checks: + - **Name** `(string)` - Option name. + - **Status** `(struct)` - `Severity` and `Message`. + - **Subsections** `(struct[])` - Nested reports (e.g. individual news servers, categories). + +### Sections +The health check covers multiple sections (representative list): + +- `Paths` — Main directories and writable/readable checks. +- `NewsServers` — Per-server configuration checks. +- `Security` — Authentication, control port/usability, TLS cert/key. +- `Categories` — Category definitions and paths. +- `Feeds` — RSS/Feed configuration checks. +- `IncomingNzb` — Incoming NZB directory options. +- `DownloadQueue` — Cache, write buffer, direct-write checks. +- `Connection` — Connection and proxy related options. +- `Logging` — Log file and rotation settings. +- `SchedulerTasks` — Scheduler-related checks. +- `CheckAndRepair` — PAR/repair and CRC checks. +- `Unpack` — Unpacker/extension handling. +- `ExtensionScripts` — Script availability and permissions. + +### Example +```json +{ + "Status": "Warning", + "Alerts": [ + { + "Name": "MainDir", + "Status": + { + "Severity": "Error", + "Message": "MainDir cannot be empty." + } + } + ], + "Sections": + [ + { + "Name": "Paths", + "Alerts": [], + "Options": + [ + { + "Name": "MainDir", + "Status": + { + "Severity": "Error", + "Message": "MainDir cannot be empty." + } + } + ], + "Subsections": [] + }, + { + "Name": "NewsServers", + "Alerts": [], + "Options": [], + "Subsections": [ + { + "Name": "Server1", + "Options": + [ + { + "Name": "Host", + "Status": + { + "Severity": "Error", + "Message": "Hostname cannot be empty." + } + } + ] + } + ] + } + ] +} +``` diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 355dcb52..9d797813 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,3 +8,4 @@ add_subdirectory(extension) add_subdirectory(nntp) add_subdirectory(system) add_subdirectory(postprocess) +add_subdirectory(systemhealth) diff --git a/tests/systemhealth/CMakeLists.txt b/tests/systemhealth/CMakeLists.txt new file mode 100644 index 00000000..b2526b74 --- /dev/null +++ b/tests/systemhealth/CMakeLists.txt @@ -0,0 +1,38 @@ +set(SystemHealthTestsSrc + main.cpp + ValidatorsTest.cpp + PathsValidatorTest.cpp + NewsServerValidatorTest.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/Status.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/Validators.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/SectionValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/PathsValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/systemhealth/NewsServerValidator.cpp + ${CMAKE_SOURCE_DIR}/daemon/nntp/NewsServer.cpp + ${CMAKE_SOURCE_DIR}/daemon/nntp/NntpConnection.cpp + ${CMAKE_SOURCE_DIR}/daemon/connect/Connection.cpp + ${CMAKE_SOURCE_DIR}/daemon/connect/TlsSocket.cpp + ${CMAKE_SOURCE_DIR}/daemon/main/Options.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/Xml.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/Json.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/NString.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/Util.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/FileSystem.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/Log.cpp +) + +if(WIN32) + set(SystemHealthTestsSrc ${SystemHealthTestsSrc} ${CMAKE_SOURCE_DIR}/daemon/util/Utf8.cpp) +endif() + +add_executable(SystemHealthTests ${SystemHealthTestsSrc}) + +target_link_libraries(SystemHealthTests PRIVATE ${LIBS}) +target_include_directories(SystemHealthTests PRIVATE ${INCLUDES}) +if (TARGET ${PACKAGE}) + target_precompile_headers(SystemHealthTests REUSE_FROM ${PACKAGE}) +else() + target_precompile_headers(SystemHealthTests PRIVATE ${CMAKE_SOURCE_DIR}/daemon/main/nzbget.h) +endif() + +add_test(NAME SystemHealthTests COMMAND $ --log_level=message) diff --git a/tests/systemhealth/NewsServerValidatorTest.cpp b/tests/systemhealth/NewsServerValidatorTest.cpp new file mode 100644 index 00000000..9d7ed434 --- /dev/null +++ b/tests/systemhealth/NewsServerValidatorTest.cpp @@ -0,0 +1,225 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include +#include + +#include "Connection.h" +#include "Options.h" +#include "NewsServerValidator.h" + +struct ServerFixture +{ + std::unique_ptr CreateServer( + bool active = true, const char* name = "ValidServer", const char* host = "news.example.com", + int port = 563, bool tls = true, const char* user = "user", const char* pass = "pass", + int maxConn = 50, int level = 0, int retention = 0, const char* cipher = "", + int ipVersion = Connection::ipAuto, bool optional = false, int group = 0, int joinGroup = 0, + unsigned int certLevel = Options::cvStrict) + { + return std::make_unique(1, active, name, host, port, ipVersion, user, pass, + (bool)joinGroup, tls, cipher, maxConn, retention, level, + group, optional, certLevel); + } +}; + +BOOST_FIXTURE_TEST_SUITE(NewsServerValidatorsSuite, ServerFixture) + +BOOST_AUTO_TEST_CASE(TestActive) +{ + auto server = CreateServer(true); + SystemHealth::NewsServer::ServerActiveValidator v(*server); + BOOST_CHECK(v.Validate().IsOk()); + + server->SetActive(false); + SystemHealth::Status s = v.Validate(); + BOOST_CHECK(s.IsWarning()); + BOOST_CHECK(s.GetMessage().find("disabled") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(TestHost) +{ + BOOST_CHECK(SystemHealth::NewsServer::ServerHostValidator( + *CreateServer(true, "MyServer", "news.valid.com")) + .Validate() + .IsOk()); + + SystemHealth::Status s = + SystemHealth::NewsServer::ServerHostValidator(*CreateServer(true, "MyServer", "")) + .Validate(); + BOOST_CHECK(s.IsError()); + + BOOST_CHECK( + SystemHealth::NewsServer::ServerHostValidator(*CreateServer(true, "MyServer", "bad@host!")) + .Validate() + .IsError()); +} + +BOOST_AUTO_TEST_CASE(TestPort) +{ + BOOST_CHECK( + SystemHealth::NewsServer::ServerPortValidator(*CreateServer(true, "", "", 563, true)) + .Validate() + .IsOk()); + + BOOST_CHECK( + SystemHealth::NewsServer::ServerPortValidator(*CreateServer(true, "", "", 443, true)) + .Validate() + .IsOk()); + + SystemHealth::Status s1 = + SystemHealth::NewsServer::ServerPortValidator(*CreateServer(true, "", "", 9000, true)) + .Validate(); + BOOST_CHECK(s1.IsInfo()); + + BOOST_CHECK( + SystemHealth::NewsServer::ServerPortValidator(*CreateServer(true, "", "", 119, false)) + .Validate() + .IsOk()); + + SystemHealth::Status s2 = + SystemHealth::NewsServer::ServerPortValidator(*CreateServer(true, "", "", 8080, false)) + .Validate(); + BOOST_CHECK(s2.IsInfo()); +} + +BOOST_AUTO_TEST_CASE(TestCredentials) +{ + auto sValid = CreateServer(true, "", "", 563, true, "user", "pass"); + BOOST_CHECK(SystemHealth::NewsServer::ServerUsernameValidator(*sValid).Validate().IsOk()); + BOOST_CHECK(SystemHealth::NewsServer::ServerPasswordValidator(*sValid).Validate().IsInfo()); + + auto sEmptyUser = CreateServer(true, "", "", 563, true, "", "pass"); + BOOST_CHECK( + SystemHealth::NewsServer::ServerUsernameValidator(*sEmptyUser).Validate().IsWarning()); + + auto sEmptyPass = CreateServer(true, "", "", 563, true, "user", ""); + BOOST_CHECK( + SystemHealth::NewsServer::ServerPasswordValidator(*sEmptyPass).Validate().IsWarning()); + + auto sShortPass = CreateServer(true, "", "", 563, true, "user", "123"); + BOOST_CHECK(SystemHealth::NewsServer::ServerPasswordValidator(*sShortPass).Validate().IsInfo()); +} + +BOOST_AUTO_TEST_CASE(TestConnections) +{ + BOOST_CHECK(SystemHealth::NewsServer::ServerConnectionsValidator( + *CreateServer(true, "", "", 0, true, "", "", 50)) + .Validate() + .IsOk()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerConnectionsValidator( + *CreateServer(true, "", "", 0, true, "", "", 4)) + .Validate() + .IsWarning()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerConnectionsValidator( + *CreateServer(true, "", "", 0, true, "", "", 0)) + .Validate() + .IsWarning()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerConnectionsValidator( + *CreateServer(true, "", "", 0, true, "", "", 1000)) + .Validate() + .IsError()); +} + +BOOST_AUTO_TEST_CASE(TestEncryption) +{ + auto s1 = CreateServer(true, "", "", 563, true, "", "", 50, 0, 0, ""); + BOOST_CHECK(SystemHealth::NewsServer::ServerEncryptionValidator(*s1).Validate().IsOk()); + + auto s2 = CreateServer(true, "", "", 119, false, "", "", 50, 0, 0, ""); + BOOST_CHECK(SystemHealth::NewsServer::ServerEncryptionValidator(*s2).Validate().IsWarning()); + + auto s3 = CreateServer(true, "", "", 563, true, "", "", 50, 0, 0, "AES"); + BOOST_CHECK(SystemHealth::NewsServer::ServerEncryptionValidator(*s3).Validate().IsOk()); + + auto s4 = CreateServer(true, "", "", 119, false, "", "", 50, 0, 0, "AES"); + BOOST_CHECK(SystemHealth::NewsServer::ServerCipherValidator(*s4).Validate().IsWarning()); +} + +BOOST_AUTO_TEST_CASE(TestRetention) +{ + BOOST_CHECK(SystemHealth::NewsServer::ServerRetentionValidator( + *CreateServer(true, "", "", 0, true, "", "", 50, 0, 0)) + .Validate() + .IsOk()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerRetentionValidator( + *CreateServer(true, "", "", 0, true, "", "", 50, 0, 3000)) + .Validate() + .IsOk()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerRetentionValidator( + *CreateServer(true, "", "", 0, true, "", "", 50, 0, 50)) + .Validate() + .IsWarning()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerRetentionValidator( + *CreateServer(true, "", "", 0, true, "", "", 50, 0, 20000)) + .Validate() + .IsInfo()); +} + +BOOST_AUTO_TEST_CASE(TestOptional) +{ + BOOST_CHECK( + SystemHealth::NewsServer::ServerOptionalValidator( + *CreateServer(true, "", "", 0, true, "", "", 50, 0, 0, "", Connection::ipAuto, false)) + .Validate() + .IsOk()); + + BOOST_CHECK( + SystemHealth::NewsServer::ServerOptionalValidator( + *CreateServer(true, "", "", 0, true, "", "", 50, 1, 0, "", Connection::ipAuto, true)) + .Validate() + .IsOk()); + + BOOST_CHECK( + SystemHealth::NewsServer::ServerOptionalValidator( + *CreateServer(true, "", "", 0, true, "", "", 50, 0, 0, "", Connection::ipAuto, true)) + .Validate() + .IsWarning()); +} + +BOOST_AUTO_TEST_CASE(TestCertVerification) +{ + BOOST_CHECK(SystemHealth::NewsServer::ServerCertVerificationValidator( + *CreateServer(true, "", "", 563, true, "", "", 50, 0, 0, "", Connection::ipAuto, + false, 0, 0, Options::cvStrict)) + .Validate() + .IsOk()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerCertVerificationValidator( + *CreateServer(true, "", "", 563, true, "", "", 50, 0, 0, "", Connection::ipAuto, + false, 0, 0, Options::cvNone)) + .Validate() + .IsWarning()); + + BOOST_CHECK(SystemHealth::NewsServer::ServerCertVerificationValidator( + *CreateServer(true, "", "", 119, false, "", "", 50, 0, 0, "", + Connection::ipAuto, false, 0, 0, Options::cvNone)) + .Validate() + .IsOk()); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/systemhealth/PathsValidatorTest.cpp b/tests/systemhealth/PathsValidatorTest.cpp new file mode 100644 index 00000000..3e23c9e2 --- /dev/null +++ b/tests/systemhealth/PathsValidatorTest.cpp @@ -0,0 +1,219 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include +#include +#include + +#include "Options.h" +#include "PathsValidator.h" + +namespace fs = boost::filesystem; +using namespace SystemHealth; +using namespace SystemHealth::Paths; + +struct StubOptions : public Options +{ + StubOptions() : Options(nullptr, nullptr) {} + + fs::path GetMainDirPath() const { return ""; } + fs::path GetDestDirPath() const { return ""; } + fs::path GetInterDirPath() const { return ""; } + fs::path GetNzbDirPath() const { return ""; } + fs::path GetQueueDirPath() const { return ""; } + fs::path GetWebDirPath() const { return ""; } + fs::path GetTempDirPath() const { return ""; } + std::vector GetScriptDirPaths() const { return {}; } + fs::path GetConfigTemplatePath() const { return ""; } + fs::path GetLogFilePath() const { return ""; } + fs::path GetCertStorePath() const { return ""; } + fs::path GetConfigFilePath() const { return ""; } + fs::path GetLockFilePath() const { return ""; } + + EWriteLog GetWriteLog() const { return EWriteLog::wlAppend; } + bool GetCertCheck() const { return false; } + bool GetDaemonMode() const { return false; } +}; + +struct PathsFixture +{ + fs::path tempPath; + StubOptions options; + + PathsFixture() + { + tempPath = fs::temp_directory_path() / fs::unique_path("nzbget_paths_test_%%%%"); + fs::create_directories(tempPath); + } + + ~PathsFixture() + { + boost::system::error_code ec; + fs::remove_all(tempPath, ec); + } +}; + +BOOST_FIXTURE_TEST_SUITE(PathsValidatorsSuite, PathsFixture) + +BOOST_AUTO_TEST_CASE(TestMainDirValidator) +{ + MainDirValidator validator(options); + fs::path dir = tempPath / "main"; + + BOOST_CHECK(validator.Validate(dir).IsError()); + + fs::create_directory(dir); + BOOST_CHECK(validator.Validate(dir).IsOk()); + + BOOST_CHECK(validator.Validate("").IsError()); +} + +BOOST_AUTO_TEST_CASE(TestDestDirValidator) +{ + DestDirValidator validator(options); + fs::path dir = tempPath / "dest"; + fs::create_directory(dir); + + BOOST_CHECK(validator.Validate(dir).IsOk()); + BOOST_CHECK(validator.Validate(tempPath / "missing").IsError()); +} + +BOOST_AUTO_TEST_CASE(TestQueueDirValidator) +{ + QueueDirValidator validator(options); + fs::path dir = tempPath / "queue"; + fs::create_directory(dir); + + BOOST_CHECK(validator.Validate(dir).IsOk()); +} + +BOOST_AUTO_TEST_CASE(TestInterDirValidator) +{ + InterDirValidator validator(options); + fs::path dir = tempPath / "inter"; + + Status s = validator.Validate(""); + BOOST_CHECK(s.IsWarning()); + BOOST_CHECK(s.GetMessage().find("not recommended") != std::string::npos); + + fs::create_directory(dir); + BOOST_CHECK(validator.Validate(dir).IsOk()); + + BOOST_CHECK(validator.Validate(tempPath / "phantom").IsError()); +} + +BOOST_AUTO_TEST_CASE(TestWebDirValidator) +{ + WebDirValidator validator(options); + fs::path dir = tempPath / "web"; + + BOOST_CHECK(validator.Validate("").IsOk()); + + fs::create_directory(dir); + BOOST_CHECK(validator.Validate(dir).IsOk()); + BOOST_CHECK(validator.Validate(tempPath / "noweb").IsError()); +} + +BOOST_AUTO_TEST_CASE(TestLogFileValidator) +{ + LogFileValidator validator(options); + fs::path log = tempPath / "nzbget.log"; + + Status s1 = validator.Validate("", Options::EWriteLog::wlAppend); + BOOST_CHECK(s1.IsError()); + + Status s2 = validator.Validate("", Options::EWriteLog::wlNone); + BOOST_CHECK(s2.IsOk()); + BOOST_CHECK(s2.GetMessage().empty()); + + { + std::ofstream(log.c_str()) << "log"; + } + BOOST_CHECK(validator.Validate(log, Options::EWriteLog::wlAppend).IsOk()); + + BOOST_CHECK(validator.Validate(tempPath / "ghost.log", Options::EWriteLog::wlAppend).IsError()); +} + +BOOST_AUTO_TEST_CASE(TestCertStoreValidator) +{ + CertStoreValidator validator(options); + fs::path certFile = tempPath / "cacert.pem"; + fs::path certDir = tempPath / "certs"; + + BOOST_CHECK(validator.Validate("", false).IsOk()); + BOOST_CHECK(validator.Validate("", true).IsError()); + + { + std::ofstream(certFile.c_str()) << "CERT"; + } + BOOST_CHECK(validator.Validate(certFile, true).IsOk()); + + fs::create_directory(certDir); + BOOST_CHECK(validator.Validate(certDir, true).IsOk()); + + Status s = validator.Validate(tempPath / "missing.pem", true); + BOOST_CHECK(s.IsError()); +} + +#ifndef _WIN32 +BOOST_AUTO_TEST_CASE(TestLockFileValidator) +{ + LockFileValidator validator(options); + fs::path lock = tempPath / "nzbget.lock"; + + Status s1 = validator.Validate("", true); + BOOST_CHECK(s1.IsWarning()); + BOOST_CHECK(s1.GetMessage().find("check for another running instance is disabled") != + std::string::npos); + + BOOST_CHECK(validator.Validate("", false).IsOk()); + BOOST_CHECK(validator.Validate("random/path", false).IsOk()); + + { + std::ofstream(lock.c_str()) << "pid"; + } + BOOST_CHECK(validator.Validate(lock, true).IsOk()); + + BOOST_CHECK(validator.Validate(tempPath / "nolock", true).IsError()); +} +#endif + +BOOST_AUTO_TEST_CASE(TestScriptDirValidator) +{ + ScriptDirValidator validator(options); + fs::path dir = tempPath / "scripts"; + + fs::create_directory(dir); + BOOST_CHECK(validator.Validate(dir).IsOk()); + + BOOST_CHECK(validator.Validate(tempPath / "missing_scripts").IsError()); +} + +BOOST_AUTO_TEST_CASE(TestPathsValidatorComposite) +{ + PathsValidator pathsVal(options); + BOOST_CHECK_EQUAL(pathsVal.GetName(), "Paths"); + + SectionReport report = pathsVal.Validate(); + BOOST_CHECK_EQUAL(report.name, "Paths"); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/systemhealth/ValidatorsTest.cpp b/tests/systemhealth/ValidatorsTest.cpp new file mode 100644 index 00000000..dc26d93b --- /dev/null +++ b/tests/systemhealth/ValidatorsTest.cpp @@ -0,0 +1,270 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include +#include +#include +#include "Validators.h" + +using namespace SystemHealth; +namespace fs = boost::filesystem; + +struct TempDirFixture +{ + fs::path tempPath; + + TempDirFixture() + { + tempPath = fs::temp_directory_path() / fs::unique_path("test_syshealth_%%%%"); + fs::create_directories(tempPath); + } + + ~TempDirFixture() + { + boost::system::error_code ec; + fs::remove_all(tempPath, ec); + } +}; + +BOOST_AUTO_TEST_SUITE(GeneralValidatorsSuite) + +BOOST_AUTO_TEST_CASE(TestRequiredOption) +{ + Status s1 = RequiredOption("Username", "admin"); + BOOST_CHECK(s1.IsOk()); + + Status s2 = RequiredOption("Username", ""); + BOOST_CHECK(s2.IsError()); + BOOST_CHECK(s2.GetMessage().find("is required") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(TestRequiredPathOption) +{ + Status s1 = RequiredPathOption("OutputDir", "/tmp"); + BOOST_CHECK(s1.IsOk()); + + Status s2 = RequiredPathOption("OutputDir", ""); + BOOST_CHECK(s2.IsError()); +} + +BOOST_AUTO_TEST_CASE(TestCheckPassword) +{ + Status s1 = CheckPassword(""); + BOOST_CHECK(s1.IsWarning()); + BOOST_CHECK(s1.GetMessage().find("empty") != std::string::npos); + + Status s2 = CheckPassword("12345"); + BOOST_CHECK(s2.IsInfo()); + BOOST_CHECK(s2.GetMessage().find("too short") != std::string::npos); + + Status s3 = CheckPassword("correcthorsebatterystaple"); + BOOST_CHECK(s3.IsOk()); +} + +BOOST_AUTO_TEST_CASE(TestCheckPositiveNum) +{ + BOOST_CHECK(CheckPositiveNum("Port", 8080).IsOk()); + BOOST_CHECK(CheckPositiveNum("Zero", 0).IsOk()); + + Status s = CheckPositiveNum("Limit", -5); + BOOST_CHECK(s.IsError()); + BOOST_CHECK(s.GetMessage().find("negative") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(TestUniquePath) +{ + fs::path p1 = "/data/downloads"; + fs::path p2 = "/data/movies"; + fs::path duplicate = "/data/downloads"; + + std::vector> others = {{"DownloadDir", p1}, + {"MovieDir", p2}}; + + BOOST_CHECK(UniquePath("MusicDir", "/data/music", others).IsOk()); + + BOOST_CHECK(UniquePath("EmptyDir", "", others).IsOk()); + + Status s = UniquePath("InterimDir", duplicate, others); + BOOST_CHECK(s.IsWarning()); + BOOST_CHECK(s.GetMessage().find("'InterimDir' and 'DownloadDir' are identical") != + std::string::npos); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(NetworkValidatorsSuite) + +BOOST_AUTO_TEST_CASE(TestValidHostname) +{ + using namespace SystemHealth::Network; + + BOOST_CHECK(ValidHostname("localhost").IsOk()); + BOOST_CHECK(ValidHostname("google.com").IsOk()); + BOOST_CHECK(ValidHostname("my-server_1").IsOk()); + + // Errors + BOOST_CHECK(ValidHostname("").IsError()); + BOOST_CHECK(ValidHostname("invalid char!").IsError()); + + std::string longHost(300, 'a'); + BOOST_CHECK(ValidHostname(longHost).IsError()); +} + +BOOST_AUTO_TEST_CASE(TestValidPort) +{ + using namespace SystemHealth::Network; + + BOOST_CHECK(ValidPort(80).IsOk()); + BOOST_CHECK(ValidPort(65535).IsOk()); + + BOOST_CHECK(ValidPort(0).IsError()); + BOOST_CHECK(ValidPort(-1).IsError()); + BOOST_CHECK(ValidPort(70000).IsError()); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(FilesystemValidatorsSuite, TempDirFixture) + +BOOST_AUTO_TEST_CASE(TestFileExists) +{ + fs::path f = tempPath / "testfile.txt"; + + Status s1 = SystemHealth::File::Exists(f); + BOOST_CHECK(s1.IsError()); + BOOST_CHECK(s1.GetMessage().find("doesn't exist") != std::string::npos); + + { + std::ofstream(f.c_str()) << "content"; + } + BOOST_CHECK(SystemHealth::File::Exists(f).IsOk()); + + fs::path d = tempPath / "subdir"; + fs::create_directory(d); + Status s2 = SystemHealth::File::Exists(d); + BOOST_CHECK(s2.IsError()); + BOOST_CHECK(s2.GetMessage().find("not a regular file") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(TestFileReadable) +{ + fs::path f = tempPath / "readable.txt"; + + { + std::ofstream(f.c_str()) << "data"; + } + BOOST_CHECK(SystemHealth::File::Readable(f).IsOk()); +} + +BOOST_AUTO_TEST_CASE(TestFileWritable) +{ + fs::path f = tempPath / "writable.txt"; + + BOOST_CHECK(SystemHealth::File::Writable(f).IsOk()); + + fs::path d = tempPath / "writedir"; + fs::create_directory(d); + + Status s = SystemHealth::File::Writable(d); + BOOST_CHECK(s.IsError()); +} + +BOOST_AUTO_TEST_CASE(TestFileExecutable) +{ +#ifdef _WIN32 + fs::path exe = tempPath / "program.exe"; + fs::path bat = tempPath / "script.bat"; + fs::path txt = tempPath / "readme.txt"; + fs::path noext = tempPath / "binary"; + + BOOST_CHECK(SystemHealth::File::Executable(exe).IsOk()); + BOOST_CHECK(SystemHealth::File::Executable(bat).IsOk()); + + Status s1 = SystemHealth::File::Executable(txt); + BOOST_CHECK(s1.IsError()); + BOOST_CHECK(s1.GetMessage().find("invalid extension") != std::string::npos); + + Status s2 = SystemHealth::File::Executable(noext); + BOOST_CHECK(s2.IsError()); + BOOST_CHECK(s2.GetMessage().find("missing file extension") != std::string::npos); + +#else + fs::path script = tempPath / "script.sh"; + { + std::ofstream(script.c_str()) << "#!/bin/bash"; + } + + Status s = SystemHealth::File::Executable(script); + BOOST_CHECK(s.IsError()); + + fs::permissions(script, fs::add_perms | fs::owner_exe); + BOOST_CHECK(SystemHealth::File::Executable(script).IsOk()); +#endif +} + +BOOST_AUTO_TEST_CASE(TestDirectoryExists) +{ + fs::path d = tempPath / "mydir"; + + BOOST_CHECK(SystemHealth::Directory::Exists(d).IsError()); + + fs::create_directory(d); + BOOST_CHECK(SystemHealth::Directory::Exists(d).IsOk()); + + fs::path f = tempPath / "fake_dir_file"; + { + std::ofstream(f.c_str()) << "x"; + } + Status s = SystemHealth::Directory::Exists(f); + BOOST_CHECK(s.IsError()); + BOOST_CHECK(s.GetMessage().find("not a directory") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(TestDirectoryWritable) +{ + fs::path d = tempPath / "writedir"; + fs::create_directory(d); + + BOOST_CHECK(SystemHealth::Directory::Writable(d).IsOk()); + BOOST_CHECK(!fs::exists(d / "nzbget_write_test.tmp")); + +#ifndef _WIN32 + fs::permissions(d, fs::remove_perms | fs::owner_write); + BOOST_CHECK(SystemHealth::Directory::Writable(d).IsError()); + fs::permissions(d, fs::add_perms | fs::owner_write); +#endif +} + +BOOST_AUTO_TEST_CASE(TestDirectoryReadable) +{ + fs::path d = tempPath / "readdir"; + fs::create_directory(d); + + BOOST_CHECK(SystemHealth::Directory::Readable(d).IsOk()); + +#ifndef _WIN32 + fs::permissions(d, fs::remove_perms | fs::owner_read); + BOOST_CHECK(SystemHealth::Directory::Readable(d).IsError()); + fs::permissions(d, fs::add_perms | fs::owner_read); +#endif +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/tests/systemhealth/main.cpp b/tests/systemhealth/main.cpp new file mode 100644 index 00000000..2a696247 --- /dev/null +++ b/tests/systemhealth/main.cpp @@ -0,0 +1,31 @@ +/* + * This file is part of nzbget. See . + */ + +#include "nzbget.h" + +#define BOOST_TEST_MODULE SystemHealthTests +#include + +#include "Log.h" +#include "Options.h" + +Log* g_Log; +Options* g_Options; + +struct InitGlobals +{ + InitGlobals() + { + g_Log = new Log(); + g_Options = new Options(nullptr, nullptr); + } + + ~InitGlobals() + { + delete g_Log; + delete g_Options; + } +}; + +BOOST_GLOBAL_FIXTURE(InitGlobals); diff --git a/webui/config.js b/webui/config.js index 1ae4558d..0c228c36 100644 --- a/webui/config.js +++ b/webui/config.js @@ -696,6 +696,7 @@ var Config = (new function($) var $ConfigInfo; var $ConfigLicenses; var $ConfigTitle; + var $ConfigTitleStatus; var $ConfigTable; var $ViewButton; var $LeaveConfigDialog; @@ -726,6 +727,7 @@ var Config = (new function($) $ConfigInfo = $('#ConfigInfo'); $ConfigLicenses = $('#ConfigLicenses'); $ConfigTitle = $('#ConfigTitle'); + $ConfigTitleStatus = $('#ConfigTitleStatus'); $ViewButton = $('#Config_ViewButton'); $LeaveConfigDialog = $('#LeaveConfigDialog'); $('#ConfigTable_filter').val(''); @@ -902,6 +904,7 @@ var Config = (new function($) } var option = section.options[i]; + option.check = SystemHealth.getCheck(SystemHealth.getSection(section.id), option.name); if (!option.template) { if (section.multi && option.multiid !== lastmultiid) @@ -1117,12 +1120,32 @@ var Config = (new function($) html += '

' + htmldescr + '

'; } + if (option.check) + { + html += makeOptionCheckSection(option.check); + } + html += ''; html += ''; return html; } + function makeOptionCheckSection(check) { + let section = '
'; + + if (check.Severity == "Error") + section += 'error' + check.Message + ''; + else if (check.Severity == "Warning") + section += 'warning' + check.Message + ''; + else if (check.Severity == "Info") + section += 'info' + check.Message + ''; + + section += '
'; + + return section; + } + function buildMultiRowStart(section, multiid, option) { var name = option.caption; @@ -1204,7 +1227,12 @@ var Config = (new function($) var section = conf.sections[i]; if (!section.hidden) { - var html = $('
  • ' + section.name + '
  • '); + var html = $('
  • '); + var link = $('' + section.name + ''); + var errorBadges = SystemHealth.makeBadges(SystemHealth.getSection(section.id)); + + html.append(link.append(errorBadges)); + if (haveExtensions) { html.addClass('list-item--nested'); @@ -1373,7 +1401,7 @@ var Config = (new function($) Config.showSection(option.sectionId, false); var element = $('#' + option.formId); - var smallScreen = $(window).width() <= 992; + var smallScreen = $(window).width() <= 768; var parent = smallScreen ? $('html,.config__main') : $('.config__main'); var offsetY = 30; @@ -1511,6 +1539,7 @@ var Config = (new function($) $ConfigInfo.show(); $ConfigData.children().hide(); $ConfigTitle.text('INFO'); + $ConfigTitleStatus.hide(); return; } @@ -1520,6 +1549,7 @@ var Config = (new function($) $('.config-status', $ConfigData).show(); SystemInfo.loadSystemInfo(); $ConfigTitle.text('STATUS'); + $ConfigTitleStatus.show(); return; } @@ -1537,6 +1567,7 @@ var Config = (new function($) $('.config-system', $ConfigData).show(); markLastControlGroup(); $ConfigTitle.text('SYSTEM'); + $ConfigTitleStatus.hide(); return; } @@ -1545,6 +1576,7 @@ var Config = (new function($) $ConfigData.children().hide(); markLastControlGroup(); $ConfigTitle.text('EXTENSION MANAGER'); + $ConfigTitleStatus.hide(); ExtensionManager.downloadRemoteExtensions(); return; } @@ -1556,6 +1588,7 @@ var Config = (new function($) var section = findSectionById(sectionId); $ConfigTitle.text(section.caption ? section.caption : section.name); + $ConfigTitleStatus.hide(); $Body.animate({ scrollTop: 0 }, { duration: animateScroll ? 'slow' : 0, easing: 'swing' }); } diff --git a/webui/dark-theme.css b/webui/dark-theme.css index d8e5263c..c9323d43 100644 --- a/webui/dark-theme.css +++ b/webui/dark-theme.css @@ -109,6 +109,26 @@ textarea, background-color: #2f96b4; } +.system-health-badge--info, +.txt-success { + color: #479f76; +} + +.system-health-badge--important, +.txt-error, +.txt-important { + color: #e35d6a; +} + +.system-health-badge--warning, +.txt-warning { + color: #ffc107; +} + +.txt-info { + color: #3a87ad; +} + #InfoBlock div:hover, .close, label, @@ -135,6 +155,9 @@ a:hover { background-color: #333; } +.accordion-inner, +.accordion-group, +.accordion-heading, .table th, .table td { border: 0.5px solid #626262; } diff --git a/webui/index.html b/webui/index.html index d7749cca..2d49353c 100644 --- a/webui/index.html +++ b/webui/index.html @@ -57,11 +57,12 @@ + %end% --> - + @@ -212,7 +213,18 @@
  • History calendar_today
     
  • Statistics analytics
     
  • Messages mail
     
  • -
  • Settings settings
     
  • +
  • + + Settings + settings +
    + + + +   + +
    +
  • @@ -550,7 +562,12 @@

    Configuration has been saved successfully

    @@ -564,7 +581,11 @@

    Configuration has been saved successfully

    -
    INFO
    +
    + INFO + +
    +
    View
    +
    +
    +
    +
    progress_activity
    diff --git a/webui/index.js b/webui/index.js index 02be23b9..dde3fcdb 100644 --- a/webui/index.js +++ b/webui/index.js @@ -297,6 +297,7 @@ var Frontend = (new function($) LimitDialog.init(); SystemInfo.init(); Statistics.init(); + SystemHealth.init(); DownloadsEditDialog.init(); DownloadsMultiDialog.init(); @@ -693,7 +694,7 @@ var Frontend = (new function($) function updateTabInfo(control, stat) { - control.toggleClass('badge-info', stat.available == stat.total).toggleClass('badge-warning', stat.available != stat.total); + control.toggleClass('badge-info', stat.available == stat.total).toggleClass('badge-default', stat.available != stat.total); control.html(stat.available); control.toggleClass('badge2', stat.total > 9); control.toggleClass('badge3', stat.total > 99); @@ -837,6 +838,7 @@ var Refresher = (new function($) RPC.safeMethods = [ 'version', 'status', + 'health', 'sysinfo', 'listgroups', 'history', diff --git a/webui/light-theme.css b/webui/light-theme.css index e7e925b8..bfbc777e 100644 --- a/webui/light-theme.css +++ b/webui/light-theme.css @@ -66,6 +66,25 @@ html { background-image: none; } +.system-health-badge--info, +.txt-success { + color: #468847; +} + +.system-health-badge--important, +.txt-error, +.txt-important { + color: #b94a48; +} + +.system-health-badge--warning, +.txt-warning { + color: #f89406; +} + +.txt-info { + color: #3a87ad; +} .navbar-fixed-top .navbar-inner { padding: 0; diff --git a/webui/status.js b/webui/status.js index f9bb4105..a808aed4 100644 --- a/webui/status.js +++ b/webui/status.js @@ -50,6 +50,8 @@ var Status = (new function($) var $ScheduledPauseDialog; var $PauseForInput; var $PauseForPreview; + var $ConfigTitle; + var $ConfigTitleStatus; // State var status; @@ -88,6 +90,8 @@ var Status = (new function($) $ScheduledPauseDialog = $('#ScheduledPauseDialog'); $PauseForInput = $('#PauseForInput'); $PauseForPreview = $('#PauseForPreview'); + $ConfigTitle = $('#ConfigTitle'); + $ConfigTitleStatus = $('#ConfigTitleStatus'); if (UISettings.setFocus) { diff --git a/webui/style.css b/webui/style.css index 26ef3b98..e7f5b9ce 100644 --- a/webui/style.css +++ b/webui/style.css @@ -17,575 +17,624 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - - * { - scrollbar-width: thin; + +* { + scrollbar-width: thin; } @font-face { - font-family: 'Material Icons'; - font-style: normal; - font-weight: normal; - src: url(lib/material-icons.woff2) format('woff2'); + font-family: "Material Icons"; + font-style: normal; + font-weight: normal; + src: url(lib/material-icons.woff2) format("woff2"); } .material-icon { - font-family: 'Material Icons'; - font-weight: normal; - font-style: normal; - font-size: 16px; - display: inline-block; - color: inherit; - text-transform: none; - letter-spacing: normal; - word-wrap: normal; - white-space: nowrap; - direction: ltr; - cursor: pointer; - vertical-align: top; - + font-family: "Material Icons"; + font-weight: normal; + font-style: normal; + font-size: 16px; + display: inline-block; + color: inherit; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + cursor: pointer; + vertical-align: top; } body { - padding-left: 0; - padding-right: 0; + padding-left: 0; + padding-right: 0; } .txt-success { - color: #5bb75b; + color: #5bb75b; } .txt-important { - color: #b94a48; + color: #b94a48; } .flex { - display: flex; + display: flex; } .justify-content-center { - justify-content: center; + justify-content: center; } .flex-center { - display: flex; - align-items: center; - width: inherit; - gap: 5px; + display: flex; + align-items: center; + width: inherit; + gap: 5px; } #AddDialog_URLProp { - vertical-align: sub; - color: inherit; + vertical-align: sub; + color: inherit; } .config-status { - max-width: 1024px; + max-width: 1024px; } .table-fixed { - table-layout: fixed; + table-layout: fixed; } -.table-fixed td, th { - vertical-align: middle; - overflow: auto; - white-space: nowrap; +.table-fixed td, +th { + vertical-align: middle; + overflow: auto; + white-space: nowrap; } .no-wrap { - white-space: nowrap; + white-space: nowrap; } .overflow-auto { - overflow: auto; + overflow: auto; } .overflow-x-auto { - overflow-x: auto; + overflow-x: auto; } -.test-server-dropdwon-menu -{ - min-width: 210px; - left: -110px; +.test-server-dropdwon-menu { + min-width: 210px; + left: -110px; } .approx-speed-txt { - padding-left: 3px; - color: darkgray; - font-style: italic; + padding-left: 3px; + color: darkgray; + font-style: italic; } .statistics__spinner-container, .system-info__spinner-container { - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; } .statistics__table { - position: relative; + position: relative; } .system-info__spinner-container { - height: 60vh; + height: 60vh; } .statistics__spinner-container { - height: 90vh; + height: 90vh; } .statistics__spinner, .system-info__spinner { - font-size: 128px; + font-size: 128px; +} + +.system-health-badge { + vertical-align: text-bottom; +} + +.system-health-badge--important { + color: #b94a48; +} + +.system-health-badge--warning { + color: #f89406; +} + +.system-health-badge--info { + color: #468847; +} + +.system-health__err-icon { + font-size: 20px; } .dist-speedtest__controls { - display: flex; - gap: 10px; + display: flex; + gap: 10px; } .help-text-error { - color: #b94a48; + color: #b94a48; } #SysInfo_DiskSpeedTestBtn.btn--disabled { - opacity: 0.6 !important; + opacity: 0.6 !important; } .spinner { - color: #0088cc; - -webkit-animation:spin 1s linear infinite; - -moz-animation:spin 1s linear infinite; - animation:spin 1s linear infinite; + color: #0088cc; + -webkit-animation: spin 1s linear infinite; + -moz-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; } -@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } -@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } -@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } +@-moz-keyframes spin { + 100% { + -moz-transform: rotate(360deg); + } +} +@-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + } +} +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} /* EXTENSION MANAGER */ .extension-manager__td { - vertical-align: middle !important; - max-width: 350px; + vertical-align: middle !important; + max-width: 350px; } .red-circle { - border-radius: 50%; - border: none; - background-color: #da4f49; + border-radius: 50%; + border: none; + background-color: #da4f49; } .green-circle { - border-radius: 50%; - border: none; - background-color: #468847; + border-radius: 50%; + border: none; + background-color: #468847; } .red-circle, .green-circle { - height: 12px; - width: 12px; + height: 12px; + width: 12px; } .btn { - text-shadow: none; + text-shadow: none; } .btn--disabled { - opacity: 0.7 !important; - pointer-events: none; + opacity: 0.7 !important; + pointer-events: none; } .list-item--nested a { - padding-left: 25px!important; + padding-left: 25px !important; } - /* NAVBAR */ .navbar-fixed-top { - margin-left: 0px; - margin-right: 0px; - position: static; + margin-left: 0px; + margin-right: 0px; + position: static; } body { - height: 100vh; - width: 100vw; - overflow: hidden; + height: 100vh; + width: 100vw; + overflow: hidden; } -body.navfixed .navbar-fixed-top { - position: static; +body.navfixed .navbar-fixed-top { + position: static; } body.navfixed.scrolled .navbar-fixed-top .navbar-inner { - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + -webkit-box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.25), + inset 0 -1px 0 rgba(0, 0, 0, 0.1); + -moz-box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.25), + inset 0 -1px 0 rgba(0, 0, 0, 0.1); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.25), + inset 0 -1px 0 rgba(0, 0, 0, 0.1); } /* end of fixed navbar */ #Logo { - float: left; - margin-top: 1px; - margin-right: 5px; - width: 88px; - height: 36px; - cursor: pointer; - opacity: 1; + float: left; + margin-top: 1px; + margin-right: 5px; + width: 88px; + height: 36px; + cursor: pointer; + opacity: 1; } #Logo i, #Logo i:hover { - opacity: 1; - filter: alpha(opacity=100); + opacity: 1; + filter: alpha(opacity=100); } .img-logo { - background-position: -8px -139px; - width: 88px; - height: 38px; - display: inline-block; + background-position: -8px -139px; + width: 88px; + height: 38px; + display: inline-block; } #PlayBlock { - width: 70px; - height: 38px; - display: block; - float: left; - position: relative; - z-index: 2; + width: 70px; + height: 38px; + display: block; + float: left; + position: relative; + z-index: 2; } .PlayBlockInner { - width: 48px; - height: 48px; - margin-left: 0px; - margin-top: 0px; - cursor: pointer; - position: absolute; - -webkit-border-radius: 18px; - -moz-border-radius: 18px; - border-radius: 18px; + width: 48px; + height: 48px; + margin-left: 0px; + margin-top: 0px; + cursor: pointer; + position: absolute; + -webkit-border-radius: 18px; + -moz-border-radius: 18px; + border-radius: 18px; } .img-download-btn-active { - background-position: -113px -80px; - width: 50px; - height: 50px; + background-position: -113px -80px; + width: 50px; + height: 50px; } .img-download-btn-pause { - background-position: -177px -80px; - width: 50px; - height: 50px; + background-position: -177px -80px; + width: 50px; + height: 50px; } .PlayBlockInner:hover .img-download-btn-active, .PlayBlockInner:hover .img-download-btn-active { - background-position: -113px -133px; - opacity: 0.9; + background-position: -113px -133px; + opacity: 0.9; } .PlayBlockInner:hover .img-download-btn-pause, .PlayBlockInner:hover .img-download-btn-pause { - background-position: -177px -133px; - opacity: 0.9; + background-position: -177px -133px; + opacity: 0.9; } #PlayCaretBlock { - left: 37px; - top: 1px; - position: absolute; + left: 37px; + top: 1px; + position: absolute; } #PlayCaretButton { - border: 0; - background: none; - width: 24px; - height: 22px; - padding: 3px; - line-height: 10px; - vertical-align: top; + border: 0; + background: none; + width: 24px; + height: 22px; + padding: 3px; + line-height: 10px; + vertical-align: top; } #PlayCaretButton:hover #PlayCaret { - opacity: 1; - filter: alpha(opacity=100); + opacity: 1; + filter: alpha(opacity=100); } @-webkit-keyframes play-rotate { - 0% { -webkit-transform: rotate(0); } - 100% { -webkit-transform: rotate(360deg); } + 0% { + -webkit-transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + } } @-moz-keyframes play-rotate { - 0% { -moz-transform: rotate(0); } - 100% { -moz-transform: rotate(360deg); } + 0% { + -moz-transform: rotate(0); + } + 100% { + -moz-transform: rotate(360deg); + } } @-ms-keyframes play-rotate { - 0% { -ms-transform: rotate(0); } - 100% { -ms-transform: rotate(360deg); } + 0% { + -ms-transform: rotate(0); + } + 100% { + -ms-transform: rotate(360deg); + } } #PlayAnimation { - position: absolute; - z-index: 4; - -webkit-background-size: 70px 70px; - -moz-background-size: 70px 70px; - background-size: 70px 70px; - background-position: center; - width: 50px; - height: 50px; - cursor: pointer; - pointer-events: none; + position: absolute; + z-index: 4; + -webkit-background-size: 70px 70px; + -moz-background-size: 70px 70px; + background-size: 70px 70px; + background-position: center; + width: 50px; + height: 50px; + cursor: pointer; + pointer-events: none; -webkit-animation: play-rotate 1s linear infinite; - -moz-animation: play-rotate 1s linear infinite; - -ms-animation: play-rotate 1s linear infinite; + -moz-animation: play-rotate 1s linear infinite; + -ms-animation: play-rotate 1s linear infinite; } #PlayAnimation.play { - background-image: url("./img/download-anim-green-2x.png"); + background-image: url("./img/download-anim-green-2x.png"); } #PlayAnimation.pause { - background-image: url("./img/download-anim-orange-2x.png"); + background-image: url("./img/download-anim-orange-2x.png"); } #InfoBlock { - float: left; - margin-right: 10px; - padding-top: 2px; - cursor: pointer; - width: 86px; + float: left; + margin-right: 10px; + padding-top: 2px; + cursor: pointer; + width: 86px; } #InfoBlock div { - margin-top: 1px; - font-size: 12px; - font-weight: bold; - margin: 0; - padding: 0; + margin-top: 1px; + font-size: 12px; + font-weight: bold; + margin: 0; + padding: 0; } #InfoBlock i { - margin-right: 1px; + margin-right: 1px; } .navbar-inner i { - opacity: 0.8; - filter: alpha(opacity=80); + opacity: 0.8; + filter: alpha(opacity=80); } #InfoBlock div:hover i { - opacity: 1; - filter: alpha(opacity=100); + opacity: 1; + filter: alpha(opacity=100); } #StatusTime.orange { - color: #F08929; + color: #f08929; } #StatusTime.orange:hover { - color: #FFA15A; + color: #ffa15a; } .navbar-inner .btn:hover i { - opacity: 1; - filter: alpha(opacity=100); + opacity: 1; + filter: alpha(opacity=100); } #NavLinks { - margin-right: 0; + margin-right: 0; } .navbar-container { - padding-left: 10px; - padding-right: 10px; + padding-left: 10px; + padding-right: 10px; } .navbar .btn-group { - padding: 0; + padding: 0; } /* needed for Safari 4 */ .btn-toolbar .btn { - height: 28px; + height: 28px; } .navbar .nav { - margin-right: 0; + margin-right: 0; } .navbar .nav > li > a { - color: inherit; + color: inherit; } #NavLinks .badge { - min-width: 14px; - display: inline-block; - text-align: center; - padding: 1px 4px; + min-width: 14px; + display: inline-block; + text-align: center; + padding: 1px 4px; } #NavLinks .badge.badge2 { - min-width: 18px; + min-width: 18px; } #NavLinks .badge.badge3 { - min-width: 28px; + min-width: 28px; } -#NavLinks .badge-empty { - background: none; +#NavLinks .badge-empty { + background: none; } /* for checkboxes in dropdown menu */ -.menu-refresh td:first-child , +.menu-refresh td:first-child, .menu-check td:first-child { - width: 18px; + width: 18px; } /* for checkboxes in dropdown menu "Refresh" */ .menu-refresh td:nth-child(2) { - width: 20px; - text-align: right; - padding-right: 5px; + width: 20px; + text-align: right; + padding-right: 5px; } /* Search box */ .navbar-search { - margin: 0px 10px; + margin: 0px 10px; } .navbar-search .search-query, .navbar-search .search-query:focus, .navbar-search .search-query.focused { - width: 130px; - padding: 3px 28px 3px 25px; - -webkit-border-radius: 12px; - -moz-border-radius: 12px; - border-radius: 12px; + width: 130px; + padding: 3px 28px 3px 25px; + -webkit-border-radius: 12px; + -moz-border-radius: 12px; + border-radius: 12px; } .navbar-search .search-query:focus, .navbar-search .search-query.focused { - margin-top: 1px; + margin-top: 1px; } .search-clear { - position: absolute; - top: 6px; - right: 9px; - cursor: pointer; - opacity: 0.65; - filter: alpha(opacity=65); + position: absolute; + top: 6px; + right: 9px; + cursor: pointer; + opacity: 0.65; + filter: alpha(opacity=65); } .search-caret-block { - left: -2px; - top: 2px; - position: absolute; + left: -2px; + top: 2px; + position: absolute; } .search-caret-button { - border: 0; - background: none; - width: 24px; - height: 22px; - padding: 3px; - line-height: 10px; - vertical-align: top; + border: 0; + background: none; + width: 24px; + height: 22px; + padding: 3px; + line-height: 10px; + vertical-align: top; } .search-caret-button:hover .caret { - opacity: 1; - filter: alpha(opacity=100); + opacity: 1; + filter: alpha(opacity=100); } /* MENUS */ -#RefreshMenu { - min-width: 160px; +#RefreshMenu { + min-width: 160px; } #SettingsMenu { - min-width: 190px; + min-width: 190px; } #PlayMenu { - min-width: 190px; + min-width: 190px; } #ToolbarOptMenu { - min-width: 215px; + min-width: 215px; } #RssMenu { - max-width: 300px; - overflow: hidden; + max-width: 300px; + overflow: hidden; } .menu-with-button a { - padding-right: 70px; - overflow: hidden; + padding-right: 70px; + overflow: hidden; } .menu-with-button a .btn, .phone .menu-with-button a .btn { - visibility: hidden; - height: 22px; - line-height: 12px; - font-size: 8pt; - padding: 2px 7px; - margin-right: 0px; - margin-top: -2px; - margin-bottom: -1px; - right: 15px; - position: absolute; + visibility: hidden; + height: 22px; + line-height: 12px; + font-size: 8pt; + padding: 2px 7px; + margin-right: 0px; + margin-top: -2px; + margin-bottom: -1px; + right: 15px; + position: absolute; } .menu-with-button a:hover .btn { - visibility: visible; + visibility: visible; } .menu-with-button .menu-no-items { - padding-left: 15px; + padding-left: 15px; } #FilterMenu { - left: -3px; - right: auto; - margin-top: 3px; - min-width: 170px; - max-width: 270px; + left: -3px; + right: auto; + margin-top: 3px; + min-width: 170px; + max-width: 270px; } #FilterMenu::after { - left: 13px; - right: auto; + left: 13px; + right: auto; } #FilterMenu::before { - left: 12px; - right: auto; + left: 12px; + right: auto; } #DownloadsEdit_ActionsMenu, #HistoryEdit_ActionsMenu { - min-width: 120px; + min-width: 120px; } ul.dropdown-menu > li > a { - line-height: 20px; + line-height: 20px; } ul.dropdown-menu > li > a.has-table { - line-height: 18px; + line-height: 18px; } ul.dropdown-menu > li > a > i { - margin-right: 5px; + margin-right: 5px; } /* BEGIN: Icons */ @@ -593,636 +642,640 @@ ul.dropdown-menu > li > a > i { [class*=" icon-"], [class^="img-"], [class*=" img-"] { - background-image: url("./img/icons.png"); + background-image: url("./img/icons.png"); } /* HiDPI screens */ @media only screen and (-webkit-min-device-pixel-ratio: 2) { - [class^="icon-"], - [class*=" icon-"], - [class^="img-"], - [class*=" img-"] { - background-image: url("./img/icons-2x.png"); - -webkit-background-size: 700px 300px; - -moz-background-size: 700px 300px; - background-size: 700px 300px; - } + [class^="icon-"], + [class*=" icon-"], + [class^="img-"], + [class*=" img-"] { + background-image: url("./img/icons-2x.png"); + -webkit-background-size: 700px 300px; + -moz-background-size: 700px 300px; + background-size: 700px 300px; + } } [class^="icon-"], [class*=" icon-"] { - display: inline-block; - vertical-align: text-top; - width: 16px; - height: 16px; - line-height: 16px; + display: inline-block; + vertical-align: text-top; + width: 16px; + height: 16px; + line-height: 16px; } .icon-empty { - background-position: -1000px -1000px; + background-position: -1000px -1000px; } .icon-plus { - background-position: -16px -16px; + background-position: -16px -16px; } .icon-minus { - background-position: -368px -112px; + background-position: -368px -112px; } .icon-remove-white { - background-position: -48px -16px; + background-position: -48px -16px; } .icon-ok { - background-position: -80px -16px; + background-position: -80px -16px; } .icon-time { - background-position: -112px -16px; + background-position: -112px -16px; } .icon-file { - background-position: -144px -16px; + background-position: -144px -16px; } .icon-messages { - background-position: -176px -16px; + background-position: -176px -16px; } .icon-play { - background-position: -208px -16px; + background-position: -208px -16px; } .icon-pause { - background-position: -240px -16px; + background-position: -240px -16px; } .icon-down { - background-position: -272px -16px; + background-position: -272px -16px; } .icon-up { - background-position: -304px -16px; + background-position: -304px -16px; } .icon-bottom { - background-position: -336px -16px; + background-position: -336px -16px; } .icon-top { - background-position: -368px -16px; + background-position: -368px -16px; } .icon-back { - background-position: -304px -80px; + background-position: -304px -80px; } .icon-forward { - background-position: -336px -80px; + background-position: -336px -80px; } .icon-nextpage { - background-position: -368px -80px; + background-position: -368px -80px; } .icon-refresh { - background-position: -16px -48px; + background-position: -16px -48px; } .icon-edit { - background-position: -48px -48px; + background-position: -48px -48px; } .icon-trash { - background-position: -80px -48px; + background-position: -80px -48px; } .icon-settings { - background-position: -112px -48px; + background-position: -112px -48px; } .icon-downloads { - background-position: -144px -48px; + background-position: -144px -48px; } .icon-plane { - background-position: -176px -48px; + background-position: -176px -48px; } .icon-truck { - background-position: -208px -48px; + background-position: -208px -48px; } .icon-history { - background-position: -240px -48px; + background-position: -240px -48px; } .icon-remove, .icon-close { - background-position: -272px -48px; + background-position: -272px -48px; } .icon-merge { - background-position: -304px -48px; + background-position: -304px -48px; } .icon-save { - background-position: -464px -80px; + background-position: -464px -80px; } .icon-rss { - background-position: -304px -112px; + background-position: -304px -112px; } .icon-trash-white { - background-position: -336px -48px; + background-position: -336px -48px; } .icon-downloads-white { - background-position: -368px -48px; + background-position: -368px -48px; } .icon-history-white { - background-position: -400px -48px; + background-position: -400px -48px; } .icon-settings-white { - background-position: -432px -48px; + background-position: -432px -48px; } .icon-messages-white { - background-position: -464px -48px; + background-position: -464px -48px; } .icon-time-orange { - background-position: -400px -80px; + background-position: -400px -80px; } .icon-split { - background-position: -432px -80px; + background-position: -432px -80px; } .img-checkmark { - background-position: -432px -16px; + background-position: -432px -16px; } .img-checkminus { - background-position: -400px -16px; + background-position: -400px -16px; } .icon-postcard { - background-position: -432px -112px; + background-position: -432px -112px; } .icon-link { - background-position: -400px -112px; + background-position: -400px -112px; } .icon-alert { - background-position: -336px -112px; + background-position: -336px -112px; } .icon-process { - background-position: -304px -144px; + background-position: -304px -144px; } .icon-process-auto { - background-position: -336px -144px; + background-position: -336px -144px; } .icon-duplicates { - background-position: -368px -144px; + background-position: -368px -144px; } .icon-mask { - background-position: -400px -144px; + background-position: -400px -144px; } .icon-mask-white { - background-position: -432px -144px; + background-position: -432px -144px; } .icon-ring-red { - background-position: -528px -16px; + background-position: -528px -16px; } .icon-ring-fill-red { - background-position: -560px -16px; + background-position: -560px -16px; } .icon-circle-red { - background-position: -496px -16px; + background-position: -496px -16px; } .icon-ring-blue { - background-position: -528px -48px; + background-position: -528px -48px; } .icon-ring-fill-blue { - background-position: -560px -48px; + background-position: -560px -48px; } .icon-ring-ltgrey { - background-position: -496px -48px; + background-position: -496px -48px; } /* END: Icons */ .btn-toolbar { - margin-top: 6px; - margin-bottom: 0px; + margin-top: 6px; + margin-bottom: 0px; } .section-toolbar, .modal-toolbar { - margin-top: 0; - margin-bottom: 7px; + margin-top: 0; + margin-bottom: 7px; } .section-title { - margin-right: 10px; + margin-right: 10px; } .label-status { - text-transform: uppercase; + text-transform: uppercase; } .label-inline { - display: inline-block; - margin-bottom: -4px; - overflow-x: hidden; - text-overflow: ellipsis; + display: inline-block; + margin-bottom: -4px; + overflow-x: hidden; + text-overflow: ellipsis; } .controls .label-status { - line-height: 22px; + line-height: 22px; } .invisible { - visibility: hidden; + visibility: hidden; } .table a.badge-link:hover { - text-decoration: none; + text-decoration: none; } table.datatable > tbody > tr > td { - word-wrap: break-word; + word-wrap: break-word; } #MainTabContent { - margin-top: 0; - overflow: visible; /* fix problem with dropdown menus */ + margin-top: 0; + overflow: visible; /* fix problem with dropdown menus */ } /* top toolbox (length-combo and pager) for tables */ .toolbox-top { - margin-bottom: 8px; + margin-bottom: 8px; } div.btn-group + div.toolbox-length { - margin-left: 10px; + margin-left: 10px; } /* combobox with page length for tables */ div.toolbox-length select { - width: 75px; - height: 28px; + width: 75px; + height: 28px; } .toolbox-info { - margin-top: 10px; + margin-top: 10px; } .pagination { - height: 32px; - margin: 0; + height: 32px; + margin: 0; } .pagination a { - padding: 0 10px; - line-height: 26px; + padding: 0 10px; + line-height: 26px; } .modal-tab .pagination { - margin-bottom: 10px; + margin-bottom: 10px; } .padded-tab { - padding-left: 20px; - padding-right: 20px; + padding-left: 20px; + padding-right: 20px; } h1 { - font-size: 24px; - line-height: 36px; - margin-bottom: 8px; - margin-right: 20px; + font-size: 24px; + line-height: 36px; + margin-bottom: 8px; + margin-right: 20px; } h2 { - font-size: 20px; - line-height: 26px; - margin-bottom: 8px; - margin-right: 20px; + font-size: 20px; + line-height: 26px; + margin-bottom: 8px; + margin-right: 20px; } .alert-heading { - margin-bottom: 10px; + margin-bottom: 10px; } /* remove focus border */ -.nav-tabs > .active > a, .nav-tabs > .active > a:hover, -.nav-pills > .active > a, .nav-pills > .active > a:hover, -.nav-list > .active > a, .nav-list > .active > a:hover, -.btn, .btn-group .btn, .pagination a, -.btn-toolbar .btn , .control-group .btn, .modal-footer .btn, .form-search .btn, +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover, +.nav-pills > .active > a, +.nav-pills > .active > a:hover, +.nav-list > .active > a, +.nav-list > .active > a:hover, +.btn, +.btn-group .btn, +.pagination a, +.btn-toolbar .btn, +.control-group .btn, +.modal-footer .btn, +.form-search .btn, .btn:focus { - outline: 0; + outline: 0; } form { - margin-bottom: 0px; + margin-bottom: 0px; } #DownloadsEdit_PostParamData, #HistoryEdit_PostParamData { - padding-bottom: 1px; + padding-bottom: 1px; } #DownloadsEdit_FileTable_filter, -#DownloadsEdit_LogTable_filter -#HistoryEdit_LogTable_filter, +#DownloadsEdit_LogTable_filter #HistoryEdit_LogTable_filter, #FeedDialog_ItemTable_filter { - width: 180px; + width: 180px; } #DownloadsLogRecordsPerPageBlock, #HistoryLogRecordsPerPageBlock { - margin-bottom: 12px; + margin-bottom: 12px; } #DownloadsEdit_LogTable_filterBlock, #HistoryEdit_LogTable_filterBlock { - float: left; - margin-left: 20px; - margin-right: 20px; - margin-bottom: 12px; + float: left; + margin-left: 20px; + margin-right: 20px; + margin-bottom: 12px; } .phone .modal-toolbox { - margin-bottom: 20px; + margin-bottom: 20px; } .loading-block { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - text-align: center; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + text-align: center; } .loading-block img { - position: absolute; - top: 50%; - left: 50%; + position: absolute; + top: 50%; + left: 50%; } .modal-body { - max-height: 360px; + max-height: 360px; } .modal.no-footer .modal-body { - max-height: 420px; + max-height: 420px; } .modal-footer .btn-primary { - min-width: 40px; + min-width: 40px; } .modal-body .alert { - margin-bottom: 0px; - padding: 6px; + margin-bottom: 0px; + padding: 6px; } .modal-max { - margin: 15px; - left: 0; - top: 0; - bottom: 0; - right: 0; - width: auto; - height: auto; + margin: 15px; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: auto; + height: auto; } .modal-max .modal-body { - position: absolute; - left: 0; - right: 0; - max-height: inherit; - /* top: 46px; // must be calculated at runtime */ - /* bottom: 58px; // must be calculated at runtime */ + position: absolute; + left: 0; + right: 0; + max-height: inherit; + /* top: 46px; // must be calculated at runtime */ + /* bottom: 58px; // must be calculated at runtime */ } .modal-max .modal-inner-scroll { - top: 54px; + top: 54px; } .modal-max .modal-footer { - position: absolute; - left: 0; - right: 0; - bottom: 0; + position: absolute; + left: 0; + right: 0; + bottom: 0; } .modal-inner-scroll { - bottom: 0; - left: 15px; - overflow-y: auto; - position: absolute; - right: 0; - padding-right: 15px; + bottom: 0; + left: 15px; + overflow-y: auto; + position: absolute; + right: 0; + padding-right: 15px; } .modal-inner-scroll .toolbox-info { - margin-bottom: 10px; + margin-bottom: 10px; } .modal-2 { - margin-top: -200px; - z-index: 1060; + margin-top: -200px; + z-index: 1060; } .modal-backdrop.modal-2 { - z-index: 1055; - opacity: 0.6; + z-index: 1055; + opacity: 0.6; } - - /* BEGIN: Tables */ .table { - margin-bottom: 0px; + margin-bottom: 0px; } .table-bordered:not(.table-fixed) th, .table-bordered:not(.table-fixed) td { - border-left: none; + border-left: none; } .text-right { - text-align: right; + text-align: right; } .text-center, table td.text-center, table th.text-center { - text-align: center; + text-align: center; } -.table th, .table td { - padding: 5px; +.table th, +.table td { + padding: 5px; } .table-condensed th, .table-condensed td { - padding: 2px; + padding: 2px; } .table-nonbordered, .table-nonbordered td { - border: none; + border: none; } /* END: Tables */ - /* BEGIN: Checkmarks in the table */ table.table-check > thead > tr > th:first-child, table.table-check > tbody > tr > td:first-child { - width: 14px; - height: 14px; - padding-left: 6px; + width: 14px; + height: 14px; + padding-left: 6px; } th > div.check { - margin-bottom: 2px; + margin-bottom: 2px; } -table.table-cancheck tr div.img-check { - background-position: 10px 10px; +table.table-cancheck tr div.img-check { + background-position: 10px 10px; } -table.table-cancheck tr.checked div.img-check { - background-position: -434px -18px; +table.table-cancheck tr.checked div.img-check { + background-position: -434px -18px; } table.table-cancheck tr.checkremove div.img-check { - background-position: -402px -18px; + background-position: -402px -18px; } .check-simple tr.checked, .check-simple tr.checked td, .table-striped.check-simple tbody tr.checked:nth-child(odd) td { - background-color: inherit; + background-color: inherit; } table.table-hidecheck thead > tr > th:first-child, table.table-hidecheck tbody > tr > td:first-child { - display: none; + display: none; } /* END: Checkmarks in the table */ - /* BEGIN: Progress bars */ .progress-block { - position: relative; - width: calc(8.5rem); + position: relative; + width: calc(8.5rem); } /* text on left side of progress bar */ .bar-text-left { - position: absolute; - top: 0; - left: 5px; - text-align: left; + position: absolute; + top: 0; + left: 5px; + text-align: left; } /* text on right side of progress bar */ .bar-text-right { - position: absolute; - top: 0; - right: 5px; - text-align: right; + position: absolute; + top: 0; + right: 5px; + text-align: right; } /* text on left side of progress bar */ .bar-text-center { - position: absolute; - top: 0; - left: 5px; - width: 100%; - text-align: center; + position: absolute; + top: 0; + left: 5px; + width: 100%; + text-align: center; } /* END: Progress bars */ /* DROP-DOWN CONTEXT MENUS */ td.dropdown-cell { - padding: 0; + padding: 0; } th.dropafter-cell, td.dropafter-cell { - padding-left: 0; + padding-left: 0; } td.dropdown-cell > div { - padding: 5px 12px 6px 4px; - margin: 0px 0; - display: inline-block; - position: relative; + padding: 5px 12px 6px 4px; + margin: 0px 0; + display: inline-block; + position: relative; } td.dropdown-cell > div:hover { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } td.dropdown-cell > div:not(.dropdown-disabled):hover { - cursor: pointer; + cursor: pointer; } .dropdown-menu { - z-index: 3000; + z-index: 3000; } table th.priority-cell { - padding-left: 8px; + padding-left: 8px; } table td.priority-cell { - width: 16px; + width: 16px; } td.dropdown-cell.priority-cell > div { - padding-right: 7px; + padding-right: 7px; } td.dropdown-cell.priority-cell > div:not(.dropdown-disabled):hover::after { - margin-left: -1px; + margin-left: -1px; } #DownloadsTable td:first-child { - padding-right: 6px; + padding-right: 6px; } td span.none-category { - color: transparent; + color: transparent; } /* controls */ @@ -1230,214 +1283,213 @@ input[readonly], select[readonly], textarea[readonly], .uneditable-input { - cursor: inherit; + cursor: inherit; } .input-prepend .add-on-small, .input-append .add-on-small { - font-size: 11px; + font-size: 11px; } .toolbtn { - min-width: 56px; + min-width: 56px; } /* BEGIN: override bootstrap styles for modals */ .modal-header h3 { - text-align: center; + text-align: center; } .modal-header .close { - margin-top: 6px; - margin-left: 10px; - opacity: 0.6; - filter: alpha(opacity=60); - outline: 0; + margin-top: 6px; + margin-left: 10px; + opacity: 0.6; + filter: alpha(opacity=60); + outline: 0; } .modal-header .close:hover { - opacity: 0.9; - filter: alpha(opacity=90); + opacity: 0.9; + filter: alpha(opacity=90); } .modal-header .back { - float: left; - margin-top: 6px; - margin-right: 10px; - font-size: 20px; - font-weight: bold; - line-height: 18px; - opacity: 0.6; - filter: alpha(opacity=60); - outline: 0; + float: left; + margin-top: 6px; + margin-right: 10px; + font-size: 20px; + font-weight: bold; + line-height: 18px; + opacity: 0.6; + filter: alpha(opacity=60); + outline: 0; } .modal-header .back:hover { - cursor: pointer; - opacity: 0.9; - filter: alpha(opacity=90); + cursor: pointer; + opacity: 0.9; + filter: alpha(opacity=90); } .modal-header .back-hidden:hover { - cursor: inherit; + cursor: inherit; } .form-horizontal .control-group { - margin-bottom: 12px; + margin-bottom: 12px; } .modal .form-horizontal .control-group:last-child { - margin-bottom: 0; + margin-bottom: 0; } .modal .form-horizontal .retain-margin .control-group:last-child { - margin-bottom: 15px; + margin-bottom: 15px; } .form-horizontal .control-group-last { - margin-bottom: 0; + margin-bottom: 0; } -.form-horizontal .help-block { - margin-top: 4px; - margin-bottom: -2px; - line-height: 18px; +.form-horizontal .help-block { + margin-top: 4px; + margin-bottom: -2px; + line-height: 18px; } .form-horizontal .help-block-uneditable { - margin-top: 3px; + margin-top: 3px; } .modal .input-medium { - width: 200px; + width: 200px; } .modal .input-xlarge { - width: 350px; + width: 350px; } .modal .input-xblarge { - width: 335px; + width: 335px; } .modal.modal-padded .input-xxlarge { - width: 470px; + width: 470px; } /* END: override bootstrap styles for modals */ .modal-bottom-toolbar .btn { - margin-top: 12px; + margin-top: 12px; } /* based on uneditable-input */ .input-medium.uneditable-input, .uneditable-mulitline-input { - margin-bottom: -4px; + margin-bottom: -4px; } .modal-mini { - width: 420px; - margin-left:-210px; + width: 420px; + margin-left: -210px; } .modal-small { - width: 480px; - margin-left:-240px; + width: 480px; + margin-left: -240px; } .modal-large { - width: 700px; - margin-left:-350px; + width: 700px; + margin-left: -350px; } .modal-padded-small .modal-body { - padding-left: 25px; - padding-right: 25px; + padding-left: 25px; + padding-right: 25px; } .modal-padded .modal-body { - padding-left: 40px; - padding-right: 40px; + padding-left: 40px; + padding-right: 40px; } .modal-tab-padded { - padding-left: 25px; - padding-right: 25px; + padding-left: 25px; + padding-right: 25px; } .modal-tab-padded-small { - padding-left: 10px; - padding-right: 10px; + padding-left: 10px; + padding-right: 10px; } ul.help > li { - margin-bottom: 10px; + margin-bottom: 10px; } /* Make "select files" native control invisible */ .hidden-file-input { - position: absolute; - left: 0; - top: 0; - width: 0; - height: 0; - opacity: 0; - filter: alpha(opacity=0); + position: absolute; + left: 0; + top: 0; + width: 0; + height: 0; + opacity: 0; + filter: alpha(opacity=0); } /* BEGIN: PopupNotification alerts */ - .alert-center { - position: fixed; - padding: 20px; - top: 50%; - left: 50%; - z-index: 2000; - overflow: auto; - text-align: center; - opacity: 0.9; - filter: alpha(opacity=90); + position: fixed; + padding: 20px; + top: 50%; + left: 50%; + z-index: 2000; + overflow: auto; + text-align: center; + opacity: 0.9; + filter: alpha(opacity=90); } .alert-center-small { - width: 200px; - margin: -80px 0 0 -100px; + width: 200px; + margin: -80px 0 0 -100px; } .alert-center-medium { - width: 400px; - margin: -80px 0 0 -200px; + width: 400px; + margin: -80px 0 0 -200px; } .alert-error.alert-center { - border-color: #B94A48; + border-color: #b94a48; } .alert-success.alert-center { - border-color: #468847; + border-color: #468847; } .alert-info.alert-center { - border-color: #3a87ad; + border-color: #3a87ad; } /* END: PopupNotification alerts */ .btn-group { - margin-right: 9px; + margin-right: 9px; } .btn-toolbar .btn-group { - margin-right: 4px; + margin-right: 4px; } .btn-group + .btn-group { - margin-left: 0; + margin-left: 0; } .input-prepend .add-on:first-child { - margin-left: 0; + margin-left: 0; } /* important for group of buttons with different colors like toggle switch */ @@ -1445,7 +1497,7 @@ ul.help > li { .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active { - z-index: inherit; + z-index: inherit; } #ErrorAlert, @@ -1456,155 +1508,157 @@ ul.help > li { #ConfigLoadError, #ConfigSaved, .unsupported-browser { - margin: 15px 20px 0 15px; + margin: 15px 20px 0 15px; } .confirm-menu { - text-align: left; - min-width: 10px; - right: 0; - left: auto; + text-align: left; + min-width: 10px; + right: 0; + left: auto; } .data-statistics { - width: 300px; + width: 300px; } .data-statistics-full { - width: 364px; + width: 364px; } .modal-center { - margin-top: 0; + margin-top: 0; } /*** STATISTICS */ .statistics__no-statistics { - width: 100%; - text-align: center; + width: 100%; + text-align: center; } .statistics__no-servers-container { - display: inline-block; - vertical-align: middle; - text-align: left; - width: auto; - max-width: 500px; - padding: 15px; - border: 1px solid #ccc; - border-radius: 5px; - margin: 0 auto; + display: inline-block; + vertical-align: middle; + text-align: left; + width: auto; + max-width: 500px; + padding: 15px; + border: 1px solid #ccc; + border-radius: 5px; + margin: 0 auto; } .text-large { - font-size: large; + font-size: large; } .text-larger { - font-size: larger; + font-size: larger; } #StatisticsTab { - max-width: 1280px; - padding: 5px; - margin: auto; + max-width: 1280px; + padding: 5px; + margin: auto; } .statistics__server-details { - height: 100%; - width: 350px; + height: 100%; + width: 350px; } .server-details__table { - margin-bottom: 15px; + margin-bottom: 15px; } #StatDialog_VolumesBlock { - text-align: center; + text-align: center; } #StatDialog_ChartBlock { - height: 300px; - width: 100%; - margin-top: 15px; - margin-bottom: 15px; + height: 300px; + width: 100%; + margin-top: 15px; + margin-bottom: 15px; } .statistics__chartblock { - margin-bottom: 10px; + margin-bottom: 10px; } .statistics__chartblock, .statistics__chart-block-spinner .spinner { - height: 250px; + height: 250px; } - #StatDialog_Chart { - height: 100%; - width: 670px; - width: 100%; + height: 100%; + width: 670px; + width: 100%; } .statistics__chart { - width: 100%; - height: 100%; + width: 100%; + height: 100%; } -#StatDialog_Tooltip, .statistics__tooltip { - float: right; - margin-top: -3px; - margin-bottom: -6px; - padding-right: 15px; - font-style: italic; +#StatDialog_Tooltip, +.statistics__tooltip { + float: right; + margin-top: -3px; + margin-bottom: -6px; + padding-right: 15px; + font-style: italic; } .statistics__chart-block-spinner { - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; } .statistics__chart-block-spinner .spinner { - font-size: 100px; - height: 250px; + font-size: 100px; + height: 250px; } .stat-size { - font-weight: bold; + font-weight: bold; } #StatDialog_TooltipSum { - font-weight: bold; + font-weight: bold; } -#StatDialog_CountersBlock, .statistics__counter-block { - padding-right: 20px; +#StatDialog_CountersBlock, +.statistics__counter-block { + padding-right: 20px; } -#StatDialog_Counters, .statistics__counters { - margin-top: 8px; - text-align: center; +#StatDialog_Counters, +.statistics__counters { + margin-top: 8px; + text-align: center; } #StatDialog_Counters .span3, .statistics__counters .span3 { - min-height: 0; + min-height: 0; } #StatDialog hr { - margin: 5px 10px; + margin: 5px 10px; } /*** CONFIG PAGE */ .config__container { - display: flex; - overflow: hidden; + display: flex; + overflow: hidden; } .config-loading-alert { - margin-top: 15px; + margin-top: 15px; } #DownloadsTab, @@ -1612,105 +1666,105 @@ ul.help > li { #StatisticsTab, #MessagesTab, .config__container { - height: calc(100vh - 55px); + height: calc(100vh - 55px); + padding-top: 15px; } .config__container { - padding-top: 12px; + padding-top: 12px; } #DownloadsTab, #HistoryTab, #StatisticsTab, #MessagesTab { - overflow: auto; - padding-top: 15px; + overflow: auto; } .config__aside { - flex-basis: 320px; - overflow: auto; + flex-basis: 320px; + overflow: auto; } .config__main { - max-width: 960px; - width: 100%; - padding: 0 15px; - overflow: auto; + max-width: 960px; + width: 100%; + padding: 0 15px; + overflow: auto; } @media (max-width: 1041px) { - .container-fluid { - padding: 0 10px; - } + .container-fluid { + padding: 0 10px; + } - #DownloadsTab, - #HistoryTab, - #StatisticsTab, - #MessagesTab, - .config__container { - height: calc(100vh - 90px); - padding-top: 7px; - } + #DownloadsTab, + #HistoryTab, + #StatisticsTab, + #MessagesTab, + .config__container { + height: calc(100vh - 90px); + padding-top: 7px; + } } @media (max-width: 960px) { - .container-fluid { - padding: 0 10px; - } + .container-fluid { + padding: 0 10px; + } } @media (max-width: 768px) { - body { - overflow: auto; - } - - .container-fluid { - padding: 0 5px; - } - - #DownloadsTab, - #HistoryTab, - #StatisticsTab, - #MessagesTab, - .config__container { - height: 100%; - } - - #DownloadsTab, - #HistoryTab, - #StatisticsTab, - #MessagesTab { - overflow: auto; - } - - .config__container { - display: flex; - flex-direction: column; - gap: 0px; - overflow-y: auto; - overflow-x: hidden; - padding-top: 0px; - } - - .config__aside { - min-width: 100%; - flex-basis: auto; - overflow-x: hidden; - } - - .config__main { - height: auto; - width: auto; - padding: 0 5px; - overflow-x: auto; - } + body { + overflow: auto; + } + + .container-fluid { + padding: 0 5px; + } + + #DownloadsTab, + #HistoryTab, + #StatisticsTab, + #MessagesTab, + .config__container { + height: 100%; + } + + #DownloadsTab, + #HistoryTab, + #StatisticsTab, + #MessagesTab { + overflow: auto; + } + + .config__container { + display: flex; + flex-direction: column; + gap: 0px; + overflow-y: auto; + overflow-x: hidden; + padding-top: 0px; + } + + .config__aside { + min-width: 100%; + flex-basis: auto; + overflow-x: hidden; + } + + .config__main { + height: auto; + width: auto; + padding: 0 5px; + overflow-x: auto; + } } @media (min-width: 1280px) { - .config__container { - justify-content: center; - } + .config__container { + justify-content: center; + } } .visible-tablet, @@ -1743,176 +1797,200 @@ ul.help > li { } #ConfigNav.nav-list.long-list a { - padding-top: 3px; - padding-bottom: 3px; + padding-top: 3px; + padding-bottom: 3px; } #ConfigNav.nav .nav-header { - font-size: 12px; + font-size: 12px; } .phone #ConfigNav { - display: flex; - flex-direction: column; - width: 100%; + display: flex; + flex-direction: column; + width: 100%; } #ConfigTitle { - margin-top: 15px; - margin-right: 15px; - font-size: 16px; - font-weight: bold; + margin-top: 15px; + margin-right: 15px; + font-size: 16px; + font-weight: bold; } .config-header .btn-group { - margin-right: 0; + margin-right: 0; } #ConfigContent p.help-block { - margin-top: 6px; - line-height: 16px; + margin-top: 6px; + line-height: 16px; } #ConfigContent.hide-help-block p.help-block { - display: none; + display: none; } #ConfigContent .control-label { - font-weight: bold; + font-weight: bold; } #ConfigContent select { - width: inherit; + width: inherit; } #ConfigContent .editnumeric { - width: 70px; + width: 70px; } #ConfigContent .editlarge { - width: 95%; + width: 95%; } #ConfigContent .editsmall { - width: 150px; + width: 150px; } #ConfigContent table.editor { - width: 97%; + width: 97%; } #ConfigContent table.editor td:first-child { - width: 100%; - padding-right:15px; + width: 100%; + padding-right: 15px; } #ConfigContent table.editor input { - width: 100%; + width: 100%; } .ConfigFooter hr { - margin: 6px 0 15px; + margin: 6px 0 15px; } div.ConfigFooter { - padding-bottom: 15px; + padding-bottom: 15px; } #ConfigContent hr { - margin: 15px 0; + margin: 15px 0; } #ConfigContent.hide-help-block .config-settitle { - margin-bottom: 15px; + margin-bottom: 15px; } #ConfigContent.hide-help-block div.control-group, #ConfigContent.hide-help-block div.control-group.multiset { - border-bottom: none; - margin-bottom: 0px; - padding-bottom: 12px; + border-bottom: none; + margin-bottom: 0px; + padding-bottom: 12px; } div.control-group.last-group { - margin-bottom: 0; + margin-bottom: 0; } #ConfigContent div.control-group.last-group, #ConfigContent.search div.control-group.last-group.multiset { - border-bottom: none; + border-bottom: none; } #ConfigContent div.control-group.multiset { - border-bottom: none; - margin-bottom: 12px; - padding-bottom: 8px; + border-bottom: none; + margin-bottom: 12px; + padding-bottom: 8px; } #ConfigContent .control-label { - width: 170px; + width: 170px; } #ConfigContent .form-horizontal .controls { - margin-left: 180px; + margin-left: 180px; } .btn-switch .btn { - text-transform: capitalize; + text-transform: capitalize; } .option { - font-weight: bold; - font-style:italic; - color: inherit; + font-weight: bold; + font-style: italic; + color: inherit; } .option-name, .option-name:focus, .option-name:hover { - color: inherit; - outline: 0; - cursor: inherit; - text-decoration: none; + color: inherit; + outline: 0; + cursor: inherit; + text-decoration: none; +} + +.option__check-section { + display: flex; + flex-direction: column; + width: 96%; + margin-top: 7px; +} + +.option-alert { + margin-bottom: 0; + font-size: larger; + padding: 2px 3px; +} + +.option-alert__icon { + font-size: larger; + margin-right: 3px; } .search .option-name, .search .option-name:focus { - cursor: pointer; + cursor: pointer; } div.multiset-toolbar button { - margin-right: 15px; + margin-right: 15px; } #ScriptListDialog_ScriptTable td:nth-child(2) { - padding-right: 100px; + padding-right: 100px; } #ScriptListDialog_ScriptTable .btn-row-order-block { - float: right; - width: 100px; - margin-right: -115px; - display: block; + float: right; + width: 100px; + margin-right: -115px; + display: block; } #ScriptListDialog_ScriptTable .btn-row-order { - float: none; - width: 20px; - display: none; + float: none; + width: 20px; + display: none; } #ScriptListDialog_ScriptTable tr:hover .btn-row-order { - display: inline-block; - cursor: pointer; + display: inline-block; + cursor: pointer; } .mv-item-arrow-btn--disabled, #ScriptListDialog_ScriptTable tbody > tr:first-child .btn-row-order:first-child, #ScriptListDialog_ScriptTable tbody > tr:last-child .btn-row-order:last-child, -#ScriptListDialog_ScriptTable tbody > tr:first-child .btn-row-order:nth-child(2), -#ScriptListDialog_ScriptTable tbody > tr:last-child .btn-row-order:nth-child(3) { - opacity: 0.4; - pointer-events: none; +#ScriptListDialog_ScriptTable + tbody + > tr:first-child + .btn-row-order:nth-child(2), +#ScriptListDialog_ScriptTable + tbody + > tr:last-child + .btn-row-order:nth-child(3) { + opacity: 0.4; + pointer-events: none; } /* UPDATE DIALOG */ @@ -1920,212 +1998,211 @@ div.multiset-toolbar button { #UpdateDialog_InstallStable, #UpdateDialog_InstallTesting, #UpdateDialog_InstallDevel { - margin-top: 5px; + margin-top: 5px; } .table .update-row-name { - font-weight: bold; - padding-top: 14px; + font-weight: bold; + padding-top: 14px; } .log-dialog { - width: 640px; - margin-left: -320px; + width: 640px; + margin-left: -320px; } .log-dialog .modal-body { - min-height: 280px; - position: relative; + min-height: 280px; + position: relative; } .update-log-error, .script-log-error { - color: #dd0000; + color: #dd0000; } .script-log-success { - color: #00dd00; + color: #00dd00; } /* FEED FILTER DIALOG */ #FeedFilterDialog_FilterNumbers { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } #FeedFilterDialog_FilterInput { - width: 100%; - height: 100%; - margin: 0; - padding: 0; - border: 0; - resize: none; - outline: none; - border: none; - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border: 0; + resize: none; + outline: none; + border: none; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; } #FeedFilterDialog_PreviewBlock { - position: absolute; - left: 325px; - right: 0; - bottom: 0; - height: auto; - width: auto; - padding-right: 15px; - margin-bottom: 12px; + position: absolute; + left: 325px; + right: 0; + bottom: 0; + height: auto; + width: auto; + padding-right: 15px; + margin-bottom: 12px; } #FeedFilterDialog_Splitter { - position: absolute; - left: 319px; - width: 5px; - padding: 0; - right: 5px; - bottom: 0; - height: auto; - cursor: col-resize; + position: absolute; + left: 319px; + width: 5px; + padding: 0; + right: 5px; + bottom: 0; + height: auto; + cursor: col-resize; } .phone #FeedFilterDialog_FilterBlock, .phone #FeedFilterDialog_PreviewBlock, .phone #FeedFilterDialog_FilterClient { - position: static; - top: inherit; - left: 0; - width: inherit; - padding-right: 0; + position: static; + top: inherit; + left: 0; + width: inherit; + padding-right: 0; } .phone #FeedFilterDialog_FilterInput { - height: 380px; + height: 380px; } .filter-rule { - cursor: pointer; + cursor: pointer; } /* DRAG-N-DROP */ .phone #TableDragBox .badge { - font-size: 22px; - top: -20px; - padding: 6px 10px; - border-radius: 12px; + font-size: 22px; + top: -20px; + padding: 6px 10px; + border-radius: 12px; } #TableDragBox .table-bordered { - border: none; + border: none; } /* drag grip */ table.table-drag > tbody > tr:hover > td:first-child, #TableDragBox table.table-drag > tbody > tr > td:first-child { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAECAYAAACk7+45AAAAFUlEQVQI12O8e/fufwYGBgYmBpwAAHhsA5rVjhOVAAAAAElFTkSuQmCC); - background-position: 1px 1px; - background-repeat: repeat-y; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAECAYAAACk7+45AAAAFUlEQVQI12O8e/fufwYGBgYmBpwAAHhsA5rVjhOVAAAAAElFTkSuQmCC); + background-position: 1px 1px; + background-repeat: repeat-y; } .phone table.table-drag > tbody > tr:hover > td:first-child, .phone #TableDragBox table.table-drag > tbody > tr > td:first-child { - background: none; - cursor: default; + background: none; + cursor: default; } table.table-drag > tbody > tr > td:first-child { - cursor: move; - cursor: -moz-grab; - cursor: -webkit-grab; + cursor: move; + cursor: -moz-grab; + cursor: -webkit-grab; } table.table-drag > tbody > tr > td:first-child > div.check { - cursor: default; + cursor: default; } #TableDragBox table.table-drag > tbody > tr > td:first-child, #TableDragBox table.table-drag > tbody > tr > td:first-child > div.check { - cursor: move; - cursor: -moz-grabbing; - cursor: -webkit-grabbing; + cursor: move; + cursor: -moz-grabbing; + cursor: -webkit-grabbing; } tr.drag-source > td > * { - visibility: hidden !important; + visibility: hidden !important; } - /****************************************************************************/ /* SMARTPHONE THEME */ body.phone { - margin-top: 0; + margin-top: 0; } .phone .navbar-fixed-top { - position: static; + position: static; } .phone #PlayBlock { - width: 75px; + width: 75px; } .phone #PlayCaretButton { - width: 25px; - margin-left: 10px; + width: 25px; + margin-left: 10px; } .phone #InfoBlock { - width: 155px; - height: 42px; - margin-right: 0; + width: 155px; + height: 42px; + margin-right: 0; } .phone #InfoBlock div { - display: inline-block; - margin-left: 5px; - margin-top: 5px; - font-size: 14px; - line-height: 32px; - height: inherit; + display: inline-block; + margin-left: 5px; + margin-top: 5px; + font-size: 14px; + line-height: 32px; + height: inherit; } /* GENERAL CLASSES */ .phone-only, .btn-group.phone-only { - display: none; + display: none; } .phone .phone-hide, .phone .btn-group.phone-hide { - display: none; + display: none; } .phone .phone-only { - display: block; + display: block; } .phone .phone-only.inline { - display: inline-block; + display: inline-block; } /* FONTS */ body.phone, .phone p, -.phone .form-horizontal .help-block , +.phone .form-horizontal .help-block, .phone h4 { - font-size: 18px; - line-height: 22px; + font-size: 18px; + line-height: 22px; } .phone table td { - line-height: 26px; - overflow-x: auto; + line-height: 26px; + overflow-x: auto; } .phone select, @@ -2136,18 +2213,18 @@ body.phone, .phone .btn, .phone .btn-toolbar .btn, .phone .uneditable-input { - font-size: 18px; - line-height: 24px; - height: inherit; + font-size: 18px; + line-height: 24px; + height: inherit; } .phone .controls .label-status { - line-height: 28px; + line-height: 28px; } .phone .menu-header { - font-size: 18px; - line-height: 24px; + font-size: 18px; + line-height: 24px; } /* SECTION MARGINGS */ @@ -2155,608 +2232,607 @@ body.phone, .phone .toolbox-top, .phone .toolbox-info, .phone #ConfigTabData { - padding-left: 5px; - padding-right: 5px; + padding-left: 5px; + padding-right: 5px; } .phone #ErrorAlert, .phone #FirstUpdateInfo, .phone #ConfigReloadInfo, -.phone #DownloadQueueEmpty { - margin-left: 5px; - margin-right: 5px; +.phone #DownloadQueueEmpty { + margin-left: 5px; + margin-right: 5px; } .phone #FirstUpdateInfo, .phone #ConfigReloadInfo { - margin-top: 5px; + margin-top: 5px; } .phone #MainContent { - padding-left: 0px; - padding-right: 0px; + padding-left: 0px; + padding-right: 0px; } .phone .section-toolbar { - margin-bottom: 0; + margin-bottom: 0; } .phone .toolbox-top { - margin-top: 0; - margin-bottom: 8px; + margin-top: 0; + margin-bottom: 8px; } /* NAVBAR */ .phone .navbar-container { - padding-left: 5px; - padding-right: 5px; + padding-left: 5px; + padding-right: 5px; } .phone .navbar-inner .btn-toolbar { - margin: 6px 0 0; + margin: 6px 0 0; } .phone .nav, .phone ul.nav { - width: 100%; - display: flex; + width: 100%; + display: flex; } .phone ul.nav > li { - text-align: center; - width: 100%; + text-align: center; + width: 100%; } .phone .menu-header { - text-align: left; + text-align: left; } .phone .navbar .nav > li > a { - padding: 4px 4px 6px; + padding: 4px 4px 6px; } .phone .navbar .nav > li.active > #DownloadsTabLink > i { - /* icon-downloads (black) */ - background-position: -144px -48px; + /* icon-downloads (black) */ + background-position: -144px -48px; } .phone .navbar .nav > li.active > #HistoryTabLink > i { - /* icon-history (black) */ - background-position: -240px -48px; + /* icon-history (black) */ + background-position: -240px -48px; } .phone .navbar .nav > li.active > #MessagesTabLink > i { - /* icon-messages (black) */ - background-position: -176px -16px; + /* icon-messages (black) */ + background-position: -176px -16px; } .phone .navbar .nav > li.active > #ConfigTabLink > i { - /* icon-settings (black) */ - background-position: -112px -48px; + /* icon-settings (black) */ + background-position: -112px -48px; } .phone .navbar .btn-toolbar .btn { - padding: 3px; - min-width: 40px; + padding: 3px; + min-width: 40px; } .phone #RefreshBlockPhone { - padding-left: 5px; + padding-left: 5px; } .phone .navbar-search { - width: 90%; + width: 90%; } .phone .navbar-search .search-query, .phone .navbar-search .search-query:focus, .phone .navbar-search .search-query.focused { - width: 85%; - padding: 4px 28px 4px 28px; - font-size: 16px; - -webkit-border-radius: 16px; - -moz-border-radius: 16px; - border-radius: 16px; - margin-bottom: 5px; - margin-top: 1px; - border: 0; + width: 85%; + padding: 4px 28px 4px 28px; + font-size: 16px; + -webkit-border-radius: 16px; + -moz-border-radius: 16px; + border-radius: 16px; + margin-bottom: 5px; + margin-top: 1px; + border: 0; } .phone .search-clear { - top: 9px; + top: 9px; } /* DATATABLE */ -.phone table.datatable , +.phone table.datatable, .phone table.datatable > tbody, .phone table.datatable > tbody > tr, .phone table.datatable > tbody > tr > td { - display: block; + display: block; } .phone table.datatable > thead { - display: none; + display: none; } .phone table.datatable > tbody > tr > td { - width: inherit; - height: inherit; + width: inherit; + height: inherit; } -.phone .datatable td { - border: 0; +.phone .datatable td { + border: 0; } .phone table.datatable > tbody > tr > td:first-child { - border-top: 1px solid #DDDDDD; + border-top: 1px solid #dddddd; } .phone table.datatable > tbody > tr:last-child > td:last-child { - border-bottom: 1px solid #DDDDDD; + border-bottom: 1px solid #dddddd; } .phone table.datatable > tbody > tr > td:first-child { - padding-top: 10px; + padding-top: 10px; } .phone table.datatable > tbody > tr > td:last-child { - padding-bottom: 10px; + padding-bottom: 10px; } .phone table.table-check > tbody > tr > td { - padding-left: 60px; + padding-left: 60px; } /* CHECKMARKS IN DATATABLE */ .phone div.check { - margin-top: -2px; - margin-left: -48px; - width: 30px; - height: 30px; - display: block; - position: absolute; - border-width: 2px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; + margin-top: -2px; + margin-left: -48px; + width: 30px; + height: 30px; + display: block; + position: absolute; + border-width: 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; } -.phone table.table-cancheck tr.checked div.img-check { - /* icon-OK */ - background-position: -74px -10px; +.phone table.table-cancheck tr.checked div.img-check { + /* icon-OK */ + background-position: -74px -10px; } /* SPECIAL TABLE STYLES */ .phone .row-title { - font-weight: bold; + font-weight: bold; } .phone .progress-block { - margin-top: -2px; + margin-top: -2px; } .phone .downloads-progresslabel { - margin-top: -8px; - margin-bottom: 10px; + margin-top: -8px; + margin-bottom: 10px; } .phone .label-inline { - display: inline; + display: inline; } /* CONTROLS AROUND DATATABLE */ .phone .records-label { - display: none; + display: none; } /* PAGER */ .phone .toolbox-info div { - float: none; - width: 100%; - text-align: center; - display: block; - margin-top: 5px; + float: none; + width: 100%; + text-align: center; + display: block; + margin-top: 5px; } .phone div.toolbox-length { - margin-top: 10px; + margin-top: 10px; } .phone .modal-toolbox div.toolbox-length { - margin-top: 0; + margin-top: 0; } .phone .pagination { - height: auto; - width: 100%; - text-align: center; - margin-top: 10px; + height: auto; + width: 100%; + text-align: center; + margin-top: 10px; } .phone .pagination a { - padding: 0 10px; - line-height: 34px; + padding: 0 10px; + line-height: 34px; } /* STATUS LABELS */ .phone .label { - font-size: 14px; - line-height: 18px; - vertical-align: middle; + font-size: 14px; + line-height: 18px; + vertical-align: middle; } .phone .datatable .label { - line-height: 21px; + line-height: 21px; } /* PROGRESS */ .phone .progress-block { - font-size: 16px; - width: 100%; + font-size: 16px; + width: 100%; } .phone .progress, .phone .progress .bar { - height: 24px; + height: 24px; } .phone .bar-text-left, .phone .bar-text-center, .phone .bar-text-right { - padding-top: 1px; - margin-top: 0px; + padding-top: 1px; + margin-top: 0px; } /* STATISTICS TABLE */ .phone #StatisticsTab table { - width: 100%; + width: 100%; } .phone #StatisticsTab td:first-child { - font-weight: bold; + font-weight: bold; } .phone #StatisticsTab td { - text-align: left; + text-align: left; } /* MENUS */ .phone .dropdown-menu a { - padding: 7px 15px; + padding: 7px 15px; } .phone .dropdown-toggle { - position: static; + position: static; } .phone #FilterMenu { - left: inherit; - right: inherit; - margin-top: inherit; - width: inherit; + left: inherit; + right: inherit; + margin-top: inherit; + width: inherit; } /* hide arrow */ .phone .navbar .dropdown-menu:before, .phone .navbar .dropdown-menu:after { - display: none; + display: none; } -.phone #RefreshMenu { - min-width: 200px; +.phone #RefreshMenu { + min-width: 200px; } .phone #SettingsMenu { - min-width: 230px; + min-width: 230px; } .phone #PlayMenu { - min-width: 250px; + min-width: 250px; } .phone #ToolbarOptMenu { - min-width: 270px; + min-width: 270px; } .phone .dropdown-context:hover { - cursor: inherit; + cursor: inherit; } .phone .dropdown-context:hover::after { - display: none; + display: none; } /* TOOLBAR AND INPUTS */ .phone .btn-toolbar .btn, .phone .btn-toolbar input { - padding: 6px; - min-width: 45px; + padding: 6px; + min-width: 45px; } .phone .btn-toolbar .btn-group { - margin-right: 0; + margin-right: 0; } .phone .btn-toolbar .input-prepend .add-on, .phone .btn-toolbar .input-append .add-on { - padding: 6px; - min-width: 45px; + padding: 6px; + min-width: 45px; } .phone input, .phone textarea, .phone .uneditable-input { - height: 24px; + height: 24px; } .phone input.btn { - height: inherit; + height: inherit; } .phone .input-prepend .add-on, .phone .input-append .add-on { - height: 24px; - line-height: 22px; - min-width: 16px; + height: 24px; + line-height: 22px; + min-width: 16px; } .phone select { - height: auto; + height: auto; } .phone [class^="icon-"] { - line-height: 24px; - vertical-align: baseline; + line-height: 24px; + vertical-align: baseline; } .phone .caret { - line-height: 24px; - margin-top: -2px; - vertical-align: middle; + line-height: 24px; + margin-top: -2px; + vertical-align: middle; } /*** MODALS */ .phone .modal-footer > .btn, .phone .modal-footer > .btn-group { - display: block; - float: none; - width: 100%; - margin: 10px auto; + display: block; + float: none; + width: 100%; + margin: 10px auto; } .phone .modal-footer .btn { - padding: 7px 0; + padding: 7px 0; } .phone .modal-footer > .btn-group > .btn { - width: 100%; - margin: 0; + width: 100%; + margin: 0; } -.phone .modal-footer { - padding-top: 5px; - padding-bottom: 5px; +.phone .modal-footer { + padding-top: 5px; + padding-bottom: 5px; } .phone .modal-footer .confirm-menu { - text-align: center; - right: inherit; - left: inherit; - float: none; - width: 100%; + text-align: center; + right: inherit; + left: inherit; + float: none; + width: 100%; } .phone .modal-footer .confirm-menu .menu-header { - text-align: center; + text-align: center; } .phone .modal-padded .modal-body, .phone .modal-padded-small .modal-body { - padding-left: 15px; - padding-right: 15px; + padding-left: 15px; + padding-right: 15px; } .phone .modal-tab-padded, .phone .modal-tab-padded-small { - padding-left: 0; - padding-right: 0; + padding-left: 0; + padding-right: 0; } .phone .modal-max .modal-body { - position: static; + position: static; } .phone .modal-max .modal-footer { - position: static; + position: static; } .phone .modal-max .modal-inner-scroll { - position: static; - top: inherit; - left: 0; - padding-right: 0; + position: static; + top: inherit; + left: 0; + padding-right: 0; } .phone .data-statistics, .phone .data-statistics-wide, .phone .data-statistics-full { - width: 100%; + width: 100%; } .phone .btn-caption { - display: none; + display: none; } .btn-caption:not(.phone) { - vertical-align: text-bottom; + vertical-align: text-bottom; } .phone div.toolbox-length select { - height: 36px; + height: 36px; } .phone .navbar .btn-toolbar { - position: relative; + position: relative; } .phone #ConfigNav.nav-list a { - font-size: 18px; + font-size: 18px; } .phone #ConfigNav.nav .nav-header { - font-size: 20px; + font-size: 20px; } .phone #ConfigContent .config-header { - font-size: 20px; + font-size: 20px; } .phone .config-settitle { - font-size: 18px; + font-size: 18px; } .phone #TableDragTip { - font-size: 18px; + font-size: 18px; } /*** STATISTICS DIALOG */ .phone #StatDialog_Tooltip { - margin-top: 3px; - margin-bottom: 3px; + margin-top: 3px; + margin-bottom: 3px; } .phone #StatDialog hr { - margin-top: 30px; + margin-top: 30px; } input[type="date"] { - width: auto; + width: auto; } /* MEDIA SMALL SCREENS */ @media (max-width: 700px) { - #ConfigContent [class*="span"] { - display: block; - float: none; - width: auto; - margin-left: 0; - } + #ConfigContent [class*="span"] { + display: block; + float: none; + width: auto; + margin-left: 0; + } - .modal-large { - width: 600px; - margin-left:-300px; - } + .modal-large { + width: 600px; + margin-left: -300px; + } } @media (max-width: 568px) { + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + + [class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: auto; + margin-left: 0; + } - input[type="checkbox"], - input[type="radio"] { - border: 1px solid #ccc; - } - - [class*="span"], - .row-fluid [class*="span"] { - display: block; - float: none; - width: auto; - margin-left: 0; - } - - .form-horizontal .control-group > label { - float: none; - width: auto; - padding-top: 0; - text-align: left; - } - - .form-horizontal .controls, - #ConfigContent .form-horizontal .controls { - margin-left: 0; - } - - .form-horizontal .control-list { - padding-top: 0; - } - - .form-horizontal .form-actions { - padding-right: 10px; - padding-left: 10px; - } - - .modal { - position: absolute; - top: 0px; - right: 0px; - left: 0px; - width: auto; - margin: 0; - } - - .modal.fade.in { - top: auto; - } - - .modal .input-xlarge , - .modal .input-xxlarge, - .modal.modal-padded .input-xxlarge, - .uneditable-mulitline-input { - width: 95%; - } - - .modal-body, - .modal.no-footer .modal-body { - max-height: none; - } - - .modal-center { - right: 20px; - left: 20px; - } - - .alert-center-small, - .alert-center-medium { - right: 20px; - left: 20px; - width: auto; - margin: -10% 0 0; - } + .form-horizontal .control-group > label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + + .form-horizontal .controls, + #ConfigContent .form-horizontal .controls { + margin-left: 0; + } + + .form-horizontal .control-list { + padding-top: 0; + } + + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + + .modal { + position: absolute; + top: 0px; + right: 0px; + left: 0px; + width: auto; + margin: 0; + } + + .modal.fade.in { + top: auto; + } + + .modal .input-xlarge, + .modal .input-xxlarge, + .modal.modal-padded .input-xxlarge, + .uneditable-mulitline-input { + width: 95%; + } + + .modal-body, + .modal.no-footer .modal-body { + max-height: none; + } + + .modal-center { + right: 20px; + left: 20px; + } + + .alert-center-small, + .alert-center-medium { + right: 20px; + left: 20px; + width: auto; + margin: -10% 0 0; + } } @media (max-width: 600px) { - #SearchBlock { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - } + #SearchBlock { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + } } @media (max-width: 560px) { - .phone .search-clear { - right: 25px; - } + .phone .search-clear { + right: 25px; + } - .list-item--nested a { - padding-left: 0px!important; - } + .list-item--nested a { + padding-left: 0px !important; + } } @media (max-width: 450px) { - .phone .search-clear { - right: 9px; - } + .phone .search-clear { + right: 9px; + } } @media (max-width: 549px) { - #Logo { - display: none; - } + #Logo { + display: none; + } } @media (max-width: 480px) { - .dialog-transmit { - display: none; - } + .dialog-transmit { + display: none; + } } /* END: MEDIA SMALL SCREENS */ diff --git a/webui/system-health.js b/webui/system-health.js new file mode 100644 index 00000000..d7ba0042 --- /dev/null +++ b/webui/system-health.js @@ -0,0 +1,405 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +function SectionHealth(issues_, options_, sectionName) { + var name = sectionName; + var issues = issues_; + var options = options_; + var health = { + info: [], + warnings: [], + errors: [], + } + + init(issues_, options_); + + this.getIssues = function () { + return issues; + } + + this.getErrorsCount = function () { + return health.errors.length; + } + + this.getWarningsCount = function () { + return health.warnings.length; + } + + this.getInfoCount = function () { + return health.info.length; + } + + this.getCheck = function (name) { + return options[name]; + } + + this.getName = function () { + return name; + } + + function init(issues_, options_) { + Object.values(options_).forEach(function (opt) { + if (opt.Severity === "Info") { + health.info.push(opt); + } + else if (opt.Severity === "Warning") { + health.warnings.push(opt); + } + else if (opt.Severity === "Error") { + health.errors.push(opt); + } + }); + issues_.forEach(function (issue) { + if (issue.Severity === "Info") { + health.info.push({name: sectionName, message: issue.Message }); + } + else if (issue.Severity === "Warning") { + health.warnings.push({name: sectionName, message: issue.Message }); + } + else if (issue.Severity === "Error") { + health.errors.push({name: sectionName, message: issue.Message }); + } + }); + } +} + +function HealthReport() { + var sections = {}; + var infoCount = 0; + var warningsCount = 0; + var errorsCount = 0; + + this.getInfoCount = function () { + return infoCount; + } + + this.getWarningsCount = function () { + return warningsCount; + } + + this.getErrorsCount = function () { + return errorsCount; + } + + this.getSection = function (name) { + return sections[name]; + } + + this.addSection = function (section) { + sections[section.getName()] = section; + } + + this.computeCounters = function () { + infoCount = 0; + warningsCount = 0; + errorsCount = 0; + + Object.values(sections).forEach(function (section) { + infoCount += section.getInfoCount(); + warningsCount += section.getWarningsCount(); + errorsCount += section.getErrorsCount(); + }); + } +} + +var SystemHealth = (new function ($) { + 'use strict'; + + var SECTION_NAMES = { + Paths: "PATHS", + NewsServers: "NEWS-SERVERS", + Security: "SECURITY", + Categories: "CATEGORIES", + Feeds: "RSS_FEEDS", + IncomingNzbs: "INCOMING_NZBS", + DownloadQueue: "DOWNLOAD_QUEUE", + Connection: "CONNECTION", + Logging: "LOGGING", + CheckAndRepair: "CHECK_AND_REPAIR", + Unpack: "UNPACK", + Scheduler: "SCHEDULER", + ExtensionScripts: "EXTENSION_SCRIPTS" + }; + + var $appHealthBadge; + var $ConfigTitleStatus; + var $SystemInfo_Health; + var $SystemInfoNavBtnBadge; + var sectionsReport = {}; + var alertsReport = {}; + this.SEVERITY = { + OK: 0, + INFO: 1, + WARNING: 2, + ERROR: 3, + } + var SEVERITY_STYLE = { + INFO: "success", + WARNING: "warning", + ERROR: "important", + } + + this.init = function () { + $appHealthBadge = $("#ConfigTabAppHealthBadge"); + $ConfigTitleStatus = $('#ConfigTitleStatus'); + $SystemInfoNavBtnBadge = $('#SystemInfoNavBtnBadge'); + $SystemInfo_Health = $('#SystemInfo_Health'); + RPC.call('systemhealth', [], + function (health) { + //console.log(health) + alertsReport = makeAlertsReport(health); + sectionsReport = makeHealthReport(health["Sections"]); + redrawGlobalBadges(alertsReport, sectionsReport); + redraw(alertsReport); + }, + function(err) + { + console.error(err); + } + ); + } + + function redraw(alertsReport) { + var $container = $("#SystemInfo_Health"); + var $mainWrapper = $("#SystemInfoHealthSection"); + var accordionId = "healthAccordion"; + var sections = [ + { + id: 'collapseErrors', + label: 'Errors', + data: alertsReport.errors, + cssClass: 'txt-error', + icon: 'error' + }, + { + id: 'collapseWarnings', + label: 'Warnings', + data: alertsReport.warnings, + cssClass: 'txt-warning', + icon: 'warning' + }, + { + id: 'collapseInfo', + label: 'Info', + data: alertsReport.info, + cssClass: 'txt-success', + icon: 'info' + } + ]; + + $container.empty(); + + var hasContent = false; + var $accordion = $('
    '); + + sections.forEach(function (section) { + if (!section.data || section.data.length === 0) { + return; + } + + hasContent = true; + + var $group = $('
    '); + var $heading = $('
    '); + var $toggle = $(''); + $toggle.attr('data-parent', '#' + accordionId); + $toggle.attr('href', '#' + section.id); + + $toggle.html( + '' + section.icon + ' ' + + '' + section.label + ' (' + section.data.length + ')' + ); + + $heading.append($toggle); + $group.append($heading); + + var $body = $('
    '); + var $inner = $('
    '); + + section.data.forEach(function (alert) { + var $link = $(''); + $link.addClass(section.cssClass); + + $link.on('click', function (e) { + e.preventDefault(); + var $mockBtn = $('' + alert.name + ''); + Config.scrollToOption(e, $mockBtn); + Config.navigateTo(alert.name); + }); + + $link.html('' + alert.message + ''); + $inner.append($link); + }); + + $body.append($inner); + $group.append($body); + $accordion.append($group); + }); + + if (!hasContent) { + $mainWrapper.hide(); + } else { + $mainWrapper.show(); + $container.append($accordion); + } + } + + this.makeBadges = function (section) { + if (!section) return null; + + var wrapper = $(''); + var $infoBadge = $(''); + var $warningsBadge = $(''); + var $errorsBadge = $(''); + var infoCount = section.getInfoCount(); + var warningsCount = section.getWarningsCount(); + var errorsCount = section.getErrorsCount(); + toggleBadgeVisibility($infoBadge, infoCount, SEVERITY_STYLE.INFO); + toggleBadgeVisibility($warningsBadge, warningsCount, SEVERITY_STYLE.WARNING); + toggleBadgeVisibility($errorsBadge, errorsCount, SEVERITY_STYLE.ERROR); + wrapper.append($errorsBadge, $warningsBadge, $warningsBadge); + + return wrapper; + } + + this.getSection = function (name) { + return sectionsReport.getSection(name); + } + + this.getCheck = function (section, name) { + if (!section) + return null; + + return section.getCheck(name); + } + + function redrawGlobalBadges(alertsReport, sectionsReport) { + $SystemInfoNavBtnBadge.hide(); + + var infoCount = sectionsReport.getInfoCount() + (alertsReport.getInfoCount ? alertsReport.getInfoCount() : 0); + var warningsCount = sectionsReport.getWarningsCount() + (alertsReport.getWarningsCount ? alertsReport.getWarningsCount() : 0); + var errorsCount = sectionsReport.getErrorsCount() + (alertsReport.getErrorsCount ? alertsReport.getErrorsCount() : 0); + //toggleBadgeVisibility($appHealthInfoBadge, infoCount, SEVERITY_STYLE.INFO); + if (errorsCount > 0) { + toggleGlobalBadgeVisibility($appHealthBadge, errorsCount, SEVERITY_STYLE.ERROR); + toggleGlobalBadgeVisibility($SystemInfoNavBtnBadge, errorsCount, SEVERITY_STYLE.ERROR); + return; + } + + // toggleGlobalBadgeVisibility($appHealthBadge, warningsCount, SEVERITY_STYLE.WARNING); + if (warningsCount > 0) { + toggleGlobalBadgeVisibility($SystemInfoNavBtnBadge, warningsCount, SEVERITY_STYLE.WARNING); + return; + } + + if (infoCount > 0) { + toggleGlobalBadgeVisibility($SystemInfoNavBtnBadge, infoCount, SEVERITY_STYLE.INFO); + return; + } + } + + function makeAlertsReport(health) { + var buckets = { + "Info": [], + "Warning": [], + "Error": [] + }; + + function add(severity, name, message) { + if (buckets[severity]) { + buckets[severity].push({ name: name, message: message }); + } + } + + if (health.Alerts) { + health.Alerts.forEach(function (alert) { + add(alert.Severity, alert.Source, alert.Message); + }); + } + + health.Sections.forEach(function (section) { + section.Issues.forEach(function (issue) { + add(issue.Severity, SECTION_NAMES[section.Name], issue.Message); + }); + + section.Options.forEach(function (option) { + add(option.Severity, option.Name, option.Message); + }); + + section.Subsections.forEach(function (sub) { + sub.Options.forEach(function (option) { + var fullName = sub.Name + "." + option.Name; + add(option.Severity, fullName, fullName + ': ' + option.Message); + }); + }); + }); + + return { + info: buckets["Info"], + warnings: buckets["Warning"], + errors: buckets["Error"] + }; + } + + function makeHealthReport(sections) { + var healthReport = new HealthReport(); + + sections.forEach(function (section) { + var optionsMap = {}; + var issues = section.Issues; + section.Options.forEach(function (option) { + optionsMap[option.Name] = { Message: option.Message, Severity: option.Severity }; + }); + + section.Subsections.forEach(function (sub) { + sub.Options.forEach(function (option) { + var key = (sub.Name ? (sub.Name + '.') : '') + option.Name; + optionsMap[key] = { Message: option.Message, Severity: option.Severity }; + }); + }); + healthReport.addSection(new SectionHealth(issues, optionsMap, SECTION_NAMES[section.Name])); + }); + + healthReport.computeCounters(); + + return healthReport; + } + + function toggleBadgeVisibility($badge, checks, severity) { + if (checks > 0) { + $badge.show(); + $badge.addClass("badge-" + severity) + $badge.text(checks); + } + else { + $badge.hide(); + } + } + + function toggleGlobalBadgeVisibility($badge, checks, severity) { + if (checks > 0) { + $badge.show(); + $badge.addClass("txt-" + severity.toLowerCase()); + } + else { + $badge.hide(); + } + } + +}(jQuery)); diff --git a/webui/system-info.js b/webui/system-info.js index dd2bb3cd..d221533c 100644 --- a/webui/system-info.js +++ b/webui/system-info.js @@ -148,6 +148,7 @@ var SystemInfo = (new function($) { this.id = "Config-SystemInfo"; + var $Container; var $SysInfo_OS; var $SysInfo_AppVersion; var $SysInfo_Uptime; @@ -241,6 +242,7 @@ var SystemInfo = (new function($) this.init = function() { + $Container = $('.config__main'); $SysInfo_OS = $('#SysInfo_OS'); $SysInfo_AppVersion = $('#SysInfo_AppVersion'); $SysInfo_Uptime = $('#SysInfo_Uptime'); @@ -388,6 +390,12 @@ var SystemInfo = (new function($) renderTools(sysInfo['Tools']); renderLibraries(sysInfo['Libraries']); renderNewsServers(Status.getStatus()['NewsServers']); + + scrollToTop(); + } + + function scrollToTop() { + $Container.animate({ scrollTop: 0 }, 'fast'); } function renderWriteBuffer(writeBufferKB)