#include "dod_evidence_panel.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include #include #include #include #include #ifdef _WIN32 # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # endif # include # include #endif namespace fn_viz { namespace { // Mapea icon_id (helpers) -> macro TI_*. const char* ti_for(const std::string& icon_id) { if (icon_id == "circle-dot") return TI_CIRCLE_DOT; if (icon_id == "circle-check") return TI_CIRCLE_CHECK; if (icon_id == "circle-x") return TI_CIRCLE_X; return TI_CIRCLE_DASHED; } // Color por token (0=neutral, 1=info, 2=success, 3=danger). ImVec4 color_for_token(int tok) { using namespace fn_tokens; switch (tok) { case 1: return colors::info; case 2: return colors::success; case 3: return colors::error; default: return colors::text_muted; } } // Abre una URL en el browser por defecto. Stub en plataformas no soportadas. void open_url(const std::string& url) { #ifdef _WIN32 ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL); #else // xdg-open con argumento quoteado minimo (sin caracteres especiales no // se gana mucho con quoting completo). Si el caller pasa una URL // controlada por usuario, esto deberia validarse antes. std::string cmd = "xdg-open '" + url + "' >/dev/null 2>&1 &"; int rc = std::system(cmd.c_str()); (void)rc; #endif } // Lee primeras N lineas de un archivo log. Devuelve "" si no se puede abrir. std::string read_first_lines(const std::string& path, int max_lines) { std::ifstream f(path); if (!f.is_open()) return ""; std::ostringstream out; std::string line; int n = 0; while (n < max_lines && std::getline(f, line)) { out << line << "\n"; ++n; } return out.str(); } // Carga entero el archivo. Truncado a 64KiB para popup. std::string read_full_capped(const std::string& path, size_t cap = 64 * 1024) { std::ifstream f(path, std::ios::binary); if (!f.is_open()) return ""; std::string s; f.seekg(0, std::ios::end); std::streamsize sz = f.tellg(); if (sz < 0) return ""; size_t n = static_cast(sz); if (n > cap) n = cap; s.resize(n); f.seekg(0, std::ios::beg); f.read(&s[0], static_cast(n)); return s; } void render_preview_screenshot(const DodEvidence& ev) { // Sin GL context cacheado ni stb_image load aqui: stub textual. La carga // real requiere mantener un map que el panel no // posee. Las apps consumidoras pueden override esto sustituyendo la // funcion (issue futura: callback render_preview). ImGui::TextDisabled("[screenshot] %s", ev.payload_path.c_str()); } void render_preview_log(const DodEvidence& ev, int row_idx) { if (ev.payload_path.empty()) { ImGui::TextDisabled("(no log path)"); return; } std::string head = read_first_lines(ev.payload_path, 5); if (head.empty()) { ImGui::TextDisabled("(log not readable)"); } else { ImGui::TextWrapped("%s", head.c_str()); } char btn_id[64]; std::snprintf(btn_id, sizeof(btn_id), "Show full##log_%d", row_idx); if (ImGui::SmallButton(btn_id)) { ImGui::OpenPopup(btn_id); } if (ImGui::BeginPopup(btn_id)) { std::string full = read_full_capped(ev.payload_path); ImGui::InputTextMultiline("##log_full", full.data(), full.size() + 1, ImVec2(600, 400), ImGuiInputTextFlags_ReadOnly); ImGui::EndPopup(); } } void render_preview_url(const DodEvidence& ev) { ImGui::TextUnformatted(TI_EXTERNAL_LINK); ImGui::SameLine(); if (ImGui::SmallButton(ev.payload_url.c_str())) { open_url(ev.payload_url); } } void render_preview_cmd(const DodEvidence& ev, const DodItem& item) { using namespace fn_tokens; const bool match = (ev.payload_text == item.expected); ImVec4 ok = colors::success; ImVec4 bad = colors::error; if (ImGui::BeginTable("##cmd_diff", 2, ImGuiTableFlags_SizingStretchSame)) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("expected"); ImGui::TextWrapped("%s", item.expected.c_str()); ImGui::TableSetColumnIndex(1); ImGui::TextDisabled("actual"); ImGui::PushStyleColor(ImGuiCol_Text, match ? ok : bad); ImGui::TextWrapped("%s", ev.payload_text.c_str()); ImGui::PopStyleColor(); ImGui::EndTable(); } } } // namespace void render_dod_evidence_panel(DodPanelState& state) { using namespace fn_tokens; namespace P = ::fn_viz::dod_panel; P::StatusCounts c = P::count_status(state); // Header. ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); ImGui::Text("run_id:"); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextUnformatted(state.run_id.empty() ? "(none)" : state.run_id.c_str()); ImGui::SameLine(); ImGui::TextDisabled(" | "); ImGui::SameLine(); ImGui::Text("total %d ", c.total); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, colors::info); ImGui::Text("done %d ", c.done); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, colors::success); ImGui::Text("validated %d ", c.validated); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, colors::error); ImGui::Text("failed %d", c.failed); ImGui::PopStyleColor(); if (c.missing_required > 0) { ImGui::SameLine(); ImGui::TextDisabled(" | "); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, colors::error); ImGui::Text("required missing: %d", c.missing_required); ImGui::PopStyleColor(); } ImGui::Separator(); // Tabla principal. const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp; if (!ImGui::BeginTable("##dod_table", 6, flags)) return; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f); ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_WidthFixed, 120.0f); ImGui::TableSetupColumn("kind", ImGuiTableColumnFlags_WidthFixed, 80.0f); ImGui::TableSetupColumn("expected", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("evidence", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("actions", ImGuiTableColumnFlags_WidthFixed, 130.0f); ImGui::TableHeadersRow(); int row_idx = 0; for (const auto& item : state.items) { ImGui::TableNextRow(); ImGui::PushID(row_idx); // Col 0: status icon. ImGui::TableSetColumnIndex(0); const std::string icon_id = P::status_icon_id(item.status); const int tok = P::status_color_token(item.status); ImGui::PushStyleColor(ImGuiCol_Text, color_for_token(tok)); ImGui::TextUnformatted(ti_for(icon_id)); ImGui::PopStyleColor(); // Col 1: id (+ required badge si missing). ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(item.id.c_str()); const DodEvidence* ev = P::find_evidence(state, item.id); const bool resolved = (item.status == "done" || item.status == "validated"); if (item.required && ev == nullptr && !resolved) { ImGui::PushStyleColor(ImGuiCol_Text, colors::error); ImGui::TextUnformatted("required, missing"); ImGui::PopStyleColor(); } // Col 2: kind. ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(item.kind.c_str()); // Col 3: expected. ImGui::TableSetColumnIndex(3); ImGui::TextWrapped("%s", item.expected.c_str()); // Col 4: evidence preview. ImGui::TableSetColumnIndex(4); if (ev == nullptr) { ImGui::TextDisabled("(no evidence)"); } else if (item.kind == "screenshot") { render_preview_screenshot(*ev); } else if (item.kind == "log") { render_preview_log(*ev, row_idx); } else if (item.kind == "url") { render_preview_url(*ev); } else if (item.kind == "cmd") { render_preview_cmd(*ev, item); } else { ImGui::TextDisabled("(unknown kind: %s)", item.kind.c_str()); } // Col 5: actions. ImGui::TableSetColumnIndex(5); if (ev != nullptr) { ImGui::PushStyleColor(ImGuiCol_Text, colors::success); const bool clicked_v = ImGui::SmallButton(TI_CHECK " Validate"); ImGui::PopStyleColor(); if (clicked_v && state.on_validate) state.on_validate(item.id); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, colors::error); const bool clicked_r = ImGui::SmallButton(TI_X " Reject"); ImGui::PopStyleColor(); if (clicked_r && state.on_reject) state.on_reject(item.id); } else { ImGui::TextDisabled("—"); } ImGui::PopID(); ++row_idx; } ImGui::EndTable(); } } // namespace fn_viz