Loading debian/changelog +6 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,12 @@ mediadb (20260422+59) unstable; urgency=low * Fix cluster status bar: use health_loop assessment (isDegraded/ isCritical) instead of raw pclient snapshot which could show DEGRADED while the health loop had already suppressed it. * Add detailed import progress: ImportSession::progress() reports phase (starting/parsing/importing/replicating/finalizing), bytes fed, media count, and store count. Exposed via /api/import/status and /api/cluster/status import_progress object. * cluster.html: show import detail card with phase, MB transferred, media count and store progress during active imports. -- Jan Koester <jan.koester@tuxist.de> Wed, 22 Apr 2026 00:00:00 +0200 Loading html/cluster.html +12 −0 Original line number Diff line number Diff line Loading @@ -151,6 +151,18 @@ async function loadClusterStatus() { health.nodes_online === health.nodes_total ? '' : 'warn'); addSummaryCard(summary, 'Import', health.import_running ? 'In Progress' : 'Idle', health.import_running ? 'warn' : ''); if (health.import_running && health.import_progress) { const ip = health.import_progress; const phaseName = {starting:'Starting',parsing:'Parsing',importing:'Importing',replicating:'Replicating',finalizing:'Finalizing',done:'Done',failed:'Failed'}[ip.phase] || ip.phase; let detail = phaseName; if (ip.bytes_fed > 0) { const mb = (ip.bytes_fed / (1024*1024)).toFixed(1); detail += ' — ' + mb + ' MB'; } if (ip.media_done > 0) detail += ', ' + ip.media_done + ' media'; if (ip.stores_total > 0) detail += ', store ' + ip.stores_done + '/' + ip.stores_total; addSummaryCard(summary, 'Import Detail', detail, ''); } if (stores.stores) { const missing = stores.stores.filter(s => s.replicated_count < s.total_nodes).length; Loading src/app.cpp +27 −0 Original line number Diff line number Diff line Loading @@ -519,6 +519,8 @@ std::unique_ptr<ImportSession> App::begin_import() { import_running_.store(false); } else { std::cerr << "[import] begin_import: session created OK\n"; std::lock_guard<std::mutex> lk(import_mutex_); active_import_session_ = session.get(); } return session; } Loading @@ -527,6 +529,7 @@ void App::finish_import(bool ok, const std::string& error) { std::lock_guard<std::mutex> lk(import_mutex_); import_ok_.store(ok); if (!ok && !error.empty()) import_error_ = error; active_import_session_ = nullptr; import_running_.store(false); if (g_Cluster) g_Cluster->setImportRunning(false); } Loading Loading @@ -658,6 +661,16 @@ HttpResponse App::handle_import_status(const HttpRequest& req) { json_object* j = json_object_new_object(); if (import_running_.load()) { json_object_object_add(j, "status", json_object_new_string("running")); std::lock_guard<std::mutex> lk(import_mutex_); if (active_import_session_) { auto p = active_import_session_->progress(); json_object_object_add(j, "phase", json_object_new_string(p.phase.c_str())); json_object_object_add(j, "bytes_fed", json_object_new_int64(static_cast<int64_t>(p.bytes_fed))); json_object_object_add(j, "media_done", json_object_new_int(static_cast<int>(p.media_done))); json_object_object_add(j, "media_total", json_object_new_int(static_cast<int>(p.media_total))); json_object_object_add(j, "stores_done", json_object_new_int(static_cast<int>(p.stores_done))); json_object_object_add(j, "stores_total", json_object_new_int(static_cast<int>(p.stores_total))); } } else if (import_ok_.load()) { json_object_object_add(j, "status", json_object_new_string("done")); json_object_object_add(j, "message", json_object_new_string("import successful")); Loading Loading @@ -700,6 +713,20 @@ HttpResponse App::handle_cluster_status(const HttpRequest& req) { // Import status — always included regardless of cluster state json_object_object_add(j, "import_running", json_object_new_boolean(import_running_.load())); if (import_running_.load()) { std::lock_guard<std::mutex> lk(import_mutex_); if (active_import_session_) { auto p = active_import_session_->progress(); json_object* ji = json_object_new_object(); json_object_object_add(ji, "phase", json_object_new_string(p.phase.c_str())); json_object_object_add(ji, "bytes_fed", json_object_new_int64(static_cast<int64_t>(p.bytes_fed))); json_object_object_add(ji, "media_done", json_object_new_int(static_cast<int>(p.media_done))); json_object_object_add(ji, "media_total", json_object_new_int(static_cast<int>(p.media_total))); json_object_object_add(ji, "stores_done", json_object_new_int(static_cast<int>(p.stores_done))); json_object_object_add(ji, "stores_total", json_object_new_int(static_cast<int>(p.stores_total))); json_object_object_add(j, "import_progress", ji); } } if (!g_Cluster || !g_Cluster->isRunning()) { json_object_object_add(j, "enabled", json_object_new_boolean(false)); Loading src/app.h +3 −0 Original line number Diff line number Diff line Loading @@ -147,6 +147,9 @@ private: std::atomic<bool> import_ok_{false}; std::string import_error_; std::thread import_thread_; // Observer pointer to active session (owned by server ImportState). // Only valid while import_running_ is true, guarded by import_mutex_. const ImportSession* active_import_session_ = nullptr; }; } // namespace mediadb src/backend.cpp +36 −0 Original line number Diff line number Diff line Loading @@ -1033,6 +1033,42 @@ public: return error_msg_; } Progress progress() const override { Progress p; p.bytes_fed = total_fed_; p.stores_total = num_stores_; p.stores_done = touched_stores_.size(); p.media_total = num_media_; // per-store count (resets per store) p.media_done = total_media_; if (done_.load(std::memory_order_acquire)) { p.phase = ok_.load(std::memory_order_acquire) ? "done" : "failed"; } else if (parsing_done_.load(std::memory_order_acquire)) { p.phase = "replicating"; } else { switch (phase_) { case Phase::MAGIC: case Phase::NUM_STORES: p.phase = "starting"; break; case Phase::STORE_HEADER: case Phase::NUM_ALBUMS: case Phase::ALBUM_HEADER: case Phase::NUM_MEDIA: p.phase = "parsing"; break; case Phase::MEDIA_HEADER: case Phase::MEDIA_DATA: p.phase = "importing"; break; case Phase::DONE: p.phase = "finalizing"; break; } } return p; } private: // ---- read helpers (return false if not enough data) ---- Loading Loading
debian/changelog +6 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,12 @@ mediadb (20260422+59) unstable; urgency=low * Fix cluster status bar: use health_loop assessment (isDegraded/ isCritical) instead of raw pclient snapshot which could show DEGRADED while the health loop had already suppressed it. * Add detailed import progress: ImportSession::progress() reports phase (starting/parsing/importing/replicating/finalizing), bytes fed, media count, and store count. Exposed via /api/import/status and /api/cluster/status import_progress object. * cluster.html: show import detail card with phase, MB transferred, media count and store progress during active imports. -- Jan Koester <jan.koester@tuxist.de> Wed, 22 Apr 2026 00:00:00 +0200 Loading
html/cluster.html +12 −0 Original line number Diff line number Diff line Loading @@ -151,6 +151,18 @@ async function loadClusterStatus() { health.nodes_online === health.nodes_total ? '' : 'warn'); addSummaryCard(summary, 'Import', health.import_running ? 'In Progress' : 'Idle', health.import_running ? 'warn' : ''); if (health.import_running && health.import_progress) { const ip = health.import_progress; const phaseName = {starting:'Starting',parsing:'Parsing',importing:'Importing',replicating:'Replicating',finalizing:'Finalizing',done:'Done',failed:'Failed'}[ip.phase] || ip.phase; let detail = phaseName; if (ip.bytes_fed > 0) { const mb = (ip.bytes_fed / (1024*1024)).toFixed(1); detail += ' — ' + mb + ' MB'; } if (ip.media_done > 0) detail += ', ' + ip.media_done + ' media'; if (ip.stores_total > 0) detail += ', store ' + ip.stores_done + '/' + ip.stores_total; addSummaryCard(summary, 'Import Detail', detail, ''); } if (stores.stores) { const missing = stores.stores.filter(s => s.replicated_count < s.total_nodes).length; Loading
src/app.cpp +27 −0 Original line number Diff line number Diff line Loading @@ -519,6 +519,8 @@ std::unique_ptr<ImportSession> App::begin_import() { import_running_.store(false); } else { std::cerr << "[import] begin_import: session created OK\n"; std::lock_guard<std::mutex> lk(import_mutex_); active_import_session_ = session.get(); } return session; } Loading @@ -527,6 +529,7 @@ void App::finish_import(bool ok, const std::string& error) { std::lock_guard<std::mutex> lk(import_mutex_); import_ok_.store(ok); if (!ok && !error.empty()) import_error_ = error; active_import_session_ = nullptr; import_running_.store(false); if (g_Cluster) g_Cluster->setImportRunning(false); } Loading Loading @@ -658,6 +661,16 @@ HttpResponse App::handle_import_status(const HttpRequest& req) { json_object* j = json_object_new_object(); if (import_running_.load()) { json_object_object_add(j, "status", json_object_new_string("running")); std::lock_guard<std::mutex> lk(import_mutex_); if (active_import_session_) { auto p = active_import_session_->progress(); json_object_object_add(j, "phase", json_object_new_string(p.phase.c_str())); json_object_object_add(j, "bytes_fed", json_object_new_int64(static_cast<int64_t>(p.bytes_fed))); json_object_object_add(j, "media_done", json_object_new_int(static_cast<int>(p.media_done))); json_object_object_add(j, "media_total", json_object_new_int(static_cast<int>(p.media_total))); json_object_object_add(j, "stores_done", json_object_new_int(static_cast<int>(p.stores_done))); json_object_object_add(j, "stores_total", json_object_new_int(static_cast<int>(p.stores_total))); } } else if (import_ok_.load()) { json_object_object_add(j, "status", json_object_new_string("done")); json_object_object_add(j, "message", json_object_new_string("import successful")); Loading Loading @@ -700,6 +713,20 @@ HttpResponse App::handle_cluster_status(const HttpRequest& req) { // Import status — always included regardless of cluster state json_object_object_add(j, "import_running", json_object_new_boolean(import_running_.load())); if (import_running_.load()) { std::lock_guard<std::mutex> lk(import_mutex_); if (active_import_session_) { auto p = active_import_session_->progress(); json_object* ji = json_object_new_object(); json_object_object_add(ji, "phase", json_object_new_string(p.phase.c_str())); json_object_object_add(ji, "bytes_fed", json_object_new_int64(static_cast<int64_t>(p.bytes_fed))); json_object_object_add(ji, "media_done", json_object_new_int(static_cast<int>(p.media_done))); json_object_object_add(ji, "media_total", json_object_new_int(static_cast<int>(p.media_total))); json_object_object_add(ji, "stores_done", json_object_new_int(static_cast<int>(p.stores_done))); json_object_object_add(ji, "stores_total", json_object_new_int(static_cast<int>(p.stores_total))); json_object_object_add(j, "import_progress", ji); } } if (!g_Cluster || !g_Cluster->isRunning()) { json_object_object_add(j, "enabled", json_object_new_boolean(false)); Loading
src/app.h +3 −0 Original line number Diff line number Diff line Loading @@ -147,6 +147,9 @@ private: std::atomic<bool> import_ok_{false}; std::string import_error_; std::thread import_thread_; // Observer pointer to active session (owned by server ImportState). // Only valid while import_running_ is true, guarded by import_mutex_. const ImportSession* active_import_session_ = nullptr; }; } // namespace mediadb
src/backend.cpp +36 −0 Original line number Diff line number Diff line Loading @@ -1033,6 +1033,42 @@ public: return error_msg_; } Progress progress() const override { Progress p; p.bytes_fed = total_fed_; p.stores_total = num_stores_; p.stores_done = touched_stores_.size(); p.media_total = num_media_; // per-store count (resets per store) p.media_done = total_media_; if (done_.load(std::memory_order_acquire)) { p.phase = ok_.load(std::memory_order_acquire) ? "done" : "failed"; } else if (parsing_done_.load(std::memory_order_acquire)) { p.phase = "replicating"; } else { switch (phase_) { case Phase::MAGIC: case Phase::NUM_STORES: p.phase = "starting"; break; case Phase::STORE_HEADER: case Phase::NUM_ALBUMS: case Phase::ALBUM_HEADER: case Phase::NUM_MEDIA: p.phase = "parsing"; break; case Phase::MEDIA_HEADER: case Phase::MEDIA_DATA: p.phase = "importing"; break; case Phase::DONE: p.phase = "finalizing"; break; } } return p; } private: // ---- read helpers (return false if not enough data) ---- Loading