Files
egutierrez ff67e4e069 chore: auto-commit (4 archivos)
- app.md
- appicon.ico
- views.cpp
- work_tab.cpp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:31:34 +02:00

379 lines
14 KiB
C++

// Tab "Work" — issue 0102.
//
// Subprocess call a `dev_console work dashboard` con cache 30s.
#include "work_tab.h"
#include "imgui.h"
#include "core/tokens.h"
#include "core/page_header.h"
#include "core/empty_state.h"
#include "core/badge.h"
#include "data_table/data_table.h"
#include "core/data_table_types.h"
#include "nlohmann/json.hpp"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <sstream>
#include <string>
#include <vector>
#ifdef _WIN32
#define POPEN _popen
#define PCLOSE _pclose
#else
#define POPEN popen
#define PCLOSE pclose
#endif
using json = nlohmann::json;
namespace fs = std::filesystem;
namespace {
struct IssueStats { int total{0}, pendiente{0}, in_progress{0}, bloqueado{0}, completado{0}; };
struct FlowSlim {
std::string id, name, status, pattern, risk, priority;
int acceptance_pct{0}, dod_pct{0}, user_facing_pct{0};
std::vector<std::string> apps;
};
struct IssueSlim {
std::string id, title, status, type, priority;
std::vector<std::string> domain, depends;
bool deps_resolved{false};
int acceptance_pct{0};
};
struct WorkData {
IssueStats issue_stats;
IssueStats flow_stats;
std::vector<FlowSlim> flows;
std::vector<IssueSlim> top_issues;
long long fetched_at_ms{0};
std::string fetch_error;
};
static WorkData g_data;
static bool g_first_fetch_done = false;
static const long long kCacheTtlMs = 30 * 1000;
// Localiza el binario dev_console.
static std::string find_dev_console() {
const char* root = std::getenv("FN_REGISTRY_ROOT");
std::vector<fs::path> candidates;
if (root && *root) candidates.emplace_back(fs::path(root) / "apps/dev_console/dev_console");
candidates.emplace_back("./apps/dev_console/dev_console");
candidates.emplace_back("../../../apps/dev_console/dev_console"); // from build/<cfg>/bin
candidates.emplace_back("dev_console");
for (auto& p : candidates) {
std::error_code ec;
if (fs::exists(p, ec)) return p.string();
}
return "dev_console"; // fallback: depend on PATH
}
static long long now_ms() {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
static bool fetch_work(WorkData& out) {
std::string bin = find_dev_console();
std::string cmd = bin + " work dashboard 2>/dev/null";
FILE* pipe = POPEN(cmd.c_str(), "r");
if (!pipe) {
out.fetch_error = "popen() failed for " + bin;
return false;
}
std::string body;
char buf[4096];
while (fgets(buf, sizeof(buf), pipe)) body.append(buf);
int rc = PCLOSE(pipe);
if (rc != 0) {
out.fetch_error = "dev_console exit " + std::to_string(rc) + " (binary at " + bin + ")";
return false;
}
try {
json j = json::parse(body);
auto parse_stats = [](const json& j, IssueStats& s) {
s.total = j.value("total", 0);
s.pendiente = j.value("pendiente", 0);
s.in_progress = j.value("in_progress", 0);
s.bloqueado = j.value("bloqueado", 0);
s.completado = j.value("completado", 0);
};
if (j.contains("issue_stats")) parse_stats(j["issue_stats"], out.issue_stats);
if (j.contains("flow_stats")) parse_stats(j["flow_stats"], out.flow_stats);
out.flows.clear();
if (j.contains("flows") && j["flows"].is_array()) {
for (auto& f : j["flows"]) {
FlowSlim fs;
fs.id = f.value("id", "");
fs.name = f.value("name", "");
fs.status = f.value("status", "");
fs.pattern = f.value("pattern", "");
fs.risk = f.value("risk", "");
fs.priority = f.value("priority", "");
fs.acceptance_pct = f.value("acceptance_pct", 0);
fs.dod_pct = f.value("dod_pct", 0);
fs.user_facing_pct = f.value("user_facing_pct", 0);
if (f.contains("apps") && f["apps"].is_array()) {
for (auto& a : f["apps"]) fs.apps.push_back(a.get<std::string>());
}
out.flows.push_back(std::move(fs));
}
}
out.top_issues.clear();
if (j.contains("top_issues") && j["top_issues"].is_array()) {
for (auto& i : j["top_issues"]) {
IssueSlim is;
is.id = i.value("id", "");
is.title = i.value("title", "");
is.status = i.value("status", "");
is.type = i.value("type", "");
is.priority = i.value("priority", "");
is.deps_resolved = i.value("deps_resolved", false);
is.acceptance_pct = i.value("acceptance_pct", 0);
if (i.contains("domain") && i["domain"].is_array()) {
for (auto& d : i["domain"]) is.domain.push_back(d.get<std::string>());
}
if (i.contains("depends") && i["depends"].is_array()) {
for (auto& d : i["depends"]) is.depends.push_back(d.get<std::string>());
}
out.top_issues.push_back(std::move(is));
}
}
out.fetch_error.clear();
out.fetched_at_ms = now_ms();
return true;
} catch (const std::exception& e) {
out.fetch_error = std::string("parse: ") + e.what();
return false;
}
}
static void maybe_refetch(bool force = false) {
long long now = now_ms();
if (!force && g_first_fetch_done && (now - g_data.fetched_at_ms) < kCacheTtlMs) return;
fetch_work(g_data);
g_first_fetch_done = true;
}
static const char* join_apps(const std::vector<std::string>& apps, std::string& buf) {
buf.clear();
for (size_t i = 0; i < apps.size(); ++i) {
if (i) buf += ", ";
buf += apps[i];
}
return buf.c_str();
}
static const char* join_domain(const std::vector<std::string>& dom, std::string& buf) {
buf.clear();
for (size_t i = 0; i < dom.size(); ++i) {
if (i) buf += ",";
buf += dom[i];
}
return buf.c_str();
}
static ImVec4 prio_color(const std::string& prio) {
if (prio == "alta" || prio == "high") return fn_tokens::colors::error;
if (prio == "media" || prio == "medium") return fn_tokens::colors::warning;
return fn_tokens::colors::text_muted;
}
static void draw_kpi_block(const char* title, const IssueStats& s) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::TextUnformatted(title);
ImGui::PopStyleColor();
ImGui::Text("total=%d pendiente=%d in-progress=%d bloqueado=%d completado=%d",
s.total, s.pendiente, s.in_progress, s.bloqueado, s.completado);
}
} // namespace
void draw_work_tab() {
maybe_refetch();
// Header + refresh
page_header_begin("Work", "Issues + flows + KPIs (issue 0102)");
if (ImGui::Button("Refresh")) maybe_refetch(true);
ImGui::SameLine();
long long age_ms = now_ms() - g_data.fetched_at_ms;
long long age_s = age_ms / 1000;
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("fetched %lld s ago", age_s);
ImGui::PopStyleColor();
page_header_end();
if (!g_data.fetch_error.empty()) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::Text("dev_console error: %s", g_data.fetch_error.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::TextWrapped("Build the binary with: cd apps/dev_console && go build -o dev_console .");
return;
}
// KPI block
ImGui::Spacing();
draw_kpi_block("Issues", g_data.issue_stats);
ImGui::Spacing();
draw_kpi_block("Flows", g_data.flow_stats);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Flows table — migrado a data_table::render (issue 0107g)
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::TextUnformatted("Flows");
ImGui::PopStyleColor();
{
static data_table::State g_st_flows;
static std::vector<std::string> g_back_flows;
static std::vector<const char*> g_ptrs_flows;
g_back_flows.clear();
std::string tmp_buf;
for (const auto& f : g_data.flows) {
g_back_flows.push_back(f.id);
g_back_flows.push_back(f.name);
g_back_flows.push_back(f.pattern.empty() ? "-" : f.pattern);
g_back_flows.push_back(f.status);
g_back_flows.push_back(f.risk);
g_back_flows.push_back(std::to_string(f.acceptance_pct) + "%");
g_back_flows.push_back(std::to_string(f.dod_pct) + "%");
g_back_flows.push_back(std::to_string(f.user_facing_pct) + "%");
}
g_ptrs_flows.clear();
for (const auto& s : g_back_flows) g_ptrs_flows.push_back(s.c_str());
data_table::TableInput tbl;
tbl.name = "flows_work";
tbl.headers = {"ID", "Name", "Pattern", "Status", "Risk", "Accept", "DoD", "UserFace"};
tbl.types = {
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String,
};
tbl.cells = g_ptrs_flows.empty() ? nullptr : g_ptrs_flows.data();
tbl.rows = (int)g_data.flows.size();
tbl.cols = 8;
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
// Status → CategoricalChip
tbl.column_specs[3].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[3].chips = {
{"activo", "#22c55e"}, {"done", "#22c55e"},
{"in-progress","#f59e0b"}, {"bloqueado","#ef4444"},
{"pendiente", "#a3a3a3"},
};
// Risk → CategoricalChip
tbl.column_specs[4].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[4].chips = {
{"alta", "#ef4444"}, {"high", "#ef4444"},
{"media", "#f59e0b"}, {"medium", "#f59e0b"},
{"baja", "#22c55e"}, {"low", "#22c55e"},
};
ImGui::BeginChild("##flows_work_host", ImVec2(-1, 220));
data_table::render("##flows_work_dt", {tbl}, g_st_flows, nullptr);
ImGui::EndChild();
}
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::TextUnformatted("Top issues (priority alta, not done)");
ImGui::PopStyleColor();
// Top issues table — migrado a data_table::render (issue 0107g)
// Nota: la columna Deps original tenia colores condicionales (verde/amber segun deps_resolved).
// Mapeado a CategoricalChip: "-" (gris), "OK" (verde), "blocked" (amber).
{
static data_table::State g_st_issues;
static std::vector<std::string> g_back_issues;
static std::vector<const char*> g_ptrs_issues;
g_back_issues.clear();
std::string dom_buf;
for (const auto& iss : g_data.top_issues) {
g_back_issues.push_back(iss.id);
g_back_issues.push_back(iss.title);
g_back_issues.push_back(iss.type);
// domain: join con coma
dom_buf.clear();
for (size_t k = 0; k < iss.domain.size(); ++k) {
if (k) dom_buf += ",";
dom_buf += iss.domain[k];
}
g_back_issues.push_back(dom_buf);
g_back_issues.push_back(iss.status);
// deps: mostrar "-" / "OK" / "blocked"
if (iss.depends.empty()) {
g_back_issues.push_back("-");
} else if (iss.deps_resolved) {
g_back_issues.push_back("OK");
} else {
g_back_issues.push_back("blocked");
}
g_back_issues.push_back(iss.priority);
}
g_ptrs_issues.clear();
for (const auto& s : g_back_issues) g_ptrs_issues.push_back(s.c_str());
data_table::TableInput tbl;
tbl.name = "top_issues_work";
tbl.headers = {"ID", "Title", "Type", "Domain", "Status", "Deps", "Prio"};
tbl.types = {
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String,
};
tbl.cells = g_ptrs_issues.empty() ? nullptr : g_ptrs_issues.data();
tbl.rows = (int)g_data.top_issues.size();
tbl.cols = 7;
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
// Status → CategoricalChip
tbl.column_specs[4].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[4].chips = {
{"pendiente", "#a3a3a3"},
{"in-progress", "#f59e0b"},
{"bloqueado", "#ef4444"},
{"completado", "#22c55e"},
};
// Deps → CategoricalChip (verde OK / amber blocked / gris -)
tbl.column_specs[5].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[5].chips = {
{"OK", "#22c55e"},
{"blocked", "#f59e0b"},
{"-", "#a3a3a3"},
};
// Prio → CategoricalChip
tbl.column_specs[6].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[6].chips = {
{"alta", "#ef4444"}, {"high", "#ef4444"},
{"media", "#f59e0b"}, {"medium", "#f59e0b"},
{"baja", "#22c55e"}, {"low", "#22c55e"},
};
ImGui::BeginChild("##top_issues_work_host", ImVec2(-1, -1));
data_table::render("##top_issues_work_dt", {tbl}, g_st_issues, nullptr);
ImGui::EndChild();
}
}