#include "data.h" #include #include #include #include #include "core/http_request.h" #include "core/sse_client.h" #include "core/logger.h" #include namespace kanban { using json = nlohmann::json; State& state() { static State s; return s; } static std::unique_ptr g_sse; static std::vector j_arr(const json& j, const char* key) { std::vector out; if (!j.contains(key) || !j[key].is_array()) return out; for (const auto& v : j[key]) { if (v.is_string()) out.push_back(v.get()); } return out; } static std::string j_str(const json& j, const char* key) { if (!j.contains(key) || !j[key].is_string()) return ""; return j[key].get(); } static Issue parse_issue(const json& j) { Issue i; i.id = j_str(j, "id"); i.title = j_str(j, "title"); i.status = j_str(j, "status"); i.type = j_str(j, "type"); i.scope = j_str(j, "scope"); i.priority = j_str(j, "priority"); i.domain = j_arr(j, "domain"); i.tags = j_arr(j, "tags"); i.depends = j_arr(j, "depends"); i.blocks = j_arr(j, "blocks"); i.related = j_arr(j, "related"); i.flow = j_str(j, "flow"); i.file_path = j_str(j, "file_path"); if (j.contains("completed") && j["completed"].is_boolean()) i.completed = j["completed"].get(); i.body = j_str(j, "body"); return i; } static Flow parse_flow(const json& j) { Flow f; f.id = j_str(j, "id"); f.title = j_str(j, "title"); f.status = j_str(j, "status"); f.kind = j_str(j, "kind"); f.tags = j_arr(j, "tags"); f.file_path = j_str(j, "file_path"); f.body = j_str(j, "body"); return f; } static fn_http::Response http_get(const std::string& path) { fn_http::Request req; req.method = "GET"; req.url = state().backend_url + path; req.timeout_ms = 5000; return fn_http::request(req); } static fn_http::Response http_patch_json(const std::string& path, const std::string& body) { fn_http::Request req; req.method = "PATCH"; req.url = state().backend_url + path; req.timeout_ms = 5000; req.headers.push_back({"Content-Type", "application/json"}); req.body = body; return fn_http::request(req); } bool refresh_issues() { auto resp = http_get("/api/issues"); if (resp.status != 200) { std::lock_guard g(state().mu); state().last_error = "issues fetch: " + std::to_string(resp.status) + " " + resp.error; return false; } auto j = json::parse(resp.body, nullptr, false); if (j.is_discarded() || !j.is_array()) { std::lock_guard g(state().mu); state().last_error = "issues: invalid JSON"; return false; } std::vector out; out.reserve(j.size()); for (const auto& it : j) out.push_back(parse_issue(it)); std::lock_guard g(state().mu); state().issues = std::move(out); state().last_refresh_ns = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count(); state().last_error.clear(); return true; } bool refresh_flows() { auto resp = http_get("/api/flows"); if (resp.status != 200) { std::lock_guard g(state().mu); state().last_error = "flows fetch: " + std::to_string(resp.status); return false; } auto j = json::parse(resp.body, nullptr, false); if (j.is_discarded() || !j.is_array()) return false; std::vector out; out.reserve(j.size()); for (const auto& it : j) out.push_back(parse_flow(it)); std::lock_guard g(state().mu); state().flows = std::move(out); return true; } bool refresh_meta() { auto resp = http_get("/api/meta"); if (resp.status != 200) return false; auto j = json::parse(resp.body, nullptr, false); if (j.is_discarded()) return false; std::lock_guard g(state().mu); state().meta.statuses = j_arr(j, "statuses"); state().meta.priorities = j_arr(j, "priorities"); state().meta.scopes = j_arr(j, "scopes"); state().meta.types = j_arr(j, "types"); return true; } bool refresh_issue_detail(const std::string& id) { auto resp = http_get("/api/issues/" + id); if (resp.status != 200) return false; auto j = json::parse(resp.body, nullptr, false); if (j.is_discarded()) return false; std::lock_guard g(state().mu); state().selected_issue_id = id; state().selected_issue_detail = parse_issue(j); return true; } static std::string json_escape(const std::string& s) { json j = s; return j.dump(); } bool patch_issue_status(const std::string& id, const std::string& new_status) { std::string body = "{\"status\":" + json_escape(new_status) + "}"; auto resp = http_patch_json("/api/issues/" + id, body); if (resp.status != 200) { std::lock_guard g(state().mu); state().last_error = "PATCH status " + id + ": " + std::to_string(resp.status); return false; } // Update local cache without full refresh. auto j = json::parse(resp.body, nullptr, false); if (!j.is_discarded()) { Issue updated = parse_issue(j); std::lock_guard g(state().mu); for (auto& i : state().issues) { if (i.id == id) { i = updated; break; } } } return true; } bool patch_issue_fields(const std::string& id, const std::string& json_partial) { auto resp = http_patch_json("/api/issues/" + id, json_partial); if (resp.status != 200) { std::lock_guard g(state().mu); state().last_error = "PATCH " + id + ": " + std::to_string(resp.status); return false; } return true; } void start_sse() { if (g_sse) return; fn_sse::Config cfg; cfg.url = state().backend_url + "/api/sse"; g_sse = std::make_unique(); g_sse->start( cfg, [](const fn_sse::Event& /*ev*/) { std::lock_guard g(state().mu); state().last_refresh_ns = 0; }, nullptr); } void stop_sse() { if (g_sse) { g_sse->stop(); g_sse.reset(); } } static bool contains(const std::set& s, const std::string& v) { return s.find(v) != s.end(); } bool passes_filters(const Issue& iss) { const auto& f = state().filters; if (!f.include_completed && iss.completed) return false; if (!f.priorities.empty() && !contains(f.priorities, iss.priority)) return false; if (!f.scopes.empty() && !contains(f.scopes, iss.scope)) return false; if (!f.domains.empty()) { bool match = false; for (const auto& d : iss.domain) { if (contains(f.domains, d)) { match = true; break; } } if (!match) return false; } if (!f.tags.empty()) { bool match = false; for (const auto& t : iss.tags) { if (contains(f.tags, t)) { match = true; break; } } if (!match) return false; } return true; } std::vector collect_domains() { std::set uniq; for (const auto& i : state().issues) { for (const auto& d : i.domain) uniq.insert(d); } return {uniq.begin(), uniq.end()}; } std::vector collect_tags() { std::set uniq; for (const auto& i : state().issues) { for (const auto& t : i.tags) uniq.insert(t); } return {uniq.begin(), uniq.end()}; } } // namespace kanban