refactor: reemplazar cpp-httplib por HTTP client minimalista
cpp-httplib requiere std::mutex que no compila con MinGW win32 thread model. Se reemplaza por http_client.cpp: sockets crudos, sin threading, sin SSL, funciona en ambos thread models. Elimina vendor/httplib.h (10K lineas). nlohmann/json se mantiene. main.cpp: doble clic sin argumentos intenta la API en localhost:8484 automaticamente. Si falla muestra pantalla de error con boton Retry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
-7
@@ -18,6 +18,7 @@ add_imgui_app(registry_dashboard
|
||||
main.cpp
|
||||
data.cpp
|
||||
data_http.cpp
|
||||
http_client.cpp
|
||||
views.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
|
||||
@@ -37,13 +38,7 @@ target_include_directories(registry_dashboard PRIVATE
|
||||
|
||||
target_link_libraries(registry_dashboard PRIVATE SQLite::SQLite3)
|
||||
|
||||
# cpp-httplib needs threads on Linux
|
||||
if(UNIX)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(registry_dashboard PRIVATE Threads::Threads)
|
||||
endif()
|
||||
|
||||
# On Windows cross-compile, link ws2_32 for sockets
|
||||
# Sockets: ws2_32 on Windows, nothing extra on Linux
|
||||
if(WIN32)
|
||||
target_link_libraries(registry_dashboard PRIVATE ws2_32)
|
||||
endif()
|
||||
|
||||
+68
-94
@@ -1,45 +1,54 @@
|
||||
#include "data_http.h"
|
||||
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT 0
|
||||
#include "vendor/httplib.h"
|
||||
#include "http_client.h"
|
||||
#include "vendor/nlohmann/json.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// Parse host and port from URL like "http://127.0.0.1:8484"
|
||||
static bool parse_url(const std::string& url, std::string& host, int& port) {
|
||||
auto pos = url.find("://");
|
||||
std::string rest = (pos != std::string::npos) ? url.substr(pos + 3) : url;
|
||||
auto colon = rest.find(':');
|
||||
if (colon == std::string::npos) {
|
||||
host = rest;
|
||||
port = 80;
|
||||
} else {
|
||||
host = rest.substr(0, colon);
|
||||
port = std::atoi(rest.substr(colon + 1).c_str());
|
||||
}
|
||||
return !host.empty() && port > 0;
|
||||
}
|
||||
|
||||
// POST a SQL query to the API and return parsed JSON, or null on failure.
|
||||
static json api_query(httplib::Client& cli, const char* sql) {
|
||||
static json api_query(HttpClient& cli, const char* sql) {
|
||||
json body;
|
||||
body["sql"] = sql;
|
||||
auto res = cli.Post("/api/databases/registry/query",
|
||||
body.dump(), "application/json");
|
||||
if (!res || res->status != 200) {
|
||||
if (res) fprintf(stderr, "[http] query error %d: %s\n", res->status, res->body.c_str());
|
||||
auto res = cli.post("/api/databases/registry/query", body.dump(), "application/json");
|
||||
if (!res.ok()) {
|
||||
if (res.status > 0) fprintf(stderr, "[http] query error %d: %s\n", res.status, res.body.c_str());
|
||||
else fprintf(stderr, "[http] connection failed for query: %s\n", sql);
|
||||
return nullptr;
|
||||
}
|
||||
return json::parse(res->body, nullptr, false);
|
||||
return json::parse(res.body, nullptr, false);
|
||||
}
|
||||
|
||||
// Extract first int from a single-row, single-column result.
|
||||
static int extract_int(const json& j) {
|
||||
if (j.is_null() || !j.contains("rows") || j["rows"].empty())
|
||||
return 0;
|
||||
if (j.is_null() || !j.contains("rows") || j["rows"].empty()) return 0;
|
||||
auto& val = j["rows"][0][0];
|
||||
if (val.is_number()) return val.get<int>();
|
||||
if (val.is_string()) return std::atoi(val.get<std::string>().c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Extract string from a row at given column index.
|
||||
static std::string extract_str(const json& row, size_t idx) {
|
||||
if (idx >= row.size() || row[idx].is_null()) return "";
|
||||
if (row[idx].is_string()) return row[idx].get<std::string>();
|
||||
return row[idx].dump();
|
||||
}
|
||||
|
||||
// Extract int from a row at given column index.
|
||||
static int extract_row_int(const json& row, size_t idx) {
|
||||
if (idx >= row.size() || row[idx].is_null()) return 0;
|
||||
if (row[idx].is_number()) return row[idx].get<int>();
|
||||
@@ -48,13 +57,18 @@ static int extract_row_int(const json& row, size_t idx) {
|
||||
}
|
||||
|
||||
bool load_registry_data_http(const std::string& api_url, RegistryData& out) {
|
||||
httplib::Client cli(api_url);
|
||||
cli.set_connection_timeout(2);
|
||||
cli.set_read_timeout(5);
|
||||
std::string host;
|
||||
int port;
|
||||
if (!parse_url(api_url, host, port)) {
|
||||
fprintf(stderr, "[http] invalid URL: %s\n", api_url.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Health check first
|
||||
auto health = cli.Get("/health");
|
||||
if (!health || health->status != 200) {
|
||||
HttpClient cli(host, port);
|
||||
|
||||
// Health check
|
||||
auto health = cli.get("/health");
|
||||
if (!health.ok()) {
|
||||
fprintf(stderr, "[http] sqlite_api not reachable at %s\n", api_url.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -62,72 +76,47 @@ bool load_registry_data_http(const std::string& api_url, RegistryData& out) {
|
||||
fprintf(stdout, "[http] Connected to sqlite_api at %s\n", api_url.c_str());
|
||||
|
||||
// --- Counts ---
|
||||
auto j = api_query(cli, "SELECT COUNT(*) FROM functions");
|
||||
out.stats.total_functions = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM types");
|
||||
out.stats.total_types = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM apps");
|
||||
out.stats.total_apps = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM analysis");
|
||||
out.stats.total_analysis = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM unit_tests");
|
||||
out.stats.total_unit_tests = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM proposals");
|
||||
out.stats.total_proposals = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM functions WHERE tested = 1");
|
||||
out.stats.tested_functions = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'pure'");
|
||||
out.stats.pure_functions = extract_int(j);
|
||||
|
||||
j = api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'impure'");
|
||||
out.stats.impure_functions = extract_int(j);
|
||||
out.stats.total_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions"));
|
||||
out.stats.total_types = extract_int(api_query(cli, "SELECT COUNT(*) FROM types"));
|
||||
out.stats.total_apps = extract_int(api_query(cli, "SELECT COUNT(*) FROM apps"));
|
||||
out.stats.total_analysis = extract_int(api_query(cli, "SELECT COUNT(*) FROM analysis"));
|
||||
out.stats.total_unit_tests = extract_int(api_query(cli, "SELECT COUNT(*) FROM unit_tests"));
|
||||
out.stats.total_proposals = extract_int(api_query(cli, "SELECT COUNT(*) FROM proposals"));
|
||||
out.stats.tested_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions WHERE tested = 1"));
|
||||
out.stats.pure_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'pure'"));
|
||||
out.stats.impure_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'impure'"));
|
||||
|
||||
// --- By language ---
|
||||
out.by_lang.clear();
|
||||
j = api_query(cli, "SELECT lang, COUNT(*) as cnt FROM functions GROUP BY lang ORDER BY cnt DESC");
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
auto j = api_query(cli, "SELECT lang, COUNT(*) as cnt FROM functions GROUP BY lang ORDER BY cnt DESC");
|
||||
if (!j.is_null() && j.contains("rows"))
|
||||
for (auto& row : j["rows"])
|
||||
out.by_lang.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
||||
}
|
||||
}
|
||||
|
||||
// --- By domain ---
|
||||
out.by_domain.clear();
|
||||
j = api_query(cli, "SELECT domain, COUNT(*) as cnt FROM functions GROUP BY domain ORDER BY cnt DESC");
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
if (!j.is_null() && j.contains("rows"))
|
||||
for (auto& row : j["rows"])
|
||||
out.by_domain.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
||||
}
|
||||
}
|
||||
|
||||
// --- By kind ---
|
||||
out.by_kind.clear();
|
||||
j = api_query(cli, "SELECT kind, COUNT(*) as cnt FROM functions GROUP BY kind ORDER BY cnt DESC");
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
if (!j.is_null() && j.contains("rows"))
|
||||
for (auto& row : j["rows"])
|
||||
out.by_kind.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
||||
}
|
||||
}
|
||||
|
||||
// --- By date (last 30 days) ---
|
||||
out.by_date.clear();
|
||||
j = api_query(cli,
|
||||
"SELECT date(created_at) as d, COUNT(*) as cnt FROM functions "
|
||||
"WHERE created_at >= date('now', '-30 days') GROUP BY d ORDER BY d");
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
if (!j.is_null() && j.contains("rows"))
|
||||
for (auto& row : j["rows"])
|
||||
out.by_date.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Recent functions (last 20) ---
|
||||
// --- Recent functions ---
|
||||
out.recent_funcs.clear();
|
||||
j = api_query(cli,
|
||||
"SELECT id, name, lang, domain, kind, purity, description, created_at, tested "
|
||||
@@ -135,14 +124,10 @@ bool load_registry_data_http(const std::string& api_url, RegistryData& out) {
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
FunctionRow r;
|
||||
r.id = extract_str(row, 0);
|
||||
r.name = extract_str(row, 1);
|
||||
r.lang = extract_str(row, 2);
|
||||
r.domain = extract_str(row, 3);
|
||||
r.kind = extract_str(row, 4);
|
||||
r.purity = extract_str(row, 5);
|
||||
r.description = extract_str(row, 6);
|
||||
r.created_at = extract_str(row, 7);
|
||||
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
||||
r.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
||||
r.kind = extract_str(row, 4); r.purity = extract_str(row, 5);
|
||||
r.description = extract_str(row, 6); r.created_at = extract_str(row, 7);
|
||||
r.tested = extract_row_int(row, 8) != 0;
|
||||
out.recent_funcs.push_back(std::move(r));
|
||||
}
|
||||
@@ -150,49 +135,38 @@ bool load_registry_data_http(const std::string& api_url, RegistryData& out) {
|
||||
|
||||
// --- Apps ---
|
||||
out.apps.clear();
|
||||
j = api_query(cli,
|
||||
"SELECT id, name, lang, domain, description, framework FROM apps ORDER BY name");
|
||||
j = api_query(cli, "SELECT id, name, lang, domain, description, framework FROM apps ORDER BY name");
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
AppRow r;
|
||||
r.id = extract_str(row, 0);
|
||||
r.name = extract_str(row, 1);
|
||||
r.lang = extract_str(row, 2);
|
||||
r.domain = extract_str(row, 3);
|
||||
r.description = extract_str(row, 4);
|
||||
r.framework = extract_str(row, 5);
|
||||
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
||||
r.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
||||
r.description = extract_str(row, 4); r.framework = extract_str(row, 5);
|
||||
out.apps.push_back(std::move(r));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Analysis ---
|
||||
out.analyses.clear();
|
||||
j = api_query(cli,
|
||||
"SELECT id, name, domain, description FROM analysis ORDER BY name");
|
||||
j = api_query(cli, "SELECT id, name, domain, description FROM analysis ORDER BY name");
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
AnalysisRow r;
|
||||
r.id = extract_str(row, 0);
|
||||
r.name = extract_str(row, 1);
|
||||
r.domain = extract_str(row, 2);
|
||||
r.description = extract_str(row, 3);
|
||||
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
||||
r.domain = extract_str(row, 2); r.description = extract_str(row, 3);
|
||||
out.analyses.push_back(std::move(r));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Types ---
|
||||
out.types.clear();
|
||||
j = api_query(cli,
|
||||
"SELECT id, name, lang, domain, algebraic, description FROM types ORDER BY name");
|
||||
j = api_query(cli, "SELECT id, name, lang, domain, algebraic, description FROM types ORDER BY name");
|
||||
if (!j.is_null() && j.contains("rows")) {
|
||||
for (auto& row : j["rows"]) {
|
||||
TypeRow r;
|
||||
r.id = extract_str(row, 0);
|
||||
r.name = extract_str(row, 1);
|
||||
r.lang = extract_str(row, 2);
|
||||
r.domain = extract_str(row, 3);
|
||||
r.algebraic = extract_str(row, 4);
|
||||
r.description = extract_str(row, 5);
|
||||
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
||||
r.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
||||
r.algebraic = extract_str(row, 4); r.description = extract_str(row, 5);
|
||||
out.types.push_back(std::move(r));
|
||||
}
|
||||
}
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
#include "http_client.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
|
||||
static bool wsa_init() {
|
||||
static bool done = false;
|
||||
if (!done) {
|
||||
WSADATA wsa;
|
||||
done = (WSAStartup(MAKEWORD(2, 2), &wsa) == 0);
|
||||
}
|
||||
return done;
|
||||
}
|
||||
typedef SOCKET sock_t;
|
||||
#define SOCK_INVALID INVALID_SOCKET
|
||||
#define SOCK_CLOSE closesocket
|
||||
#define SOCK_ERR WSAGetLastError()
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <netdb.h>
|
||||
#include <errno.h>
|
||||
typedef int sock_t;
|
||||
#define SOCK_INVALID (-1)
|
||||
#define SOCK_CLOSE close
|
||||
#define SOCK_ERR errno
|
||||
#endif
|
||||
|
||||
HttpClient::HttpClient(const std::string& host, int port)
|
||||
: host_(host), port_(port) {}
|
||||
|
||||
HttpResponse HttpClient::get(const std::string& path) {
|
||||
return request("GET", path, "", "");
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post(const std::string& path, const std::string& body,
|
||||
const std::string& content_type) {
|
||||
return request("POST", path, body, content_type);
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::request(const std::string& method, const std::string& path,
|
||||
const std::string& body, const std::string& content_type) {
|
||||
HttpResponse resp;
|
||||
|
||||
#ifdef _WIN32
|
||||
if (!wsa_init()) {
|
||||
fprintf(stderr, "[http] WSAStartup failed\n");
|
||||
return resp;
|
||||
}
|
||||
#endif
|
||||
|
||||
sock_t sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == SOCK_INVALID) {
|
||||
fprintf(stderr, "[http] socket() failed: %d\n", SOCK_ERR);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
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));
|
||||
|
||||
// Connect
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(port_));
|
||||
addr.sin_addr.s_addr = inet_addr(host_.c_str());
|
||||
|
||||
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
|
||||
SOCK_CLOSE(sock);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Build request
|
||||
std::ostringstream req;
|
||||
req << method << " " << path << " HTTP/1.1\r\n";
|
||||
req << "Host: " << host_ << ":" << port_ << "\r\n";
|
||||
req << "Connection: close\r\n";
|
||||
if (!body.empty()) {
|
||||
req << "Content-Type: " << content_type << "\r\n";
|
||||
req << "Content-Length: " << body.size() << "\r\n";
|
||||
}
|
||||
req << "\r\n";
|
||||
if (!body.empty()) req << body;
|
||||
|
||||
std::string raw_req = req.str();
|
||||
send(sock, raw_req.c_str(), static_cast<int>(raw_req.size()), 0);
|
||||
|
||||
// Read response
|
||||
std::vector<char> buf(8192);
|
||||
std::string raw;
|
||||
for (;;) {
|
||||
int n = recv(sock, buf.data(), static_cast<int>(buf.size()), 0);
|
||||
if (n <= 0) break;
|
||||
raw.append(buf.data(), n);
|
||||
}
|
||||
SOCK_CLOSE(sock);
|
||||
|
||||
// Parse status line
|
||||
auto hdr_end = raw.find("\r\n\r\n");
|
||||
if (hdr_end == std::string::npos) return resp;
|
||||
|
||||
// "HTTP/1.1 200 OK\r\n..."
|
||||
auto first_line_end = raw.find("\r\n");
|
||||
if (first_line_end == std::string::npos) return resp;
|
||||
std::string status_line = raw.substr(0, first_line_end);
|
||||
auto sp1 = status_line.find(' ');
|
||||
if (sp1 != std::string::npos) {
|
||||
resp.status = std::atoi(status_line.c_str() + sp1 + 1);
|
||||
}
|
||||
|
||||
resp.body = raw.substr(hdr_end + 4);
|
||||
|
||||
// Handle chunked transfer encoding
|
||||
std::string headers_str = raw.substr(0, hdr_end);
|
||||
if (headers_str.find("chunked") != std::string::npos) {
|
||||
// Decode chunked body
|
||||
std::string decoded;
|
||||
const char* p = resp.body.c_str();
|
||||
const char* end = p + resp.body.size();
|
||||
while (p < end) {
|
||||
// Read chunk size (hex)
|
||||
char* chunk_end = nullptr;
|
||||
long chunk_size = strtol(p, &chunk_end, 16);
|
||||
if (chunk_size <= 0) break;
|
||||
// Skip \r\n after size
|
||||
p = chunk_end;
|
||||
if (p < end && *p == '\r') p++;
|
||||
if (p < end && *p == '\n') p++;
|
||||
// Read chunk data
|
||||
if (p + chunk_size <= end) {
|
||||
decoded.append(p, chunk_size);
|
||||
}
|
||||
p += chunk_size;
|
||||
// Skip trailing \r\n
|
||||
if (p < end && *p == '\r') p++;
|
||||
if (p < end && *p == '\n') p++;
|
||||
}
|
||||
resp.body = decoded;
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
// Minimal HTTP client — no threading, no SSL, just plain TCP to localhost.
|
||||
// Works with both win32 and posix MinGW thread models.
|
||||
|
||||
#include <string>
|
||||
|
||||
struct HttpResponse {
|
||||
int status = 0;
|
||||
std::string body;
|
||||
bool ok() const { return status >= 200 && status < 300; }
|
||||
};
|
||||
|
||||
// Simple blocking HTTP GET/POST over TCP sockets.
|
||||
// host: "127.0.0.1", port: 8484
|
||||
class HttpClient {
|
||||
public:
|
||||
HttpClient(const std::string& host, int port);
|
||||
|
||||
HttpResponse get(const std::string& path);
|
||||
HttpResponse post(const std::string& path, const std::string& body,
|
||||
const std::string& content_type = "application/json");
|
||||
|
||||
private:
|
||||
std::string host_;
|
||||
int port_;
|
||||
int timeout_sec_ = 5;
|
||||
|
||||
HttpResponse request(const std::string& method, const std::string& path,
|
||||
const std::string& body, const std::string& content_type);
|
||||
};
|
||||
@@ -103,15 +103,8 @@ int main(int argc, char** argv) {
|
||||
fprintf(stderr, "Not found: %s\n", candidate.c_str());
|
||||
}
|
||||
|
||||
if (g_api_url.empty() && g_db_path.empty() && db_candidates.empty()) {
|
||||
fprintf(stderr, "Usage: registry_dashboard [--api URL] [db_path ...]\n");
|
||||
fprintf(stderr, " --api URL Connect to sqlite_api (default: http://127.0.0.1:8484)\n");
|
||||
fprintf(stderr, " db_path Direct SQLite path(s) as fallback\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (g_db_path.empty() && !db_candidates.empty()) {
|
||||
g_db_path = db_candidates.back();
|
||||
if (!db_candidates.empty()) {
|
||||
if (g_db_path.empty()) g_db_path = db_candidates.back();
|
||||
}
|
||||
|
||||
reload_data();
|
||||
|
||||
Vendored
-10255
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user