diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index bbdef1a..bcd1a8d 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -67,6 +67,7 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") { setStatus(Running); debug("Storage module started."); QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); emit startCompleted(); } })) { @@ -156,6 +157,8 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") { m_uploadStatus = "Upload completed!"; emit uploadProgressChanged(); emit uploadStatusChanged(); + + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); } })) { qWarning() << "StorageWidget: failed to subscribe to storageUploadProgress events"; @@ -550,13 +553,13 @@ void StorageBackend::remove(const QString& cid) { LogosResult result = m_logos->storage_module.remove(cid); if (!result.success) { - debug("StorageBackend::remove failed with error=" + result.getError()); - return; + // Log but continue — manifest might not have local data, remove it from the list anyway + debug("StorageBackend::remove: storage returned error=" + result.getError() + " (removing from list regardless)"); + } else { + debug("Cid " + cid + " removed from storage."); } - debug("Cid " + cid + " removed."); - - // Remove from manifests list + // Always remove from manifests list for (int i = 0; i < m_manifests.size(); ++i) { if (m_manifests[i].toMap().value("cid").toString() == cid) { m_manifests.removeAt(i); @@ -564,6 +567,8 @@ void StorageBackend::remove(const QString& cid) { break; } } + + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); } void StorageBackend::fetch(const QString& cid) { @@ -711,12 +716,33 @@ void StorageBackend::space() { return; } - debug(QString("Space datasetSize %1").arg(result.getInt("totalBlocks"))); - debug(QString("Space quotaMaxBytes %1").arg(result.getInt("quotaMaxBytes"))); - debug(QString("Space quotaUsedBytes %1").arg(result.getInt("quotaUsedBytes"))); - debug(QString("Space quotaReservedBytes %1").arg(result.getInt("quotaReservedBytes"))); + qDebug() << "StorageBackend::space raw value:" << result.value; + + static constexpr qint64 DEFAULT_QUOTA = 20LL * 1024 * 1024 * 1024; // 20 GB + + // Check config for a quota-max-bytes override + qint64 configQuota = 0; + QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); + if (!doc.isNull()) { + configQuota = doc.object().value("quota-max-bytes").toVariant().toLongLong(); + } + + qint64 apiQuota = result.getInt("quotaMaxBytes"); + m_quotaMaxBytes = apiQuota > 0 ? apiQuota : (configQuota > 0 ? configQuota : DEFAULT_QUOTA); + m_quotaUsedBytes = result.getInt("quotaUsedBytes"); + m_quotaReservedBytes = result.getInt("quotaReservedBytes"); + emit quotaChanged(); + + debug(QString("Space totalBlocks %1").arg(result.getInt("totalBlocks"))); + debug(QString("Space quotaMaxBytes %1").arg(m_quotaMaxBytes)); + debug(QString("Space quotaUsedBytes %1").arg(m_quotaUsedBytes)); + debug(QString("Space quotaReservedBytes %1").arg(m_quotaReservedBytes)); } +qint64 StorageBackend::quotaMaxBytes() const { return m_quotaMaxBytes; } +qint64 StorageBackend::quotaUsedBytes() const { return m_quotaUsedBytes; } +qint64 StorageBackend::quotaReservedBytes() const { return m_quotaReservedBytes; } + void StorageBackend::updateLogLevel(const QString& logLevel) { qDebug() << "StorageBackend::updateLogLevel called with logLevel=" << logLevel; diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 79fb1ce..0745991 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -34,6 +34,9 @@ class StorageBackend : public QObject { Q_PROPERTY(int uploadProgress READ uploadProgress NOTIFY uploadProgressChanged) Q_PROPERTY(QString uploadStatus READ uploadStatus NOTIFY uploadStatusChanged) Q_PROPERTY(QVariantList manifests READ manifests NOTIFY manifestsChanged) + Q_PROPERTY(qint64 quotaMaxBytes READ quotaMaxBytes NOTIFY quotaChanged) + Q_PROPERTY(qint64 quotaUsedBytes READ quotaUsedBytes NOTIFY quotaChanged) + Q_PROPERTY(qint64 quotaReservedBytes READ quotaReservedBytes NOTIFY quotaChanged) public: enum StorageStatus { Stopped = 0, Starting, Running, Stopping, Destroyed }; @@ -46,6 +49,9 @@ class StorageBackend : public QObject { int uploadProgress() const; QString uploadStatus() const; QVariantList manifests() const; + qint64 quotaMaxBytes() const; + qint64 quotaUsedBytes() const; + qint64 quotaReservedBytes() const; Q_INVOKABLE static QString defaultDataDir(); @@ -91,6 +97,7 @@ class StorageBackend : public QObject { void uploadProgressChanged(); void uploadStatusChanged(); void manifestsChanged(); + void quotaChanged(); private slots: @@ -110,4 +117,7 @@ class StorageBackend : public QObject { qint64 m_uploadTotalBytes = 0; qint64 m_uploadedBytes = 0; QVariantList m_manifests; + qint64 m_quotaMaxBytes = 0; + qint64 m_quotaUsedBytes = 0; + qint64 m_quotaReservedBytes = 0; }; diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index c256c7f..f8976f3 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -128,6 +128,17 @@ Rectangle { function updateLogLevel(logLevel) {} property var manifests: [] + property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default + property var quotaUsedBytes: 0 + property var quotaReservedBytes: 0 + } + + function formatBytes(bytes) { + if (bytes <= 0) return "0 B" + if (bytes < 1024) return bytes + " B" + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB" + if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" + return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" } Text { @@ -397,11 +408,96 @@ Rectangle { anchors.horizontalCenter: parent.horizontalCenter } + // ── Disk space bar ──────────────────────────────────────────────────── + Item { + id: spaceBarSection + anchors.top: manifestsTitle.bottom + anchors.topMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 40 + height: root.backend.quotaMaxBytes > 0 ? 36 : 20 + + readonly property real total: root.backend.quotaMaxBytes + readonly property real used: root.backend.quotaUsedBytes + readonly property real reserved: root.backend.quotaReservedBytes + + // No quota configured + Text { + anchors.centerIn: parent + text: "No quota configured" + color: "#555555" + font.pixelSize: 11 + visible: spaceBarSection.total <= 0 + } + + // Background track + Rectangle { + id: spaceBarTrack + visible: spaceBarSection.total > 0 + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: 14 + radius: 7 + color: "#2a2a2a" + border.color: "#3a3a3a" + border.width: 1 + clip: true + + // Used (green) + Rectangle { + width: Math.min(parent.width * (spaceBarSection.used / spaceBarSection.total), parent.width) + height: parent.height + radius: parent.radius + color: "#4CAF50" + } + + // Reserved (orange), stacked after used + Rectangle { + x: parent.width * (spaceBarSection.used / spaceBarSection.total) + width: Math.min(parent.width * (spaceBarSection.reserved / spaceBarSection.total), + parent.width - x) + height: parent.height + color: "#FF9800" + } + } + + // Labels + Row { + visible: spaceBarSection.total > 0 + anchors.top: spaceBarTrack.bottom + anchors.topMargin: 4 + anchors.horizontalCenter: parent.horizontalCenter + spacing: 16 + + Text { + text: "Used: " + root.formatBytes(spaceBarSection.used) + color: "#4CAF50" + font.pixelSize: 10 + } + Text { + text: "Reserved: " + root.formatBytes(spaceBarSection.reserved) + color: "#FF9800" + font.pixelSize: 10 + } + Text { + text: "Free: " + root.formatBytes(spaceBarSection.total - spaceBarSection.used - spaceBarSection.reserved) + color: "#888888" + font.pixelSize: 10 + } + Text { + text: "Total: " + root.formatBytes(spaceBarSection.total) + color: "#555555" + font.pixelSize: 10 + } + } + } + Row { id: manifestInputRow spacing: 8 - anchors.top: manifestsTitle.bottom - anchors.topMargin: 8 + anchors.top: spaceBarSection.bottom + anchors.topMargin: 16 anchors.horizontalCenter: parent.horizontalCenter TextField {