b767b5b85e
App C++ ImGui que abre cualquier operations.db del registry y lo visualiza
como grafo con shapes/iconos/layouts/filtros/labels.
Composicion del registry:
- viz/graph_renderer + graph_force_layout(_gpu) + graph_layouts +
graph_viewport + graph_labels + graph_icons + graph_sources
- core: toolbar, modal_dialog, select, text_input, tree_view, page_header,
fullscreen_window, button, badge, empty_state
Capas:
- data.{h,cpp} — dispatcher GraphLoadFn (operations hoy; json/graphml manana).
- types_registry.{h,cpp} — parser YAML minimal + tabler_codepoint_by_name +
apply_types_yaml + IconAtlas builder.
- views.{h,cpp} — Toolbar, Legend, Inspector, Stats, modal Filters/Open.
- layout_store.{h,cpp} — graph_explorer.db SQLite con tabla layouts(graph_hash,
node_id, x, y, pinned, updated_at). UPSERT por nodo.
- main.cpp — CLI (--input/--types/--layout) + fn::run_app + bucle
force layout (CPU/GPU toggle) + render con 3 columnas (Legend / Viewport /
Inspector+Stats).
examples/types.yaml: 10 entidades OSINT (Person/Email/Domain/Phone/Org/IBAN/
Account/Document/Address/Url) + 5 relaciones (owns/knows/located_in/
transfers_to/member_of) con shapes Tabler reales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
173 lines
5.2 KiB
C++
173 lines
5.2 KiB
C++
#include "layout_store.h"
|
|
|
|
#include "viz/graph_types.h"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <string>
|
|
|
|
namespace ge {
|
|
|
|
namespace {
|
|
sqlite3* g_db = nullptr;
|
|
} // namespace
|
|
|
|
// FNV1a 64 sobre el path. No requiere normalizacion canonica (el caller pasa
|
|
// el path tal cual lo recibio en argv) — basta con que el mismo path produzca
|
|
// el mismo hash entre runs.
|
|
uint64_t compute_graph_hash(const char* path) {
|
|
if (!path) return 0;
|
|
uint64_t h = 1469598103934665603ull; // FNV offset
|
|
while (*path) {
|
|
h ^= (uint64_t)(unsigned char)(*path++);
|
|
h *= 1099511628211ull; // FNV prime
|
|
}
|
|
return h;
|
|
}
|
|
|
|
bool layout_store_open(const char* db_path) {
|
|
if (g_db) return true;
|
|
if (!db_path) return false;
|
|
int rc = sqlite3_open(db_path, &g_db);
|
|
if (rc != SQLITE_OK) {
|
|
std::fprintf(stderr, "[layout_store] sqlite3_open failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
if (g_db) sqlite3_close(g_db);
|
|
g_db = nullptr;
|
|
return false;
|
|
}
|
|
const char* schema =
|
|
"CREATE TABLE IF NOT EXISTS layouts ("
|
|
" graph_hash TEXT NOT NULL,"
|
|
" node_id TEXT NOT NULL,"
|
|
" x REAL NOT NULL,"
|
|
" y REAL NOT NULL,"
|
|
" pinned INTEGER NOT NULL DEFAULT 0,"
|
|
" updated_at INTEGER NOT NULL,"
|
|
" PRIMARY KEY(graph_hash, node_id)"
|
|
");";
|
|
char* err = nullptr;
|
|
rc = sqlite3_exec(g_db, schema, nullptr, nullptr, &err);
|
|
if (rc != SQLITE_OK) {
|
|
std::fprintf(stderr, "[layout_store] schema error: %s\n",
|
|
err ? err : "(null)");
|
|
sqlite3_free(err);
|
|
sqlite3_close(g_db);
|
|
g_db = nullptr;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void layout_store_close() {
|
|
if (g_db) {
|
|
sqlite3_close(g_db);
|
|
g_db = nullptr;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
std::string hash_to_hex(uint64_t h) {
|
|
char buf[17];
|
|
std::snprintf(buf, sizeof(buf), "%016llx",
|
|
(unsigned long long)h);
|
|
return std::string(buf);
|
|
}
|
|
|
|
std::string node_user_to_hex(uint64_t u) {
|
|
char buf[17];
|
|
std::snprintf(buf, sizeof(buf), "%016llx",
|
|
(unsigned long long)u);
|
|
return std::string(buf);
|
|
}
|
|
} // namespace
|
|
|
|
int layout_store_save(uint64_t graph_hash, const GraphData& graph) {
|
|
if (!g_db || graph.node_count <= 0) return 0;
|
|
|
|
sqlite3_exec(g_db, "BEGIN", nullptr, nullptr, nullptr);
|
|
|
|
const char* sql =
|
|
"INSERT INTO layouts(graph_hash, node_id, x, y, pinned, updated_at) "
|
|
"VALUES(?, ?, ?, ?, ?, ?) "
|
|
"ON CONFLICT(graph_hash, node_id) DO UPDATE SET "
|
|
" x=excluded.x, y=excluded.y, pinned=excluded.pinned, "
|
|
" updated_at=excluded.updated_at;";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
|
|
if (rc != SQLITE_OK) {
|
|
std::fprintf(stderr, "[layout_store] prepare save failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
sqlite3_exec(g_db, "ROLLBACK", nullptr, nullptr, nullptr);
|
|
return -1;
|
|
}
|
|
|
|
std::string ghex = hash_to_hex(graph_hash);
|
|
int written = 0;
|
|
int64_t now = (int64_t)std::time(nullptr);
|
|
|
|
for (int i = 0; i < graph.node_count; ++i) {
|
|
const GraphNode& n = graph.nodes[i];
|
|
if (n.user_data == 0) continue; // sin id estable: no persistible
|
|
std::string nhex = node_user_to_hex(n.user_data);
|
|
|
|
sqlite3_bind_text(stmt, 1, ghex.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, nhex.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_double(stmt, 3, (double)n.x);
|
|
sqlite3_bind_double(stmt, 4, (double)n.y);
|
|
sqlite3_bind_int(stmt, 5, (n.flags & NF_PINNED) ? 1 : 0);
|
|
sqlite3_bind_int64(stmt, 6, now);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_DONE) ++written;
|
|
sqlite3_reset(stmt);
|
|
sqlite3_clear_bindings(stmt);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_exec(g_db, "COMMIT", nullptr, nullptr, nullptr);
|
|
return written;
|
|
}
|
|
|
|
int layout_store_load(uint64_t graph_hash, GraphData& graph) {
|
|
if (!g_db || graph.node_count <= 0) return 0;
|
|
|
|
const char* sql =
|
|
"SELECT node_id, x, y, pinned FROM layouts WHERE graph_hash = ?;";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
|
|
if (rc != SQLITE_OK) {
|
|
std::fprintf(stderr, "[layout_store] prepare load failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
|
|
std::string ghex = hash_to_hex(graph_hash);
|
|
sqlite3_bind_text(stmt, 1, ghex.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
int touched = 0;
|
|
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
|
const unsigned char* nid = sqlite3_column_text(stmt, 0);
|
|
if (!nid) continue;
|
|
uint64_t u = std::strtoull((const char*)nid, nullptr, 16);
|
|
int idx = graph.find_node_by_user_data(u);
|
|
if (idx < 0) continue;
|
|
GraphNode& n = graph.nodes[idx];
|
|
n.x = (float)sqlite3_column_double(stmt, 1);
|
|
n.y = (float)sqlite3_column_double(stmt, 2);
|
|
if (sqlite3_column_int(stmt, 3))
|
|
n.flags |= NF_PINNED;
|
|
else
|
|
n.flags &= ~NF_PINNED;
|
|
++touched;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return touched;
|
|
}
|
|
|
|
} // namespace ge
|