Files
kanban_cpp/data.cpp
T
agent 255e8dcf71 feat: initial scaffold of kanban_cpp v2 (issue 0130)
Frontend C++ ImGui (main.cpp + 4 paneles) + backend Go (HTTP + SQLite + fsnotify + SSE).
Reusa parse/scan/watch funcs del registry (issue 0130a).
2026-05-22 22:19:47 +02:00

262 lines
7.7 KiB
C++

#include "data.h"
#include <atomic>
#include <chrono>
#include <memory>
#include <thread>
#include "core/http_request.h"
#include "core/sse_client.h"
#include "core/logger.h"
#include <nlohmann/json.hpp>
namespace kanban {
using json = nlohmann::json;
State& state() {
static State s;
return s;
}
static std::unique_ptr<fn_sse::Client> g_sse;
static std::vector<std::string> j_arr(const json& j, const char* key) {
std::vector<std::string> 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<std::string>());
}
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<std::string>();
}
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<bool>();
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<std::mutex> 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<std::mutex> g(state().mu);
state().last_error = "issues: invalid JSON";
return false;
}
std::vector<Issue> out;
out.reserve(j.size());
for (const auto& it : j) out.push_back(parse_issue(it));
std::lock_guard<std::mutex> g(state().mu);
state().issues = std::move(out);
state().last_refresh_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
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<std::mutex> 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<Flow> out;
out.reserve(j.size());
for (const auto& it : j) out.push_back(parse_flow(it));
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<fn_sse::Client>();
g_sse->start(
cfg,
[](const fn_sse::Event& /*ev*/) {
std::lock_guard<std::mutex> 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<std::string>& 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<std::string> collect_domains() {
std::set<std::string> uniq;
for (const auto& i : state().issues) {
for (const auto& d : i.domain) uniq.insert(d);
}
return {uniq.begin(), uniq.end()};
}
std::vector<std::string> collect_tags() {
std::set<std::string> uniq;
for (const auto& i : state().issues) {
for (const auto& t : i.tags) uniq.insert(t);
}
return {uniq.begin(), uniq.end()};
}
} // namespace kanban