feat: initial scaffold kanban_cpp v0.1.0
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).
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user