a76ec74338
C++ ImGui kanban for steering LLM agents. Six panels (Board, Calendar, Dashboard, Agent runs, Worktrees, DoD inspector) wired to registry functions http_request, kpi_card, sparkline, agent_runs_timeline, dod_evidence_panel. Backend Go on :8403 (independent operations.db from kanban_web).
175 lines
6.3 KiB
C++
175 lines
6.3 KiB
C++
// 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 <cstring>
|
|
#include <cstdio>
|
|
|
|
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<int64_t>(v);
|
|
}
|
|
|
|
// Split JSON array of objects at depth 1. Returns each object as a substring.
|
|
std::vector<std::string> split_objects(const std::string& s) {
|
|
std::vector<std::string> 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) {
|
|
auto r = do_get(cfg.base_url + "/health", cfg.timeout_ms);
|
|
return r.status >= 200 && r.status < 300;
|
|
}
|
|
|
|
std::vector<Card> list_cards(const ClientConfig& cfg, std::string& err) {
|
|
auto r = do_get(cfg.base_url + "/api/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<Card> 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.column_id = find_str_field(obj, "column_id");
|
|
c.priority = find_str_field(obj, "priority");
|
|
c.status = find_str_field(obj, "status");
|
|
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<Column> list_columns(const ClientConfig& cfg, std::string& err) {
|
|
auto r = do_get(cfg.base_url + "/api/columns", 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<Column> out;
|
|
for (const auto& obj : split_objects(r.body)) {
|
|
Column c;
|
|
c.id = find_str_field(obj, "id");
|
|
c.name = find_str_field(obj, "name");
|
|
c.order = static_cast<int>(find_int_field(obj, "order"));
|
|
if (!c.id.empty()) out.push_back(c);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
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<AgentRunSummary> 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<AgentRunSummary> 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
|