// main.cpp — kanban_cpp entry point. // // Six panels declared via cfg.panels. fn::run_app paints the menubar / // dockspace / about / layouts automatically. #include "app_base.h" #include "core/panel_menu.h" #include "core/icons_tabler.h" #include "core/logger.h" #include "core/sse_client.h" #include "panels.h" #include #include #include #include #include #include #include static bool g_show_board = true; static bool g_show_calendar = true; static bool g_show_dashboard = true; static bool g_show_runs = true; static bool g_show_worktrees = true; static bool g_show_dod = true; static kanban_cpp::AppState g_state; // SSE client: receives push notifications from the backend stream so the // board updates without polling. Lifetime tied to main() — stop() before // returning so the worker thread joins cleanly. static fn_sse::Client g_sse_client; static void render() { if (g_show_board) kanban_cpp::draw_board (g_state, &g_show_board); if (g_show_calendar) kanban_cpp::draw_calendar (g_state, &g_show_calendar); if (g_show_dashboard) kanban_cpp::draw_dashboard (g_state, &g_show_dashboard); if (g_show_runs) kanban_cpp::draw_agent_runs(g_state, &g_show_runs); if (g_show_worktrees) kanban_cpp::draw_worktrees (g_state, &g_show_worktrees); if (g_show_dod) kanban_cpp::draw_dod (g_state, &g_show_dod); } // Headless self-test: verifies the binary links, panels include compile, // and the data layer accepts a config. Used by app.md e2e_checks. static int run_self_test() { std::printf("kanban_cpp --self-test\n"); kanban_cpp::AppState s; s.cfg.base_url = "http://127.0.0.1:65535"; // unreachable on purpose bool ok = kanban_cpp::health(s.cfg); std::printf(" health(unreachable) = %s (expected: false)\n", ok ? "true" : "false"); if (ok) return 1; std::printf("OK\n"); return 0; } int main(int argc, char** argv) { for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--self-test") == 0) return run_self_test(); } static fn_ui::PanelToggle panels[] = { { "Board", nullptr, &g_show_board }, { "Calendar", nullptr, &g_show_calendar }, { "Dashboard", nullptr, &g_show_dashboard }, { "Agent runs", nullptr, &g_show_runs }, { "Worktrees", nullptr, &g_show_worktrees }, { "DoD inspector", nullptr, &g_show_dod }, }; fn::AppConfig cfg; cfg.title = "kanban_cpp — agentes LLM con DoD"; cfg.about = { "kanban_cpp", "0.1.0", "Clon C++ ImGui de kanban_web — agentes LLM con DoD evidence" }; cfg.log = { "kanban_cpp.log", 1 }; cfg.panels = panels; cfg.panel_count = sizeof(panels) / sizeof(panels[0]); // First refresh on startup en thread separado: no bloquea primer frame // si el backend :8403 esta caido (timeout HTTP ~9s). std::thread([](){ kanban_cpp::refresh_data(g_state); }).detach(); // SSE live updates: arranca tras 500ms para no competir con el primer // refresh inicial. Auto-reconecta con backoff si el endpoint no existe // aun o si el backend cae — NUNCA crashea el frame. std::thread([](){ std::this_thread::sleep_for(std::chrono::milliseconds(500)); fn_sse::Config sse_cfg; sse_cfg.url = g_state.cfg.base_url + "/api/boards/issues/stream"; sse_cfg.auto_reconnect = true; g_sse_client.start(sse_cfg, // on_event: cualquier evento dispara un refresh asincrono. [](const fn_sse::Event& /*ev*/) { std::thread([](){ kanban_cpp::refresh_data(g_state); }).detach(); }, // on_status: actualiza el badge UI bajo mutex. [](const std::string& status) { std::lock_guard lock(g_state.mu); g_state.sse_status = status; }); }).detach(); int rc = fn::run_app(cfg, render); // Kill SSE worker antes de salir — orden importa para evitar dangling // thread storage cuando se destruyen los globales. g_sse_client.stop(); return rc; }