Unraid Monitor for one-screen overview

This commit is contained in:
Tronax 2026-06-13 10:22:24 +02:00
commit e7c3d20c8e
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
13 changed files with 1545 additions and 0 deletions

41
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}