feat: Settings submenu (Settings.../About...), git column, projects tab

- main.cpp: registrar info About via fn_ui::about_window_set_info
- views.cpp: nueva columna "Git" en la tabla Apps (remote/local/-)
- data.h/cpp + data_http.cpp: AppRow gana repo_url + dir_path
- views.cpp: actions bar (Reindex / + Add / Reload / inbox) y modal Add
- views.cpp: tab Projects con tree + detalle anidado
- data_http.cpp: load_projects_http, load_project_detail_http, http_post_*
- http_client.cpp: SO_RCVTIMEO en Windows como DWORD ms (timeout 5 ms bug)
- CMakeLists: limpieza de srcs duplicados con fn_framework
- app.md: notas operativas y estado actual

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 22:04:58 +02:00
parent 57d8f0198a
commit a466fff71a
10 changed files with 800 additions and 59 deletions
+24 -6
View File
@@ -8,15 +8,19 @@
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <mutex>
#pragma comment(lib, "ws2_32.lib")
// std::call_once para evitar race condition si hay peticiones simultaneas
// desde multiples threads (main + runners).
static std::once_flag g_wsa_once;
static bool g_wsa_ok = false;
static bool wsa_init() {
static bool done = false;
if (!done) {
std::call_once(g_wsa_once, []() {
WSADATA wsa;
done = (WSAStartup(MAKEWORD(2, 2), &wsa) == 0);
}
return done;
g_wsa_ok = (WSAStartup(MAKEWORD(2, 2), &wsa) == 0);
});
return g_wsa_ok;
}
typedef SOCKET sock_t;
#define SOCK_INVALID INVALID_SOCKET
@@ -64,12 +68,19 @@ HttpResponse HttpClient::request(const std::string& method, const std::string& p
return resp;
}
// Set timeout
// Timeout — Windows y POSIX usan formatos distintos para SO_{RCV,SND}TIMEO.
// Windows: DWORD milisegundos. POSIX: struct timeval.
#ifdef _WIN32
DWORD timeout_ms = static_cast<DWORD>(timeout_sec_ * 1000);
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms));
#else
struct timeval tv;
tv.tv_sec = timeout_sec_;
tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
#endif
// Connect
struct sockaddr_in addr;
@@ -79,7 +90,14 @@ HttpResponse HttpClient::request(const std::string& method, const std::string& p
addr.sin_addr.s_addr = inet_addr(host_.c_str());
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
int err = SOCK_ERR;
SOCK_CLOSE(sock);
// Reportamos el errno/WSAError en el body para que el toast sea util.
char buf[128];
std::snprintf(buf, sizeof(buf),
"connect() failed to %s:%d (err=%d)",
host_.c_str(), port_, err);
resp.body = buf;
return resp;
}