// data.cpp — HTTP client implementation for kanban_cpp. // // JSON parsing is intentionally manual + permissive: backend is "ours" and // payload shapes are stable. If we ever need a real parser, swap to nlohmann // or rapidjson; today the extra dep is not justified (KISS). #include "data.h" #include "core/http_request.h" #include "core/logger.h" #include #include namespace kanban_cpp { namespace { // Tiny helpers: scan JSON strings out of a raw buffer. NOT a real parser — // only handles flat-ish payloads our backend emits. Good enough for MVP. std::string find_str_field(const std::string& s, const std::string& key) { std::string needle = "\"" + key + "\":"; size_t p = s.find(needle); if (p == std::string::npos) return ""; p += needle.size(); while (p < s.size() && (s[p] == ' ' || s[p] == '\t')) ++p; if (p >= s.size() || s[p] != '"') return ""; ++p; std::string out; while (p < s.size() && s[p] != '"') { if (s[p] == '\\' && p + 1 < s.size()) { char c = s[p + 1]; if (c == 'n') out += '\n'; else if (c == 't') out += '\t'; else out += c; p += 2; continue; } out += s[p++]; } return out; } int64_t find_int_field(const std::string& s, const std::string& key) { std::string needle = "\"" + key + "\":"; size_t p = s.find(needle); if (p == std::string::npos) return 0; p += needle.size(); while (p < s.size() && (s[p] == ' ' || s[p] == '\t')) ++p; char* end = nullptr; long long v = std::strtoll(s.c_str() + p, &end, 10); return static_cast(v); } // Split JSON array of objects at depth 1. Returns each object as a substring. std::vector split_objects(const std::string& s) { std::vector out; int depth = 0; size_t start = 0; bool in_obj = false; for (size_t i = 0; i < s.size(); ++i) { char c = s[i]; if (c == '{') { if (depth == 0) { start = i; in_obj = true; } ++depth; } else if (c == '}') { --depth; if (depth == 0 && in_obj) { out.push_back(s.substr(start, i - start + 1)); in_obj = false; } } } return out; } fn_http::Response do_get(const std::string& url, int timeout_ms) { fn_http::Request req; req.method = "GET"; req.url = url; req.timeout_ms = timeout_ms; return fn_http::request(req); } fn_http::Response do_post_json(const std::string& url, const std::string& body, int timeout_ms) { fn_http::Request req; req.method = "POST"; req.url = url; req.timeout_ms = timeout_ms; req.body = body; req.headers.push_back({"Content-Type", "application/json"}); return fn_http::request(req); } } // namespace bool health(const ClientConfig& cfg) { // /health legacy tiene auth middleware → 500. Usar endpoint sync layer // (issue 0119) sin auth como ping. auto r = do_get(cfg.base_url + "/api/boards/issues/cards", cfg.timeout_ms); return r.status >= 200 && r.status < 300; } static std::string status_to_column(const std::string& s) { if (s == "pendiente" || s == "pending") return "backlog"; if (s == "en-curso" || s == "in-progress") return "doing"; if (s == "en-revision" || s == "review") return "review"; if (s == "done" || s == "completado") return "done"; if (s == "deferred") return "deferred"; return "backlog"; } std::vector list_cards(const ClientConfig& cfg, std::string& err) { // Issue 0119 sync layer: cards = issues + flows. Aqui solo issues; flows // viven en su propio tab/panel cuando se anada. auto r = do_get(cfg.base_url + "/api/boards/issues/cards", cfg.timeout_ms); if (r.status == 0) { err = "transport: " + r.error; return {}; } if (r.status >= 400) { err = "http " + std::to_string(r.status); return {}; } std::vector out; for (const auto& obj : split_objects(r.body)) { Card c; c.id = find_str_field(obj, "id"); c.title = find_str_field(obj, "title"); c.description = find_str_field(obj, "description"); c.status = find_str_field(obj, "status"); c.column_id = status_to_column(c.status); c.priority = find_str_field(obj, "priority"); c.position = find_int_field(obj, "position"); c.due_date = find_int_field(obj, "due_date"); c.assignee = find_str_field(obj, "assignee"); if (!c.id.empty()) out.push_back(c); } return out; } std::vector list_columns(const ClientConfig& /*cfg*/, std::string& /*err*/) { // Columnas fijas derivadas de taxonomia (issue 0103). return { {"backlog", "Backlog", 0}, {"doing", "Doing", 1}, {"review", "Review", 2}, {"done", "Done", 3}, {"deferred", "Deferred", 4}, }; } bool move_card(const ClientConfig& cfg, const std::string& card_id, const std::string& new_column_id, std::string& err) { std::string body = "{\"column_id\":\"" + new_column_id + "\"}"; auto r = do_post_json(cfg.base_url + "/api/cards/" + card_id + "/move", body, cfg.timeout_ms); if (r.status == 0) { err = "transport: " + r.error; return false; } if (r.status >= 400) { err = "http " + std::to_string(r.status); return false; } return true; } std::vector list_runs(const ClientConfig& cfg, std::string& err) { auto r = do_get(cfg.agent_runner_url + "/api/runs", cfg.timeout_ms); if (r.status == 0) { err = "transport: " + r.error; return {}; } if (r.status >= 400) { err = "http " + std::to_string(r.status); return {}; } std::vector out; for (const auto& obj : split_objects(r.body)) { AgentRunSummary s; s.id = find_str_field(obj, "id"); s.card_id = find_str_field(obj, "card_id"); s.branch = find_str_field(obj, "branch"); s.status = find_str_field(obj, "status"); s.started_at = find_int_field(obj, "started_at"); s.finished_at = find_int_field(obj, "finished_at"); if (!s.id.empty()) out.push_back(s); } return out; } bool launch_workflow(const ClientConfig& cfg, const std::string& card_id, std::string& out_run_id, std::string& err) { std::string body = "{\"card_id\":\"" + card_id + "\"}"; auto r = do_post_json(cfg.agent_runner_url + "/api/runs", body, cfg.timeout_ms); if (r.status == 0) { err = "transport: " + r.error; return false; } if (r.status >= 400) { err = "http " + std::to_string(r.status); return false; } out_run_id = find_str_field(r.body, "id"); return true; } } // namespace kanban_cpp