Unraid Monitor for one-screen overview
This commit is contained in:
commit
e7c3d20c8e
13 changed files with 1545 additions and 0 deletions
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Build-Verzeichnisse
|
||||||
|
build/
|
||||||
|
build-*/
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# CMake-Artefakte
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
Makefile
|
||||||
|
*.cmake
|
||||||
|
!CMakeLists.txt
|
||||||
|
|
||||||
|
# Qt / moc / uic / rcc
|
||||||
|
moc_*.cpp
|
||||||
|
moc_*.h
|
||||||
|
*_autogen/
|
||||||
|
qrc_*.cpp
|
||||||
|
ui_*.h
|
||||||
|
*.qmake.stash
|
||||||
|
.qmake.cache
|
||||||
|
|
||||||
|
# Kompilate
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.so
|
||||||
|
*.a
|
||||||
|
*.exe
|
||||||
|
unraid-monitor
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.user
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Betriebssystem
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
19
CMakeLists.txt
Normal file
19
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(unraid-monitor VERSION 1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Widgets Network)
|
||||||
|
|
||||||
|
add_executable(unraid-monitor
|
||||||
|
src/main.cpp
|
||||||
|
src/MainWindow.cpp
|
||||||
|
src/UnraidClient.cpp
|
||||||
|
src/Cards.cpp
|
||||||
|
src/SettingsDialog.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(unraid-monitor PRIVATE src)
|
||||||
|
target_link_libraries(unraid-monitor PRIVATE Qt6::Widgets Qt6::Network)
|
||||||
504
src/Cards.cpp
Normal file
504
src/Cards.cpp
Normal file
|
|
@ -0,0 +1,504 @@
|
||||||
|
#include "Cards.h"
|
||||||
|
#include "Theme.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
// ---------- Hilfsfunktionen ----------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString bytesHuman(double bytes)
|
||||||
|
{
|
||||||
|
static const char *units[] = {"B", "KB", "MB", "GB", "TB", "PB"};
|
||||||
|
int i = 0;
|
||||||
|
while (bytes >= 1024.0 && i < 5) {
|
||||||
|
bytes /= 1024.0;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return QString::number(bytes, 'f', bytes >= 100 || i == 0 ? 0 : 1) + " " + units[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
QString kbHuman(qint64 kb)
|
||||||
|
{
|
||||||
|
return bytesHuman(static_cast<double>(kb) * 1024.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel *makeLabel(const QString &text, const char *color = nullptr, bool bold = false)
|
||||||
|
{
|
||||||
|
auto *l = new QLabel(text);
|
||||||
|
QString style;
|
||||||
|
if (color)
|
||||||
|
style += QString("color:%1;").arg(color);
|
||||||
|
if (bold)
|
||||||
|
style += "font-weight:600;";
|
||||||
|
if (!style.isEmpty())
|
||||||
|
l->setStyleSheet(style);
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel *tempLabel(double t)
|
||||||
|
{
|
||||||
|
if (t < 0)
|
||||||
|
return makeLabel("–", Theme::MUTED);
|
||||||
|
const char *c = t < 45 ? Theme::GREEN : (t < 55 ? Theme::YELLOW : Theme::RED);
|
||||||
|
return makeLabel(QString::number(t, 'f', 0) + " °C", c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void styleBar(QProgressBar *bar, double pct)
|
||||||
|
{
|
||||||
|
const char *c = pct < 70 ? Theme::GREEN : (pct < 90 ? Theme::YELLOW : Theme::RED);
|
||||||
|
bar->setStyleSheet(QString("QProgressBar{background:%1;border:none;border-radius:3px;}"
|
||||||
|
"QProgressBar::chunk{background:%2;border-radius:3px;}")
|
||||||
|
.arg(Theme::BORDER, c));
|
||||||
|
bar->setValue(qBound(0, static_cast<int>(pct + 0.5), 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
QProgressBar *makeBar()
|
||||||
|
{
|
||||||
|
auto *bar = new QProgressBar;
|
||||||
|
bar->setRange(0, 100);
|
||||||
|
bar->setTextVisible(false);
|
||||||
|
bar->setFixedHeight(7);
|
||||||
|
styleBar(bar, 0);
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk-Status → (Text, Farbe)
|
||||||
|
QPair<QString, const char *> diskStatus(const QString &status)
|
||||||
|
{
|
||||||
|
if (status == "DISK_OK")
|
||||||
|
return {"OK", Theme::GREEN};
|
||||||
|
if (status == "DISK_NP" || status == "DISK_NP_DSBL")
|
||||||
|
return {"N/V", Theme::MUTED};
|
||||||
|
if (status.contains("DSBL") || status.contains("INVALID"))
|
||||||
|
return {"Fehler", Theme::RED};
|
||||||
|
if (status.isEmpty())
|
||||||
|
return {"–", Theme::MUTED};
|
||||||
|
return {status, Theme::YELLOW};
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearLayout(QLayout *layout)
|
||||||
|
{
|
||||||
|
while (QLayoutItem *item = layout->takeAt(0)) {
|
||||||
|
if (QWidget *w = item->widget())
|
||||||
|
w->deleteLater();
|
||||||
|
if (QLayout *child = item->layout())
|
||||||
|
clearLayout(child);
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QGridLayout *makeTableGrid(QVBoxLayout *body)
|
||||||
|
{
|
||||||
|
auto *grid = new QGridLayout;
|
||||||
|
grid->setHorizontalSpacing(18);
|
||||||
|
grid->setVerticalSpacing(8);
|
||||||
|
body->addLayout(grid);
|
||||||
|
body->addStretch();
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addHeaderRow(QGridLayout *grid, const QStringList &titles, int stretchCol)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < titles.size(); ++i) {
|
||||||
|
auto *l = new QLabel(titles.at(i));
|
||||||
|
l->setObjectName("tableHeader");
|
||||||
|
grid->addWidget(l, 0, i);
|
||||||
|
}
|
||||||
|
grid->setColumnStretch(stretchCol, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------- CardWidget ----------
|
||||||
|
|
||||||
|
CardWidget::CardWidget(const QString &title, bool wide, QWidget *parent)
|
||||||
|
: QFrame(parent)
|
||||||
|
, m_wide(wide)
|
||||||
|
{
|
||||||
|
setObjectName("card");
|
||||||
|
auto *outer = new QVBoxLayout(this);
|
||||||
|
outer->setContentsMargins(16, 14, 16, 14);
|
||||||
|
outer->setSpacing(10);
|
||||||
|
|
||||||
|
auto *header = new QHBoxLayout;
|
||||||
|
auto *titleLabel = new QLabel(title);
|
||||||
|
titleLabel->setObjectName("cardTitle");
|
||||||
|
header->addWidget(titleLabel);
|
||||||
|
header->addStretch();
|
||||||
|
m_headerRight = new QLabel;
|
||||||
|
m_headerRight->setObjectName("muted");
|
||||||
|
header->addWidget(m_headerRight);
|
||||||
|
outer->addLayout(header);
|
||||||
|
|
||||||
|
m_body = new QVBoxLayout;
|
||||||
|
m_body->setSpacing(8);
|
||||||
|
outer->addLayout(m_body);
|
||||||
|
outer->addStretch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SystemCard ----------
|
||||||
|
|
||||||
|
static void addInfoRow(QGridLayout *grid, int row, const QString &key, QLabel *value)
|
||||||
|
{
|
||||||
|
auto *k = new QLabel(key);
|
||||||
|
k->setObjectName("muted");
|
||||||
|
grid->addWidget(k, row, 0);
|
||||||
|
value->setWordWrap(true);
|
||||||
|
grid->addWidget(value, row, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemCard::SystemCard(QWidget *parent)
|
||||||
|
: CardWidget(tr("System"), false, parent)
|
||||||
|
{
|
||||||
|
auto *grid = new QGridLayout;
|
||||||
|
grid->setHorizontalSpacing(16);
|
||||||
|
grid->setVerticalSpacing(6);
|
||||||
|
grid->setColumnStretch(1, 1);
|
||||||
|
|
||||||
|
int r = 0;
|
||||||
|
addInfoRow(grid, r++, tr("Hostname"), m_hostname = new QLabel("–"));
|
||||||
|
addInfoRow(grid, r++, tr("Unraid"), m_version = new QLabel("–"));
|
||||||
|
addInfoRow(grid, r++, tr("OS"), m_os = new QLabel("–"));
|
||||||
|
addInfoRow(grid, r++, tr("Kernel"), m_kernel = new QLabel("–"));
|
||||||
|
addInfoRow(grid, r++, tr("CPU"), m_cpu = new QLabel("–"));
|
||||||
|
addInfoRow(grid, r++, tr("Uptime"), m_uptime = new QLabel("–"));
|
||||||
|
m_body->addLayout(grid);
|
||||||
|
|
||||||
|
auto *cpuRow = new QHBoxLayout;
|
||||||
|
auto *cpuTitle = new QLabel(tr("CPU-Auslastung"));
|
||||||
|
cpuTitle->setObjectName("muted");
|
||||||
|
cpuRow->addWidget(cpuTitle);
|
||||||
|
cpuRow->addStretch();
|
||||||
|
cpuRow->addWidget(m_cpuPctLabel = new QLabel("–"));
|
||||||
|
m_body->addLayout(cpuRow);
|
||||||
|
m_body->addWidget(m_cpuBar = makeBar());
|
||||||
|
|
||||||
|
auto *memRow = new QHBoxLayout;
|
||||||
|
auto *memTitle = new QLabel(tr("RAM"));
|
||||||
|
memTitle->setObjectName("muted");
|
||||||
|
memRow->addWidget(memTitle);
|
||||||
|
memRow->addStretch();
|
||||||
|
memRow->addWidget(m_memLabel = new QLabel("–"));
|
||||||
|
m_body->addLayout(memRow);
|
||||||
|
m_body->addWidget(m_memBar = makeBar());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemCard::updateData(const ServerSnapshot &s)
|
||||||
|
{
|
||||||
|
m_hostname->setText(s.hostname.isEmpty() ? "–" : s.hostname);
|
||||||
|
m_version->setText(s.unraidVersion.isEmpty()
|
||||||
|
? "–"
|
||||||
|
: tr("%1 (API %2)").arg(s.unraidVersion, s.apiVersion));
|
||||||
|
m_os->setText(QString("%1 %2").arg(s.distro, s.release).trimmed());
|
||||||
|
m_kernel->setText(s.kernel.isEmpty() ? "–" : s.kernel);
|
||||||
|
m_cpu->setText(s.cpuBrand.isEmpty()
|
||||||
|
? "–"
|
||||||
|
: tr("%1 (%2C/%3T)").arg(s.cpuBrand).arg(s.cpuCores).arg(s.cpuThreads));
|
||||||
|
|
||||||
|
if (s.bootTime.isValid()) {
|
||||||
|
qint64 secs = s.bootTime.secsTo(QDateTime::currentDateTime());
|
||||||
|
if (secs < 0)
|
||||||
|
secs = 0;
|
||||||
|
m_uptime->setText(tr("%1 Tage, %2 Std, %3 Min")
|
||||||
|
.arg(secs / 86400)
|
||||||
|
.arg((secs % 86400) / 3600)
|
||||||
|
.arg((secs % 3600) / 60));
|
||||||
|
} else {
|
||||||
|
m_uptime->setText("–");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.cpuPercent >= 0) {
|
||||||
|
m_cpuPctLabel->setText(QString::number(s.cpuPercent, 'f', 1) + " %");
|
||||||
|
styleBar(m_cpuBar, s.cpuPercent);
|
||||||
|
} else {
|
||||||
|
m_cpuPctLabel->setText("–");
|
||||||
|
styleBar(m_cpuBar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.memTotal > 0) {
|
||||||
|
// "used" der API enthält Buffer/Cache – tatsächliche Belegung ist total - available
|
||||||
|
const qint64 effectiveUsed = s.memAvailable > 0 ? s.memTotal - s.memAvailable : s.memUsed;
|
||||||
|
const double pct = s.memPercent >= 0
|
||||||
|
? s.memPercent
|
||||||
|
: 100.0 * static_cast<double>(effectiveUsed) / static_cast<double>(s.memTotal);
|
||||||
|
m_memLabel->setText(tr("%1 / %2 (%3 %)")
|
||||||
|
.arg(bytesHuman(effectiveUsed), bytesHuman(s.memTotal))
|
||||||
|
.arg(pct, 0, 'f', 1));
|
||||||
|
styleBar(m_memBar, pct);
|
||||||
|
} else {
|
||||||
|
m_memLabel->setText("–");
|
||||||
|
styleBar(m_memBar, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- ArrayCard ----------
|
||||||
|
|
||||||
|
ArrayCard::ArrayCard(QWidget *parent)
|
||||||
|
: CardWidget(tr("Array"), false, parent)
|
||||||
|
{
|
||||||
|
auto *stateRow = new QHBoxLayout;
|
||||||
|
auto *stateTitle = new QLabel(tr("Status"));
|
||||||
|
stateTitle->setObjectName("muted");
|
||||||
|
stateRow->addWidget(stateTitle);
|
||||||
|
stateRow->addStretch();
|
||||||
|
stateRow->addWidget(m_state = new QLabel("–"));
|
||||||
|
m_body->addLayout(stateRow);
|
||||||
|
|
||||||
|
m_body->addSpacing(4);
|
||||||
|
auto *usageRow = new QHBoxLayout;
|
||||||
|
auto *usageTitle = new QLabel(tr("Belegung"));
|
||||||
|
usageTitle->setObjectName("muted");
|
||||||
|
usageRow->addWidget(usageTitle);
|
||||||
|
usageRow->addStretch();
|
||||||
|
usageRow->addWidget(m_usageLabel = new QLabel("–"));
|
||||||
|
m_body->addLayout(usageRow);
|
||||||
|
m_body->addWidget(m_bar = makeBar());
|
||||||
|
|
||||||
|
m_freeLabel = new QLabel("–");
|
||||||
|
m_freeLabel->setObjectName("muted");
|
||||||
|
m_body->addWidget(m_freeLabel);
|
||||||
|
|
||||||
|
m_disksLabel = new QLabel("–");
|
||||||
|
m_disksLabel->setObjectName("muted");
|
||||||
|
m_body->addWidget(m_disksLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArrayCard::updateData(const ServerSnapshot &s)
|
||||||
|
{
|
||||||
|
if (s.arrayState == "STARTED") {
|
||||||
|
m_state->setText(tr("Gestartet"));
|
||||||
|
m_state->setStyleSheet(QString("color:%1;font-weight:600;").arg(Theme::GREEN));
|
||||||
|
} else if (s.arrayState == "STOPPED") {
|
||||||
|
m_state->setText(tr("Gestoppt"));
|
||||||
|
m_state->setStyleSheet(QString("color:%1;font-weight:600;").arg(Theme::RED));
|
||||||
|
} else {
|
||||||
|
m_state->setText(s.arrayState.isEmpty() ? "–" : s.arrayState);
|
||||||
|
m_state->setStyleSheet(QString("color:%1;font-weight:600;").arg(Theme::YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.capTotalKb > 0) {
|
||||||
|
const double pct = 100.0 * static_cast<double>(s.capUsedKb) / static_cast<double>(s.capTotalKb);
|
||||||
|
m_usageLabel->setText(tr("%1 / %2 (%3 %)")
|
||||||
|
.arg(kbHuman(s.capUsedKb), kbHuman(s.capTotalKb))
|
||||||
|
.arg(pct, 0, 'f', 1));
|
||||||
|
styleBar(m_bar, pct);
|
||||||
|
m_freeLabel->setText(tr("%1 frei").arg(kbHuman(s.capFreeKb)));
|
||||||
|
} else {
|
||||||
|
m_usageLabel->setText("–");
|
||||||
|
m_freeLabel->setText("–");
|
||||||
|
styleBar(m_bar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 errors = 0;
|
||||||
|
for (const auto &d : s.dataDisks)
|
||||||
|
errors += d.numErrors;
|
||||||
|
for (const auto &d : s.parityDisks)
|
||||||
|
errors += d.numErrors;
|
||||||
|
m_disksLabel->setText(tr("%1 Daten-Disks, %2 Parität · %3 Fehler")
|
||||||
|
.arg(s.dataDisks.size())
|
||||||
|
.arg(s.parityDisks.size())
|
||||||
|
.arg(errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- PoolsCard ----------
|
||||||
|
|
||||||
|
PoolsCard::PoolsCard(QWidget *parent)
|
||||||
|
: CardWidget(tr("Pools / Cache"), false, parent)
|
||||||
|
{
|
||||||
|
m_list = new QVBoxLayout;
|
||||||
|
m_list->setSpacing(10);
|
||||||
|
m_body->addLayout(m_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PoolsCard::updateData(const ServerSnapshot &s)
|
||||||
|
{
|
||||||
|
clearLayout(m_list);
|
||||||
|
|
||||||
|
if (s.cachePools.isEmpty()) {
|
||||||
|
auto *l = new QLabel(tr("Keine Pools vorhanden"));
|
||||||
|
l->setObjectName("muted");
|
||||||
|
m_list->addWidget(l);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const DiskInfo &p : s.cachePools) {
|
||||||
|
auto *row = new QHBoxLayout;
|
||||||
|
row->addWidget(makeLabel(p.name, nullptr, true));
|
||||||
|
row->addStretch();
|
||||||
|
row->addWidget(tempLabel(p.temp));
|
||||||
|
m_list->addLayout(row);
|
||||||
|
|
||||||
|
auto *bar = makeBar();
|
||||||
|
double pct = 0;
|
||||||
|
QString text = "–";
|
||||||
|
if (p.fsSizeKb > 0) {
|
||||||
|
pct = 100.0 * static_cast<double>(p.fsUsedKb) / static_cast<double>(p.fsSizeKb);
|
||||||
|
text = tr("%1 / %2 (%3 %)")
|
||||||
|
.arg(kbHuman(p.fsUsedKb), kbHuman(p.fsSizeKb))
|
||||||
|
.arg(pct, 0, 'f', 1);
|
||||||
|
}
|
||||||
|
styleBar(bar, pct);
|
||||||
|
m_list->addWidget(bar);
|
||||||
|
auto *info = new QLabel(text);
|
||||||
|
info->setObjectName("muted");
|
||||||
|
m_list->addWidget(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- DisksCard ----------
|
||||||
|
|
||||||
|
DisksCard::DisksCard(QWidget *parent)
|
||||||
|
: CardWidget(tr("Festplatten (Array)"), true, parent)
|
||||||
|
{
|
||||||
|
m_grid = makeTableGrid(m_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisksCard::updateData(const ServerSnapshot &s)
|
||||||
|
{
|
||||||
|
clearLayout(m_grid);
|
||||||
|
addHeaderRow(m_grid,
|
||||||
|
{tr("Disk"), tr("Gerät"), tr("Größe"), tr("Temp"), tr("Status"),
|
||||||
|
tr("Fehler"), tr("Belegt"), ""},
|
||||||
|
7);
|
||||||
|
|
||||||
|
int row = 1;
|
||||||
|
auto addSection = [&](const QString &name) {
|
||||||
|
auto *l = new QLabel(name);
|
||||||
|
l->setObjectName("sectionLabel");
|
||||||
|
m_grid->addWidget(l, row++, 0, 1, 8);
|
||||||
|
};
|
||||||
|
auto addDisk = [&](const DiskInfo &d, bool hasFs) {
|
||||||
|
m_grid->addWidget(makeLabel(d.name, nullptr, true), row, 0);
|
||||||
|
m_grid->addWidget(makeLabel(d.device.isEmpty() ? "–" : d.device, Theme::MUTED), row, 1);
|
||||||
|
m_grid->addWidget(makeLabel(d.sizeKb > 0 ? kbHuman(d.sizeKb) : "–"), row, 2);
|
||||||
|
if (!d.isSpinning && d.temp < 0)
|
||||||
|
m_grid->addWidget(makeLabel(tr("Standby"), Theme::MUTED), row, 3);
|
||||||
|
else
|
||||||
|
m_grid->addWidget(tempLabel(d.temp), row, 3);
|
||||||
|
const auto st = diskStatus(d.status);
|
||||||
|
m_grid->addWidget(makeLabel(st.first, st.second, true), row, 4);
|
||||||
|
m_grid->addWidget(makeLabel(QString::number(d.numErrors),
|
||||||
|
d.numErrors > 0 ? Theme::RED : Theme::MUTED),
|
||||||
|
row, 5);
|
||||||
|
if (hasFs && d.fsSizeKb > 0) {
|
||||||
|
const double pct = 100.0 * static_cast<double>(d.fsUsedKb) / static_cast<double>(d.fsSizeKb);
|
||||||
|
m_grid->addWidget(makeLabel(tr("%1 / %2")
|
||||||
|
.arg(kbHuman(d.fsUsedKb), kbHuman(d.fsSizeKb))),
|
||||||
|
row, 6);
|
||||||
|
auto *bar = makeBar();
|
||||||
|
bar->setMinimumWidth(90);
|
||||||
|
styleBar(bar, pct);
|
||||||
|
m_grid->addWidget(bar, row, 7);
|
||||||
|
} else {
|
||||||
|
m_grid->addWidget(makeLabel("–", Theme::MUTED), row, 6);
|
||||||
|
}
|
||||||
|
++row;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!s.parityDisks.isEmpty()) {
|
||||||
|
addSection(tr("PARITÄT"));
|
||||||
|
for (const auto &d : s.parityDisks)
|
||||||
|
addDisk(d, false);
|
||||||
|
}
|
||||||
|
if (!s.dataDisks.isEmpty()) {
|
||||||
|
addSection(tr("DATEN"));
|
||||||
|
for (const auto &d : s.dataDisks)
|
||||||
|
addDisk(d, true);
|
||||||
|
}
|
||||||
|
if (s.parityDisks.isEmpty() && s.dataDisks.isEmpty()) {
|
||||||
|
auto *l = new QLabel(tr("Keine Festplatten gefunden"));
|
||||||
|
l->setObjectName("muted");
|
||||||
|
m_grid->addWidget(l, row, 0, 1, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SmartCard ----------
|
||||||
|
|
||||||
|
SmartCard::SmartCard(QWidget *parent)
|
||||||
|
: CardWidget(tr("SMART / Laufwerke"), true, parent)
|
||||||
|
{
|
||||||
|
m_grid = makeTableGrid(m_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SmartCard::updateData(const ServerSnapshot &s)
|
||||||
|
{
|
||||||
|
clearLayout(m_grid);
|
||||||
|
addHeaderRow(m_grid,
|
||||||
|
{tr("Gerät"), tr("Modell"), tr("Seriennummer"), tr("Schnittstelle"),
|
||||||
|
tr("Größe"), tr("Temp"), tr("SMART")},
|
||||||
|
1);
|
||||||
|
|
||||||
|
int row = 1;
|
||||||
|
for (const PhysicalDiskInfo &p : s.physicalDisks) {
|
||||||
|
m_grid->addWidget(makeLabel(p.device, nullptr, true), row, 0);
|
||||||
|
m_grid->addWidget(makeLabel(p.model.isEmpty() ? "–" : p.model), row, 1);
|
||||||
|
m_grid->addWidget(makeLabel(p.serial.isEmpty() ? "–" : p.serial, Theme::MUTED), row, 2);
|
||||||
|
m_grid->addWidget(makeLabel(p.interfaceType.isEmpty() ? "–" : p.interfaceType,
|
||||||
|
Theme::MUTED),
|
||||||
|
row, 3);
|
||||||
|
m_grid->addWidget(makeLabel(p.sizeBytes > 0 ? bytesHuman(p.sizeBytes) : "–"), row, 4);
|
||||||
|
m_grid->addWidget(tempLabel(p.temperature), row, 5);
|
||||||
|
if (p.smartStatus == "OK")
|
||||||
|
m_grid->addWidget(makeLabel(tr("OK"), Theme::GREEN, true), row, 6);
|
||||||
|
else
|
||||||
|
m_grid->addWidget(makeLabel(p.smartStatus.isEmpty() ? "–" : p.smartStatus,
|
||||||
|
Theme::YELLOW, true),
|
||||||
|
row, 6);
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
if (s.physicalDisks.isEmpty()) {
|
||||||
|
auto *l = new QLabel(tr("Keine Laufwerke gefunden"));
|
||||||
|
l->setObjectName("muted");
|
||||||
|
m_grid->addWidget(l, row, 0, 1, 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- DockerCard ----------
|
||||||
|
|
||||||
|
DockerCard::DockerCard(QWidget *parent)
|
||||||
|
: CardWidget(tr("Docker-Container"), true, parent)
|
||||||
|
{
|
||||||
|
m_grid = makeTableGrid(m_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DockerCard::updateData(const ServerSnapshot &s)
|
||||||
|
{
|
||||||
|
clearLayout(m_grid);
|
||||||
|
addHeaderRow(m_grid,
|
||||||
|
{"", tr("Container"), tr("Image"), tr("Status"), tr("Autostart")}, 2);
|
||||||
|
|
||||||
|
int running = 0;
|
||||||
|
int row = 1;
|
||||||
|
for (const ContainerInfo &c : s.containers) {
|
||||||
|
const char *dotColor = Theme::MUTED;
|
||||||
|
if (c.state == "RUNNING") {
|
||||||
|
dotColor = Theme::GREEN;
|
||||||
|
++running;
|
||||||
|
} else if (c.state == "PAUSED") {
|
||||||
|
dotColor = Theme::YELLOW;
|
||||||
|
}
|
||||||
|
auto *dot = makeLabel("●", dotColor);
|
||||||
|
dot->setToolTip(c.state);
|
||||||
|
m_grid->addWidget(dot, row, 0);
|
||||||
|
m_grid->addWidget(makeLabel(c.name, nullptr, true), row, 1);
|
||||||
|
m_grid->addWidget(makeLabel(c.image, Theme::MUTED), row, 2);
|
||||||
|
m_grid->addWidget(makeLabel(c.status.isEmpty() ? "–" : c.status), row, 3);
|
||||||
|
m_grid->addWidget(makeLabel(c.autoStart ? tr("Ja") : tr("Nein"),
|
||||||
|
c.autoStart ? nullptr : Theme::MUTED),
|
||||||
|
row, 4);
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_headerRight->setText(tr("%1 von %2 laufen").arg(running).arg(s.containers.size()));
|
||||||
|
|
||||||
|
if (s.containers.isEmpty()) {
|
||||||
|
auto *l = new QLabel(tr("Keine Container gefunden"));
|
||||||
|
l->setObjectName("muted");
|
||||||
|
m_grid->addWidget(l, row, 0, 1, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Cards.h
Normal file
87
src/Cards.h
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QFrame>
|
||||||
|
#include "UnraidClient.h"
|
||||||
|
|
||||||
|
class QGridLayout;
|
||||||
|
class QLabel;
|
||||||
|
class QProgressBar;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
// Basis-Karte: Titel + Inhaltsbereich
|
||||||
|
class CardWidget : public QFrame {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit CardWidget(const QString &title, bool wide, QWidget *parent = nullptr);
|
||||||
|
bool isWide() const { return m_wide; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QVBoxLayout *m_body; // Inhaltsbereich unter dem Titel
|
||||||
|
QLabel *m_headerRight; // optionale Zusatzinfo rechts im Titel
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_wide;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SystemCard : public CardWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SystemCard(QWidget *parent = nullptr);
|
||||||
|
void updateData(const ServerSnapshot &s);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel *m_hostname, *m_version, *m_os, *m_kernel, *m_cpu, *m_uptime;
|
||||||
|
QLabel *m_cpuPctLabel, *m_memLabel;
|
||||||
|
QProgressBar *m_cpuBar, *m_memBar;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArrayCard : public CardWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ArrayCard(QWidget *parent = nullptr);
|
||||||
|
void updateData(const ServerSnapshot &s);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel *m_state, *m_usageLabel, *m_freeLabel, *m_disksLabel;
|
||||||
|
QProgressBar *m_bar;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PoolsCard : public CardWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit PoolsCard(QWidget *parent = nullptr);
|
||||||
|
void updateData(const ServerSnapshot &s);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVBoxLayout *m_list;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DisksCard : public CardWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DisksCard(QWidget *parent = nullptr);
|
||||||
|
void updateData(const ServerSnapshot &s);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QGridLayout *m_grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SmartCard : public CardWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SmartCard(QWidget *parent = nullptr);
|
||||||
|
void updateData(const ServerSnapshot &s);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QGridLayout *m_grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DockerCard : public CardWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DockerCard(QWidget *parent = nullptr);
|
||||||
|
void updateData(const ServerSnapshot &s);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QGridLayout *m_grid;
|
||||||
|
};
|
||||||
199
src/MainWindow.cpp
Normal file
199
src/MainWindow.cpp
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
#include "MainWindow.h"
|
||||||
|
|
||||||
|
#include "Cards.h"
|
||||||
|
#include "SettingsDialog.h"
|
||||||
|
#include "Theme.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QStatusBar>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
|
: QMainWindow(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Unraid Monitor"));
|
||||||
|
resize(1150, 780);
|
||||||
|
|
||||||
|
auto *central = new QWidget;
|
||||||
|
auto *outer = new QVBoxLayout(central);
|
||||||
|
outer->setContentsMargins(18, 14, 18, 0);
|
||||||
|
outer->setSpacing(12);
|
||||||
|
|
||||||
|
// Kopfzeile
|
||||||
|
auto *header = new QHBoxLayout;
|
||||||
|
auto *title = new QLabel(tr("UNRAID MONITOR"));
|
||||||
|
title->setObjectName("appTitle");
|
||||||
|
header->addWidget(title);
|
||||||
|
|
||||||
|
m_hostLabel = new QLabel;
|
||||||
|
m_hostLabel->setObjectName("muted");
|
||||||
|
header->addSpacing(12);
|
||||||
|
header->addWidget(m_hostLabel);
|
||||||
|
header->addStretch();
|
||||||
|
|
||||||
|
m_statusDot = new QLabel("●");
|
||||||
|
m_statusDot->setStyleSheet(QString("color:%1;").arg(Theme::MUTED));
|
||||||
|
header->addWidget(m_statusDot);
|
||||||
|
m_statusText = new QLabel(tr("Nicht verbunden"));
|
||||||
|
m_statusText->setObjectName("muted");
|
||||||
|
header->addWidget(m_statusText);
|
||||||
|
header->addSpacing(12);
|
||||||
|
|
||||||
|
auto *refreshBtn = new QToolButton;
|
||||||
|
refreshBtn->setText(tr("⟳ Aktualisieren"));
|
||||||
|
header->addWidget(refreshBtn);
|
||||||
|
auto *settingsBtn = new QToolButton;
|
||||||
|
settingsBtn->setText(tr("⚙ Einstellungen"));
|
||||||
|
header->addWidget(settingsBtn);
|
||||||
|
outer->addLayout(header);
|
||||||
|
|
||||||
|
// Karten in Scrollbereich
|
||||||
|
auto *scroll = new QScrollArea;
|
||||||
|
scroll->setWidgetResizable(true);
|
||||||
|
auto *scrollContent = new QWidget;
|
||||||
|
scrollContent->setObjectName("scrollContent");
|
||||||
|
auto *scrollLayout = new QVBoxLayout(scrollContent);
|
||||||
|
scrollLayout->setContentsMargins(0, 0, 6, 14);
|
||||||
|
m_cardGrid = new QGridLayout;
|
||||||
|
m_cardGrid->setSpacing(14);
|
||||||
|
m_cardGrid->setColumnStretch(0, 1);
|
||||||
|
m_cardGrid->setColumnStretch(1, 1);
|
||||||
|
scrollLayout->addLayout(m_cardGrid);
|
||||||
|
scrollLayout->addStretch();
|
||||||
|
scroll->setWidget(scrollContent);
|
||||||
|
outer->addWidget(scroll, 1);
|
||||||
|
|
||||||
|
setCentralWidget(central);
|
||||||
|
statusBar()->showMessage(tr("Bereit"));
|
||||||
|
|
||||||
|
// Karten
|
||||||
|
m_systemCard = new SystemCard;
|
||||||
|
m_arrayCard = new ArrayCard;
|
||||||
|
m_poolsCard = new PoolsCard;
|
||||||
|
m_disksCard = new DisksCard;
|
||||||
|
m_smartCard = new SmartCard;
|
||||||
|
m_dockerCard = new DockerCard;
|
||||||
|
|
||||||
|
// Client + Timer
|
||||||
|
m_client = new UnraidClient(this);
|
||||||
|
connect(m_client, &UnraidClient::snapshotReady, this, &MainWindow::onSnapshot);
|
||||||
|
connect(m_client, &UnraidClient::requestFailed, this, &MainWindow::onError);
|
||||||
|
|
||||||
|
m_timer = new QTimer(this);
|
||||||
|
connect(m_timer, &QTimer::timeout, m_client, &UnraidClient::refresh);
|
||||||
|
|
||||||
|
connect(refreshBtn, &QToolButton::clicked, m_client, &UnraidClient::refresh);
|
||||||
|
connect(settingsBtn, &QToolButton::clicked, this, &MainWindow::openSettings);
|
||||||
|
|
||||||
|
m_settings = AppSettings::load();
|
||||||
|
applySettings();
|
||||||
|
|
||||||
|
if (m_settings.baseUrl.isEmpty() || m_settings.apiKey.isEmpty())
|
||||||
|
QTimer::singleShot(0, this, &MainWindow::openSettings);
|
||||||
|
else
|
||||||
|
m_client->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::applySettings()
|
||||||
|
{
|
||||||
|
m_client->setConnection(m_settings.baseUrl, m_settings.apiKey, m_settings.ignoreSsl);
|
||||||
|
m_timer->start(m_settings.intervalSec * 1000);
|
||||||
|
relayoutCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::relayoutCards()
|
||||||
|
{
|
||||||
|
const QList<QPair<CardWidget *, bool>> cards = {
|
||||||
|
{m_systemCard, m_settings.showSystem},
|
||||||
|
{m_arrayCard, m_settings.showArray},
|
||||||
|
{m_poolsCard, m_settings.showPools},
|
||||||
|
{m_disksCard, m_settings.showDisks},
|
||||||
|
{m_smartCard, m_settings.showSmart},
|
||||||
|
{m_dockerCard, m_settings.showDocker},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alle Karten aus dem Grid nehmen (Widgets bleiben erhalten)
|
||||||
|
for (const auto &c : cards) {
|
||||||
|
m_cardGrid->removeWidget(c.first);
|
||||||
|
c.first->setVisible(false);
|
||||||
|
c.first->setParent(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
int row = 0, col = 0;
|
||||||
|
for (const auto &c : cards) {
|
||||||
|
if (!c.second)
|
||||||
|
continue;
|
||||||
|
CardWidget *card = c.first;
|
||||||
|
if (card->isWide()) {
|
||||||
|
if (col == 1) {
|
||||||
|
++row;
|
||||||
|
col = 0;
|
||||||
|
}
|
||||||
|
m_cardGrid->addWidget(card, row, 0, 1, 2);
|
||||||
|
++row;
|
||||||
|
} else {
|
||||||
|
m_cardGrid->addWidget(card, row, col);
|
||||||
|
if (++col == 2) {
|
||||||
|
col = 0;
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
card->setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::setConnected(bool ok)
|
||||||
|
{
|
||||||
|
m_statusDot->setStyleSheet(QString("color:%1;").arg(ok ? Theme::GREEN : Theme::RED));
|
||||||
|
m_statusText->setText(ok ? tr("Verbunden") : tr("Fehler"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onSnapshot(const ServerSnapshot &snapshot)
|
||||||
|
{
|
||||||
|
m_hasSnapshot = true;
|
||||||
|
m_lastSnapshot = snapshot;
|
||||||
|
setConnected(true);
|
||||||
|
|
||||||
|
m_hostLabel->setText(snapshot.hostname.isEmpty()
|
||||||
|
? QString()
|
||||||
|
: QString("%1 · Unraid %2").arg(snapshot.hostname,
|
||||||
|
snapshot.unraidVersion));
|
||||||
|
|
||||||
|
m_systemCard->updateData(snapshot);
|
||||||
|
m_arrayCard->updateData(snapshot);
|
||||||
|
m_poolsCard->updateData(snapshot);
|
||||||
|
m_disksCard->updateData(snapshot);
|
||||||
|
m_smartCard->updateData(snapshot);
|
||||||
|
m_dockerCard->updateData(snapshot);
|
||||||
|
|
||||||
|
QString msg = tr("Zuletzt aktualisiert: %1")
|
||||||
|
.arg(QDateTime::currentDateTime().toString("HH:mm:ss"));
|
||||||
|
if (!snapshot.partialErrors.isEmpty())
|
||||||
|
msg += tr(" · Hinweis: %1").arg(snapshot.partialErrors.join("; "));
|
||||||
|
statusBar()->showMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onError(const QString &error)
|
||||||
|
{
|
||||||
|
setConnected(false);
|
||||||
|
statusBar()->showMessage(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::openSettings()
|
||||||
|
{
|
||||||
|
SettingsDialog dlg(m_settings, this);
|
||||||
|
if (dlg.exec() != QDialog::Accepted)
|
||||||
|
return;
|
||||||
|
m_settings = dlg.result();
|
||||||
|
m_settings.save();
|
||||||
|
applySettings();
|
||||||
|
if (m_hasSnapshot)
|
||||||
|
onSnapshot(m_lastSnapshot); // sichtbar geschaltete Karten sofort füllen
|
||||||
|
m_client->refresh();
|
||||||
|
}
|
||||||
50
src/MainWindow.h
Normal file
50
src/MainWindow.h
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include "Settings.h"
|
||||||
|
#include "UnraidClient.h"
|
||||||
|
|
||||||
|
class QGridLayout;
|
||||||
|
class QLabel;
|
||||||
|
class QTimer;
|
||||||
|
class ArrayCard;
|
||||||
|
class DisksCard;
|
||||||
|
class DockerCard;
|
||||||
|
class PoolsCard;
|
||||||
|
class SmartCard;
|
||||||
|
class SystemCard;
|
||||||
|
|
||||||
|
class MainWindow : public QMainWindow {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit MainWindow(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void openSettings();
|
||||||
|
void onSnapshot(const ServerSnapshot &snapshot);
|
||||||
|
void onError(const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void applySettings();
|
||||||
|
void relayoutCards();
|
||||||
|
void setConnected(bool ok);
|
||||||
|
|
||||||
|
AppSettings m_settings;
|
||||||
|
UnraidClient *m_client;
|
||||||
|
QTimer *m_timer;
|
||||||
|
|
||||||
|
QGridLayout *m_cardGrid;
|
||||||
|
QLabel *m_statusDot;
|
||||||
|
QLabel *m_statusText;
|
||||||
|
QLabel *m_hostLabel;
|
||||||
|
|
||||||
|
SystemCard *m_systemCard;
|
||||||
|
ArrayCard *m_arrayCard;
|
||||||
|
PoolsCard *m_poolsCard;
|
||||||
|
DisksCard *m_disksCard;
|
||||||
|
SmartCard *m_smartCard;
|
||||||
|
DockerCard *m_dockerCard;
|
||||||
|
|
||||||
|
bool m_hasSnapshot = false;
|
||||||
|
ServerSnapshot m_lastSnapshot;
|
||||||
|
};
|
||||||
51
src/Settings.h
Normal file
51
src/Settings.h
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
struct AppSettings {
|
||||||
|
QString baseUrl; // z.B. http://192.168.1.10
|
||||||
|
QString apiKey;
|
||||||
|
int intervalSec = 10;
|
||||||
|
bool ignoreSsl = false;
|
||||||
|
|
||||||
|
// Sichtbarkeit der Module
|
||||||
|
bool showSystem = true;
|
||||||
|
bool showArray = true;
|
||||||
|
bool showPools = true;
|
||||||
|
bool showDisks = true;
|
||||||
|
bool showSmart = true;
|
||||||
|
bool showDocker = true;
|
||||||
|
|
||||||
|
static AppSettings load()
|
||||||
|
{
|
||||||
|
QSettings s;
|
||||||
|
AppSettings a;
|
||||||
|
a.baseUrl = s.value("server/baseUrl").toString();
|
||||||
|
a.apiKey = s.value("server/apiKey").toString();
|
||||||
|
a.intervalSec = s.value("server/intervalSec", 10).toInt();
|
||||||
|
a.ignoreSsl = s.value("server/ignoreSsl", false).toBool();
|
||||||
|
a.showSystem = s.value("cards/system", true).toBool();
|
||||||
|
a.showArray = s.value("cards/array", true).toBool();
|
||||||
|
a.showPools = s.value("cards/pools", true).toBool();
|
||||||
|
a.showDisks = s.value("cards/disks", true).toBool();
|
||||||
|
a.showSmart = s.value("cards/smart", true).toBool();
|
||||||
|
a.showDocker = s.value("cards/docker", true).toBool();
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
void save() const
|
||||||
|
{
|
||||||
|
QSettings s;
|
||||||
|
s.setValue("server/baseUrl", baseUrl);
|
||||||
|
s.setValue("server/apiKey", apiKey);
|
||||||
|
s.setValue("server/intervalSec", intervalSec);
|
||||||
|
s.setValue("server/ignoreSsl", ignoreSsl);
|
||||||
|
s.setValue("cards/system", showSystem);
|
||||||
|
s.setValue("cards/array", showArray);
|
||||||
|
s.setValue("cards/pools", showPools);
|
||||||
|
s.setValue("cards/disks", showDisks);
|
||||||
|
s.setValue("cards/smart", showSmart);
|
||||||
|
s.setValue("cards/docker", showDocker);
|
||||||
|
}
|
||||||
|
};
|
||||||
100
src/SettingsDialog.cpp
Normal file
100
src/SettingsDialog.cpp
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
#include "SettingsDialog.h"
|
||||||
|
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
SettingsDialog::SettingsDialog(const AppSettings &settings, QWidget *parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Einstellungen"));
|
||||||
|
setMinimumWidth(460);
|
||||||
|
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
layout->setSpacing(14);
|
||||||
|
|
||||||
|
// Verbindung
|
||||||
|
auto *connBox = new QGroupBox(tr("Verbindung"));
|
||||||
|
auto *form = new QFormLayout(connBox);
|
||||||
|
form->setSpacing(10);
|
||||||
|
|
||||||
|
m_url = new QLineEdit(settings.baseUrl);
|
||||||
|
m_url->setPlaceholderText("http://192.168.1.10");
|
||||||
|
form->addRow(tr("Server-URL"), m_url);
|
||||||
|
|
||||||
|
m_apiKey = new QLineEdit(settings.apiKey);
|
||||||
|
m_apiKey->setEchoMode(QLineEdit::Password);
|
||||||
|
m_apiKey->setPlaceholderText(tr("Unraid API-Key"));
|
||||||
|
form->addRow(tr("API-Key"), m_apiKey);
|
||||||
|
|
||||||
|
m_interval = new QSpinBox;
|
||||||
|
m_interval->setRange(3, 600);
|
||||||
|
m_interval->setSuffix(tr(" Sekunden"));
|
||||||
|
m_interval->setValue(settings.intervalSec);
|
||||||
|
form->addRow(tr("Intervall"), m_interval);
|
||||||
|
|
||||||
|
m_ignoreSsl = new QCheckBox(tr("SSL-Zertifikatsfehler ignorieren (selbstsigniert)"));
|
||||||
|
m_ignoreSsl->setChecked(settings.ignoreSsl);
|
||||||
|
form->addRow(QString(), m_ignoreSsl);
|
||||||
|
|
||||||
|
auto *hint = new QLabel(tr("API-Key erstellen: Unraid-WebGUI → Einstellungen → "
|
||||||
|
"Management Access → API Keys"));
|
||||||
|
hint->setObjectName("muted");
|
||||||
|
hint->setWordWrap(true);
|
||||||
|
form->addRow(QString(), hint);
|
||||||
|
layout->addWidget(connBox);
|
||||||
|
|
||||||
|
// Module
|
||||||
|
auto *modBox = new QGroupBox(tr("Angezeigte Module"));
|
||||||
|
auto *grid = new QGridLayout(modBox);
|
||||||
|
grid->setSpacing(10);
|
||||||
|
m_system = new QCheckBox(tr("System"));
|
||||||
|
m_array = new QCheckBox(tr("Array"));
|
||||||
|
m_pools = new QCheckBox(tr("Pools / Cache"));
|
||||||
|
m_disks = new QCheckBox(tr("Festplatten"));
|
||||||
|
m_smart = new QCheckBox(tr("SMART / Laufwerke"));
|
||||||
|
m_docker = new QCheckBox(tr("Docker-Container"));
|
||||||
|
m_system->setChecked(settings.showSystem);
|
||||||
|
m_array->setChecked(settings.showArray);
|
||||||
|
m_pools->setChecked(settings.showPools);
|
||||||
|
m_disks->setChecked(settings.showDisks);
|
||||||
|
m_smart->setChecked(settings.showSmart);
|
||||||
|
m_docker->setChecked(settings.showDocker);
|
||||||
|
grid->addWidget(m_system, 0, 0);
|
||||||
|
grid->addWidget(m_array, 0, 1);
|
||||||
|
grid->addWidget(m_pools, 1, 0);
|
||||||
|
grid->addWidget(m_disks, 1, 1);
|
||||||
|
grid->addWidget(m_smart, 2, 0);
|
||||||
|
grid->addWidget(m_docker, 2, 1);
|
||||||
|
layout->addWidget(modBox);
|
||||||
|
|
||||||
|
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
buttons->button(QDialogButtonBox::Ok)->setText(tr("Speichern"));
|
||||||
|
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Abbrechen"));
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
layout->addWidget(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettings SettingsDialog::result() const
|
||||||
|
{
|
||||||
|
AppSettings a;
|
||||||
|
a.baseUrl = m_url->text().trimmed();
|
||||||
|
a.apiKey = m_apiKey->text().trimmed();
|
||||||
|
a.intervalSec = m_interval->value();
|
||||||
|
a.ignoreSsl = m_ignoreSsl->isChecked();
|
||||||
|
a.showSystem = m_system->isChecked();
|
||||||
|
a.showArray = m_array->isChecked();
|
||||||
|
a.showPools = m_pools->isChecked();
|
||||||
|
a.showDisks = m_disks->isChecked();
|
||||||
|
a.showSmart = m_smart->isChecked();
|
||||||
|
a.showDocker = m_docker->isChecked();
|
||||||
|
return a;
|
||||||
|
}
|
||||||
21
src/SettingsDialog.h
Normal file
21
src/SettingsDialog.h
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include "Settings.h"
|
||||||
|
|
||||||
|
class QCheckBox;
|
||||||
|
class QLineEdit;
|
||||||
|
class QSpinBox;
|
||||||
|
|
||||||
|
class SettingsDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SettingsDialog(const AppSettings &settings, QWidget *parent = nullptr);
|
||||||
|
AppSettings result() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLineEdit *m_url, *m_apiKey;
|
||||||
|
QSpinBox *m_interval;
|
||||||
|
QCheckBox *m_ignoreSsl;
|
||||||
|
QCheckBox *m_system, *m_array, *m_pools, *m_disks, *m_smart, *m_docker;
|
||||||
|
};
|
||||||
84
src/Theme.h
Normal file
84
src/Theme.h
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace Theme {
|
||||||
|
|
||||||
|
// Farbpalette
|
||||||
|
inline const char *BG = "#131519";
|
||||||
|
inline const char *CARD = "#1b1e24";
|
||||||
|
inline const char *BORDER = "#262b33";
|
||||||
|
inline const char *TEXT = "#d6dae2";
|
||||||
|
inline const char *MUTED = "#8a93a3";
|
||||||
|
inline const char *ACCENT = "#ff8c2f"; // Unraid-Orange
|
||||||
|
inline const char *GREEN = "#3fb950";
|
||||||
|
inline const char *YELLOW = "#d29922";
|
||||||
|
inline const char *RED = "#f85149";
|
||||||
|
|
||||||
|
inline QString styleSheet()
|
||||||
|
{
|
||||||
|
return QStringLiteral(R"(
|
||||||
|
QMainWindow, QDialog { background-color: %1; }
|
||||||
|
QWidget { color: %4; font-size: 13px; }
|
||||||
|
|
||||||
|
QScrollArea { border: none; background: %1; }
|
||||||
|
QWidget#scrollContent { background: %1; }
|
||||||
|
|
||||||
|
QFrame#card {
|
||||||
|
background-color: %2;
|
||||||
|
border: 1px solid %3;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
QLabel { background: transparent; }
|
||||||
|
QLabel#cardTitle { font-size: 14px; font-weight: 600; color: #f0f2f5; }
|
||||||
|
QLabel#appTitle { font-size: 18px; font-weight: 700; color: %6; }
|
||||||
|
QLabel#muted { color: %5; }
|
||||||
|
QLabel#sectionLabel { color: %5; font-size: 11px; font-weight: 600; }
|
||||||
|
QLabel#tableHeader { color: %5; font-size: 11px; font-weight: 600; }
|
||||||
|
|
||||||
|
QToolButton, QPushButton {
|
||||||
|
background-color: #232830;
|
||||||
|
border: 1px solid %3;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
color: %4;
|
||||||
|
}
|
||||||
|
QToolButton:hover, QPushButton:hover { background-color: #2c323c; border-color: %6; }
|
||||||
|
QToolButton:pressed, QPushButton:pressed { background-color: #1b1e24; }
|
||||||
|
|
||||||
|
QLineEdit, QSpinBox {
|
||||||
|
background-color: #11141a;
|
||||||
|
border: 1px solid %3;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: %4;
|
||||||
|
selection-background-color: %6;
|
||||||
|
}
|
||||||
|
QLineEdit:focus, QSpinBox:focus { border-color: %6; }
|
||||||
|
QSpinBox::up-button, QSpinBox::down-button { width: 0; }
|
||||||
|
|
||||||
|
QCheckBox { spacing: 8px; }
|
||||||
|
QCheckBox::indicator {
|
||||||
|
width: 16px; height: 16px;
|
||||||
|
border: 1px solid %3; border-radius: 4px;
|
||||||
|
background: #11141a;
|
||||||
|
}
|
||||||
|
QCheckBox::indicator:checked { background-color: %6; border-color: %6; }
|
||||||
|
|
||||||
|
QStatusBar { background: %1; color: %5; border-top: 1px solid %3; }
|
||||||
|
QStatusBar::item { border: none; }
|
||||||
|
|
||||||
|
QScrollBar:vertical { background: %1; width: 10px; margin: 0; }
|
||||||
|
QScrollBar::handle:vertical { background: #2c323c; border-radius: 5px; min-height: 30px; }
|
||||||
|
QScrollBar::handle:vertical:hover { background: #3a4250; }
|
||||||
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
|
||||||
|
QScrollBar:horizontal { background: %1; height: 10px; margin: 0; }
|
||||||
|
QScrollBar::handle:horizontal { background: #2c323c; border-radius: 5px; min-width: 30px; }
|
||||||
|
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }
|
||||||
|
|
||||||
|
QToolTip { background-color: #232830; color: %4; border: 1px solid %3; }
|
||||||
|
)")
|
||||||
|
.arg(BG, CARD, BORDER, TEXT, MUTED, ACCENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Theme
|
||||||
255
src/UnraidClient.cpp
Normal file
255
src/UnraidClient.cpp
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
#include "UnraidClient.h"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QSslConfiguration>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
// Eine kombinierte Abfrage für alle Dashboard-Daten (Unraid API v4.x / Unraid 7.x)
|
||||||
|
static const char *kQuery = R"(
|
||||||
|
query Monitor {
|
||||||
|
info {
|
||||||
|
cpu { brand cores threads }
|
||||||
|
os { distro release kernel hostname uptime }
|
||||||
|
versions { core { unraid api } }
|
||||||
|
}
|
||||||
|
metrics {
|
||||||
|
cpu { percentTotal }
|
||||||
|
memory { total used available percentTotal }
|
||||||
|
}
|
||||||
|
array {
|
||||||
|
state
|
||||||
|
capacity { kilobytes { free used total } }
|
||||||
|
disks { name device size status temp numErrors fsSize fsFree fsUsed isSpinning }
|
||||||
|
parities { name device size status temp numErrors isSpinning }
|
||||||
|
caches { name device size status temp fsSize fsFree fsUsed isSpinning }
|
||||||
|
}
|
||||||
|
disks { device name vendor serialNum interfaceType smartStatus temperature size }
|
||||||
|
docker { containers { names image state status autoStart } }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// BigInt-Felder kommen je nach Größe als Zahl oder String
|
||||||
|
static qint64 toLL(const QJsonValue &v)
|
||||||
|
{
|
||||||
|
if (v.isString())
|
||||||
|
return v.toString().toLongLong();
|
||||||
|
if (v.isDouble())
|
||||||
|
return static_cast<qint64>(v.toDouble());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int toTemp(const QJsonValue &v)
|
||||||
|
{
|
||||||
|
if (!v.isDouble() || std::isnan(v.toDouble()))
|
||||||
|
return -1;
|
||||||
|
return static_cast<int>(v.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
UnraidClient::UnraidClient(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_nam(new QNetworkAccessManager(this))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnraidClient::setConnection(const QString &baseUrl, const QString &apiKey, bool ignoreSsl)
|
||||||
|
{
|
||||||
|
m_baseUrl = baseUrl.trimmed();
|
||||||
|
while (m_baseUrl.endsWith('/'))
|
||||||
|
m_baseUrl.chop(1);
|
||||||
|
m_apiKey = apiKey.trimmed();
|
||||||
|
m_ignoreSsl = ignoreSsl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnraidClient::refresh()
|
||||||
|
{
|
||||||
|
if (m_busy || m_baseUrl.isEmpty())
|
||||||
|
return;
|
||||||
|
m_busy = true;
|
||||||
|
|
||||||
|
QNetworkRequest req(QUrl(m_baseUrl + "/graphql"));
|
||||||
|
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
req.setRawHeader("x-api-key", m_apiKey.toUtf8());
|
||||||
|
req.setTransferTimeout(10000);
|
||||||
|
|
||||||
|
if (m_ignoreSsl) {
|
||||||
|
// Zertifikatsprüfung komplett deaktivieren (selbstsignierte Zertifikate)
|
||||||
|
QSslConfiguration ssl = QSslConfiguration::defaultConfiguration();
|
||||||
|
ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||||
|
req.setSslConfiguration(ssl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject body{{"query", QString::fromLatin1(kQuery)}};
|
||||||
|
QNetworkReply *reply = m_nam->post(req, QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||||
|
|
||||||
|
if (m_ignoreSsl) {
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||||
|
[reply](const QList<QSslError> &) { reply->ignoreSslErrors(); });
|
||||||
|
}
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
||||||
|
m_busy = false;
|
||||||
|
handleReply(reply);
|
||||||
|
reply->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnraidClient::handleReply(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError && httpStatus == 0) {
|
||||||
|
// Unraids nginx bricht den TLS-Handshake ab, wenn der Hostname (SNI) nicht
|
||||||
|
// passt – z.B. bei https://<IP>. Das ist kein Zertifikatsfehler und kann
|
||||||
|
// clientseitig nicht ignoriert werden.
|
||||||
|
if (reply->errorString().contains("unrecognized name", Qt::CaseInsensitive)) {
|
||||||
|
emit requestFailed(tr("Server lehnt den Hostnamen ab (SNI). Bei HTTPS den "
|
||||||
|
"Hostnamen statt der IP verwenden (z.B. https://….myunraid.net "
|
||||||
|
"oder https://tower.local) – oder http:// nutzen."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit requestFailed(tr("Netzwerkfehler: %1").arg(reply->errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (httpStatus == 401 || httpStatus == 403) {
|
||||||
|
emit requestFailed(tr("Authentifizierung fehlgeschlagen – API-Key prüfen (HTTP %1)").arg(httpStatus));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseErr;
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &parseErr);
|
||||||
|
if (parseErr.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||||
|
emit requestFailed(tr("Ungültige Antwort vom Server (HTTP %1)").arg(httpStatus));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject root = doc.object();
|
||||||
|
QStringList gqlErrors;
|
||||||
|
for (const QJsonValue &e : root.value("errors").toArray())
|
||||||
|
gqlErrors << e.toObject().value("message").toString();
|
||||||
|
|
||||||
|
const QJsonObject data = root.value("data").toObject();
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
emit requestFailed(gqlErrors.isEmpty()
|
||||||
|
? tr("Leere Antwort vom Server (HTTP %1)").arg(httpStatus)
|
||||||
|
: tr("API-Fehler: %1").arg(gqlErrors.join("; ")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSnapshot snap = parseData(data);
|
||||||
|
snap.partialErrors = gqlErrors;
|
||||||
|
emit snapshotReady(snap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DiskInfo parseArrayDisk(const QJsonObject &o)
|
||||||
|
{
|
||||||
|
DiskInfo d;
|
||||||
|
d.name = o.value("name").toString();
|
||||||
|
d.device = o.value("device").toString();
|
||||||
|
d.status = o.value("status").toString();
|
||||||
|
d.sizeKb = toLL(o.value("size"));
|
||||||
|
d.fsSizeKb = toLL(o.value("fsSize"));
|
||||||
|
d.fsFreeKb = toLL(o.value("fsFree"));
|
||||||
|
d.fsUsedKb = toLL(o.value("fsUsed"));
|
||||||
|
d.numErrors = toLL(o.value("numErrors"));
|
||||||
|
d.temp = toTemp(o.value("temp"));
|
||||||
|
d.isSpinning = o.value("isSpinning").toBool(true);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSnapshot UnraidClient::parseData(const QJsonObject &data)
|
||||||
|
{
|
||||||
|
ServerSnapshot s;
|
||||||
|
|
||||||
|
// info
|
||||||
|
const QJsonObject info = data.value("info").toObject();
|
||||||
|
const QJsonObject cpu = info.value("cpu").toObject();
|
||||||
|
s.cpuBrand = cpu.value("brand").toString();
|
||||||
|
s.cpuCores = cpu.value("cores").toInt();
|
||||||
|
s.cpuThreads = cpu.value("threads").toInt();
|
||||||
|
|
||||||
|
const QJsonObject os = info.value("os").toObject();
|
||||||
|
s.hostname = os.value("hostname").toString();
|
||||||
|
s.distro = os.value("distro").toString();
|
||||||
|
s.release = os.value("release").toString();
|
||||||
|
s.kernel = os.value("kernel").toString();
|
||||||
|
s.bootTime = QDateTime::fromString(os.value("uptime").toString(), Qt::ISODate);
|
||||||
|
|
||||||
|
const QJsonObject core = info.value("versions").toObject().value("core").toObject();
|
||||||
|
s.unraidVersion = core.value("unraid").toString();
|
||||||
|
s.apiVersion = core.value("api").toString();
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
const QJsonObject metrics = data.value("metrics").toObject();
|
||||||
|
if (!metrics.isEmpty()) {
|
||||||
|
const QJsonValue cpuPct = metrics.value("cpu").toObject().value("percentTotal");
|
||||||
|
if (cpuPct.isDouble())
|
||||||
|
s.cpuPercent = cpuPct.toDouble();
|
||||||
|
const QJsonObject mem = metrics.value("memory").toObject();
|
||||||
|
s.memTotal = toLL(mem.value("total"));
|
||||||
|
s.memUsed = toLL(mem.value("used"));
|
||||||
|
s.memAvailable = toLL(mem.value("available"));
|
||||||
|
if (mem.value("percentTotal").isDouble())
|
||||||
|
s.memPercent = mem.value("percentTotal").toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
// array
|
||||||
|
const QJsonObject array = data.value("array").toObject();
|
||||||
|
s.arrayState = array.value("state").toString();
|
||||||
|
const QJsonObject kb = array.value("capacity").toObject().value("kilobytes").toObject();
|
||||||
|
s.capFreeKb = toLL(kb.value("free"));
|
||||||
|
s.capUsedKb = toLL(kb.value("used"));
|
||||||
|
s.capTotalKb = toLL(kb.value("total"));
|
||||||
|
|
||||||
|
for (const QJsonValue &v : array.value("disks").toArray())
|
||||||
|
s.dataDisks << parseArrayDisk(v.toObject());
|
||||||
|
for (const QJsonValue &v : array.value("parities").toArray())
|
||||||
|
s.parityDisks << parseArrayDisk(v.toObject());
|
||||||
|
for (const QJsonValue &v : array.value("caches").toArray())
|
||||||
|
s.cachePools << parseArrayDisk(v.toObject());
|
||||||
|
|
||||||
|
// physische Disks / SMART
|
||||||
|
for (const QJsonValue &v : data.value("disks").toArray()) {
|
||||||
|
const QJsonObject o = v.toObject();
|
||||||
|
PhysicalDiskInfo p;
|
||||||
|
p.device = o.value("device").toString();
|
||||||
|
p.model = o.value("name").toString();
|
||||||
|
p.vendor = o.value("vendor").toString();
|
||||||
|
p.serial = o.value("serialNum").toString();
|
||||||
|
p.interfaceType = o.value("interfaceType").toString();
|
||||||
|
p.smartStatus = o.value("smartStatus").toString();
|
||||||
|
p.sizeBytes = o.value("size").toDouble();
|
||||||
|
const QJsonValue t = o.value("temperature");
|
||||||
|
p.temperature = (t.isDouble() && !std::isnan(t.toDouble())) ? t.toDouble() : -1;
|
||||||
|
s.physicalDisks << p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker
|
||||||
|
for (const QJsonValue &v : data.value("docker").toObject().value("containers").toArray()) {
|
||||||
|
const QJsonObject o = v.toObject();
|
||||||
|
ContainerInfo c;
|
||||||
|
const QJsonArray names = o.value("names").toArray();
|
||||||
|
if (!names.isEmpty()) {
|
||||||
|
c.name = names.first().toString();
|
||||||
|
if (c.name.startsWith('/'))
|
||||||
|
c.name.remove(0, 1);
|
||||||
|
}
|
||||||
|
c.image = o.value("image").toString();
|
||||||
|
c.state = o.value("state").toString();
|
||||||
|
c.status = o.value("status").toString();
|
||||||
|
c.autoStart = o.value("autoStart").toBool();
|
||||||
|
s.containers << c;
|
||||||
|
}
|
||||||
|
std::sort(s.containers.begin(), s.containers.end(),
|
||||||
|
[](const ContainerInfo &a, const ContainerInfo &b) {
|
||||||
|
const bool ra = a.state == "RUNNING", rb = b.state == "RUNNING";
|
||||||
|
if (ra != rb)
|
||||||
|
return ra;
|
||||||
|
return a.name.localeAwareCompare(b.name) < 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
92
src/UnraidClient.h
Normal file
92
src/UnraidClient.h
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class QNetworkAccessManager;
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
// Array-/Pool-Disk (Werte in KB, wie von der API geliefert)
|
||||||
|
struct DiskInfo {
|
||||||
|
QString name;
|
||||||
|
QString device;
|
||||||
|
QString status; // DISK_OK, DISK_NP, ...
|
||||||
|
qint64 sizeKb = 0;
|
||||||
|
qint64 fsSizeKb = 0;
|
||||||
|
qint64 fsFreeKb = 0;
|
||||||
|
qint64 fsUsedKb = 0;
|
||||||
|
qint64 numErrors = 0;
|
||||||
|
int temp = -1; // -1 = unbekannt / spun down
|
||||||
|
bool isSpinning = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Physische Disk inkl. SMART (Größe in Bytes)
|
||||||
|
struct PhysicalDiskInfo {
|
||||||
|
QString device;
|
||||||
|
QString model; // "name" im Schema
|
||||||
|
QString vendor;
|
||||||
|
QString serial;
|
||||||
|
QString interfaceType;
|
||||||
|
QString smartStatus; // OK / UNKNOWN
|
||||||
|
double sizeBytes = 0;
|
||||||
|
double temperature = -1; // -1 = unbekannt
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ContainerInfo {
|
||||||
|
QString name;
|
||||||
|
QString image;
|
||||||
|
QString state; // RUNNING, PAUSED, EXITED
|
||||||
|
QString status; // "Up 2 days" etc.
|
||||||
|
bool autoStart = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ServerSnapshot {
|
||||||
|
// System
|
||||||
|
QString hostname;
|
||||||
|
QString distro, release, kernel;
|
||||||
|
QString unraidVersion, apiVersion;
|
||||||
|
QString cpuBrand;
|
||||||
|
int cpuCores = 0, cpuThreads = 0;
|
||||||
|
QDateTime bootTime;
|
||||||
|
double cpuPercent = -1;
|
||||||
|
qint64 memTotal = 0, memUsed = 0, memAvailable = 0; // Bytes
|
||||||
|
double memPercent = -1;
|
||||||
|
|
||||||
|
// Array
|
||||||
|
QString arrayState;
|
||||||
|
qint64 capFreeKb = 0, capUsedKb = 0, capTotalKb = 0;
|
||||||
|
QVector<DiskInfo> dataDisks;
|
||||||
|
QVector<DiskInfo> parityDisks;
|
||||||
|
QVector<DiskInfo> cachePools;
|
||||||
|
|
||||||
|
QVector<PhysicalDiskInfo> physicalDisks;
|
||||||
|
QVector<ContainerInfo> containers;
|
||||||
|
|
||||||
|
QStringList partialErrors; // GraphQL-Fehler bei Teilantworten
|
||||||
|
};
|
||||||
|
|
||||||
|
class UnraidClient : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit UnraidClient(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setConnection(const QString &baseUrl, const QString &apiKey, bool ignoreSsl);
|
||||||
|
void refresh();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void snapshotReady(const ServerSnapshot &snapshot);
|
||||||
|
void requestFailed(const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleReply(QNetworkReply *reply);
|
||||||
|
static ServerSnapshot parseData(const QJsonObject &data);
|
||||||
|
|
||||||
|
QNetworkAccessManager *m_nam;
|
||||||
|
QString m_baseUrl;
|
||||||
|
QString m_apiKey;
|
||||||
|
bool m_ignoreSsl = false;
|
||||||
|
bool m_busy = false;
|
||||||
|
};
|
||||||
42
src/main.cpp
Normal file
42
src/main.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QStyleFactory>
|
||||||
|
|
||||||
|
#include "MainWindow.h"
|
||||||
|
#include "Theme.h"
|
||||||
|
|
||||||
|
static void applyDarkPalette(QApplication &app)
|
||||||
|
{
|
||||||
|
QPalette p;
|
||||||
|
const QColor bg(Theme::BG), card(Theme::CARD), text(Theme::TEXT), accent(Theme::ACCENT);
|
||||||
|
p.setColor(QPalette::Window, bg);
|
||||||
|
p.setColor(QPalette::WindowText, text);
|
||||||
|
p.setColor(QPalette::Base, QColor("#11141a"));
|
||||||
|
p.setColor(QPalette::AlternateBase, card);
|
||||||
|
p.setColor(QPalette::Text, text);
|
||||||
|
p.setColor(QPalette::Button, card);
|
||||||
|
p.setColor(QPalette::ButtonText, text);
|
||||||
|
p.setColor(QPalette::Highlight, accent);
|
||||||
|
p.setColor(QPalette::HighlightedText, QColor("#11141a"));
|
||||||
|
p.setColor(QPalette::ToolTipBase, card);
|
||||||
|
p.setColor(QPalette::ToolTipText, text);
|
||||||
|
p.setColor(QPalette::PlaceholderText, QColor(Theme::MUTED));
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::Text, QColor(Theme::MUTED));
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(Theme::MUTED));
|
||||||
|
app.setPalette(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QApplication app(argc, argv);
|
||||||
|
QCoreApplication::setOrganizationName("tronax");
|
||||||
|
QCoreApplication::setApplicationName("unraid-monitor");
|
||||||
|
|
||||||
|
app.setStyle(QStyleFactory::create("Fusion"));
|
||||||
|
applyDarkPalette(app);
|
||||||
|
app.setStyleSheet(Theme::styleSheet());
|
||||||
|
|
||||||
|
MainWindow window;
|
||||||
|
window.show();
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue