feat: catch-up de decisiones previas (Webpage→Url, anti-bot, UI 2-col, tests cross-platform)
Bloque de cambios revisados y validados con el usuario en sesiones previas que no habian aterrizado en commits propios. Lista por tema: * enrichers: web_search ahora usa lite.duckduckgo.com como endpoint primario (mas tolerante con bot detection desde IP residencial), con fallback al endpoint html. Detecta pagina captcha y emite error claro si ambos fallan. Anyade _DDGLiteParser para el formato lite + auto-pick de parser por contenido. * enrichers: tipo Webpage unificado en Url (campos de cuerpo cacheado viven en metadata del Url). Manifests actualizados (applies_to: [Url]). fetch_webpage ya no convierte Url->Webpage. * enrichers/manifest: campo `params` parseado a EnricherSpec.params (name, type, default_value, description). UI puede renderizar dialog de configuracion. * jobs: fix de path conversion para Python embebido nativo Windows (no convertir a /mnt/c/... cuando el subproceso es Windows-native; solo cuando es bash o python via WSL). * main.cpp: ventana ImGui (no modal) "Run enricher" con layout 2-col (label izq, input der). Inserta job con JSON tipado. Layout clustering apretado: hijos del mismo anchor en un solo anillo alrededor del padre, sin desperdigar por anillos crecientes. * views: inspector con layout 2-col via BeginTable (Identity, Schema fields, Extras). Description full-width debajo de su label. * tests: portable conftest (auto-detecta REGISTRY_ROOT, PYTHON_BIN, ENRICHERS_DIR para WSL y Windows portable). _runner.py trampoline inyecta stub via sys.path porque embedded Python ignora PYTHONPATH. Tests bash-only (vendor_script, freeze, dispatcher bash, resolver Linux-binary) skipean en Windows. Tests existentes adaptados a Webpage->Url. Resultado actual: 32 passed WSL, 21 passed + 11 skipped Windows.
This commit is contained in:
@@ -874,173 +874,234 @@ void views_inspector(AppState& app) {
|
||||
bool any_change = false;
|
||||
|
||||
// ---- Identidad ----
|
||||
// Layout label-izquierda / input-derecha via 2-col table. El label
|
||||
// alineado al frame del input y el input estirado al ancho restante.
|
||||
ImGui::TextUnformatted("Identity");
|
||||
ImGui::Separator();
|
||||
if (ImGui::InputText("name", app.insp_name_buf, sizeof(app.insp_name_buf)))
|
||||
any_change = true;
|
||||
|
||||
// type combo
|
||||
{
|
||||
int cur = -1;
|
||||
for (size_t i = 0; i < app.insp_type_options.size(); ++i) {
|
||||
if (app.insp_type_options[i] == app.insp_type_buf) { cur = (int)i; break; }
|
||||
}
|
||||
// Si el tipo no esta en el cache (raro), mostrar como tal y permitir
|
||||
// introducirlo via input. Combo simple aqui.
|
||||
if (ImGui::BeginCombo("type", app.insp_type_buf)) {
|
||||
if (ImGui::BeginTable("##insp_id", 2,
|
||||
ImGuiTableFlags_SizingStretchProp |
|
||||
ImGuiTableFlags_NoBordersInBody)) {
|
||||
ImGui::TableSetupColumn("k", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
ImGui::TableSetupColumn("v", ImGuiTableColumnFlags_WidthStretch);
|
||||
|
||||
// name
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("name");
|
||||
ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::InputText("##name", app.insp_name_buf,
|
||||
sizeof(app.insp_name_buf)))
|
||||
any_change = true;
|
||||
|
||||
// type combo
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("type");
|
||||
ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
{
|
||||
int cur = -1;
|
||||
for (size_t i = 0; i < app.insp_type_options.size(); ++i) {
|
||||
bool is_sel = (int)i == cur;
|
||||
if (ImGui::Selectable(app.insp_type_options[i].c_str(), is_sel)) {
|
||||
copy_to_buf(app.insp_type_buf, sizeof(app.insp_type_buf),
|
||||
app.insp_type_options[i]);
|
||||
any_change = true;
|
||||
if (app.insp_type_options[i] == app.insp_type_buf) {
|
||||
cur = (int)i; break;
|
||||
}
|
||||
if (is_sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
if (ImGui::BeginCombo("##type", app.insp_type_buf)) {
|
||||
for (size_t i = 0; i < app.insp_type_options.size(); ++i) {
|
||||
bool is_sel = (int)i == cur;
|
||||
if (ImGui::Selectable(app.insp_type_options[i].c_str(), is_sel)) {
|
||||
copy_to_buf(app.insp_type_buf, sizeof(app.insp_type_buf),
|
||||
app.insp_type_options[i]);
|
||||
any_change = true;
|
||||
}
|
||||
if (is_sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
// status combo
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("status");
|
||||
ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::Combo("##status", &app.insp_status_idx,
|
||||
k_status_options, k_status_count))
|
||||
any_change = true;
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
// status combo
|
||||
if (ImGui::Combo("status", &app.insp_status_idx,
|
||||
k_status_options, k_status_count))
|
||||
any_change = true;
|
||||
// description multiline
|
||||
|
||||
// description — multiline va debajo de su label, ocupando todo el
|
||||
// ancho. Con 60 px de alto entra ~3 lineas; el usuario hace scroll
|
||||
// dentro del input para textos mas largos.
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("description");
|
||||
if (app.insp_desc_buf.empty()) ensure_desc_buf(app.insp_desc_buf, 4096);
|
||||
if (ImGui::InputTextMultiline("description",
|
||||
if (ImGui::InputTextMultiline("##desc",
|
||||
app.insp_desc_buf.data(),
|
||||
app.insp_desc_buf.size(),
|
||||
ImVec2(-FLT_MIN, 60.0f)))
|
||||
any_change = true;
|
||||
|
||||
// ---- Schema fields + Extras ----
|
||||
// Misma idea que Identity: 2-col table con label izquierda, input
|
||||
// derecha. Para extras añadimos un boton trash inline; para URLs un
|
||||
// boton Open. Ambos son SmallButton tras un input mas estrecho.
|
||||
if (!app.insp_field_keys.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("Fields");
|
||||
ImGui::Separator();
|
||||
const EntitySpec* spec = find_entity_spec(app.parsed_types,
|
||||
app.insp_type_buf);
|
||||
for (size_t i = 0; i < app.insp_field_keys.size(); ++i) {
|
||||
const std::string& key = app.insp_field_keys[i];
|
||||
std::string& val = app.insp_field_values[i];
|
||||
bool is_extra = app.insp_is_extra[i] != 0;
|
||||
ImGui::PushID((int)i);
|
||||
|
||||
// Encuentra la FieldSpec si es del schema.
|
||||
const FieldSpec* fs = nullptr;
|
||||
if (!is_extra && spec) {
|
||||
for (const auto& f : spec->fields) {
|
||||
if (f.name == key) { fs = &f; break; }
|
||||
}
|
||||
}
|
||||
if (ImGui::BeginTable("##insp_fields", 2,
|
||||
ImGuiTableFlags_SizingStretchProp |
|
||||
ImGuiTableFlags_NoBordersInBody)) {
|
||||
ImGui::TableSetupColumn("k", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
ImGui::TableSetupColumn("v", ImGuiTableColumnFlags_WidthStretch);
|
||||
|
||||
FieldKind kind = fs ? fs->kind : FK_STRING;
|
||||
std::string label = key;
|
||||
if (fs && fs->required) label += " *";
|
||||
if (is_extra) label = "[extra] " + key;
|
||||
for (size_t i = 0; i < app.insp_field_keys.size(); ++i) {
|
||||
const std::string& key = app.insp_field_keys[i];
|
||||
std::string& val = app.insp_field_values[i];
|
||||
bool is_extra = app.insp_is_extra[i] != 0;
|
||||
ImGui::PushID((int)i);
|
||||
|
||||
char buf[1024];
|
||||
size_t k = std::min(sizeof(buf) - 1, val.size());
|
||||
std::memcpy(buf, val.data(), k);
|
||||
buf[k] = 0;
|
||||
|
||||
bool changed = false;
|
||||
switch (kind) {
|
||||
case FK_BOOL: {
|
||||
bool b = (val == "true" || val == "1");
|
||||
if (ImGui::Checkbox(label.c_str(), &b)) {
|
||||
val = b ? "true" : "false";
|
||||
changed = true;
|
||||
// Encuentra la FieldSpec si es del schema.
|
||||
const FieldSpec* fs = nullptr;
|
||||
if (!is_extra && spec) {
|
||||
for (const auto& f : spec->fields) {
|
||||
if (f.name == key) { fs = &f; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FK_INT: {
|
||||
int n = std::atoi(val.c_str());
|
||||
if (ImGui::InputInt(label.c_str(), &n)) {
|
||||
char nb[32]; std::snprintf(nb, sizeof(nb), "%d", n);
|
||||
val = nb;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
|
||||
FieldKind kind = fs ? fs->kind : FK_STRING;
|
||||
|
||||
// Label izquierdo. Marca `*` si es required, prefijo
|
||||
// [extra] si es campo libre añadido por el usuario.
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
if (is_extra) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImVec4(0.65f, 0.65f, 0.50f, 1.0f));
|
||||
ImGui::Text("%s", key.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
} else if (fs && fs->required) {
|
||||
ImGui::Text("%s *", key.c_str());
|
||||
} else {
|
||||
ImGui::TextUnformatted(key.c_str());
|
||||
}
|
||||
case FK_FLOAT: {
|
||||
double d = std::atof(val.c_str());
|
||||
if (ImGui::InputDouble(label.c_str(), &d, 0.0, 0.0, "%.6g")) {
|
||||
char nb[64]; std::snprintf(nb, sizeof(nb), "%.10g", d);
|
||||
val = nb;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FK_ENUM: {
|
||||
if (fs && !fs->enum_values.empty()) {
|
||||
int cur = -1;
|
||||
for (size_t e = 0; e < fs->enum_values.size(); ++e) {
|
||||
if (fs->enum_values[e] == val) { cur = (int)e; break; }
|
||||
|
||||
// Input derecha. Reserva espacio para el trailing button
|
||||
// cuando aplique (URL Open, extras trash).
|
||||
ImGui::TableNextColumn();
|
||||
bool needs_trail_btn = is_extra ||
|
||||
(kind == FK_URL && !val.empty() &&
|
||||
(val.rfind("http://", 0) == 0 ||
|
||||
val.rfind("https://", 0) == 0));
|
||||
ImGui::SetNextItemWidth(needs_trail_btn ? -32.0f : -FLT_MIN);
|
||||
|
||||
char buf[1024];
|
||||
size_t k = std::min(sizeof(buf) - 1, val.size());
|
||||
std::memcpy(buf, val.data(), k);
|
||||
buf[k] = 0;
|
||||
|
||||
bool changed = false;
|
||||
switch (kind) {
|
||||
case FK_BOOL: {
|
||||
bool b = (val == "true" || val == "1");
|
||||
if (ImGui::Checkbox("##v", &b)) {
|
||||
val = b ? "true" : "false";
|
||||
changed = true;
|
||||
}
|
||||
if (ImGui::BeginCombo(label.c_str(), val.c_str())) {
|
||||
break;
|
||||
}
|
||||
case FK_INT: {
|
||||
int n = std::atoi(val.c_str());
|
||||
if (ImGui::InputInt("##v", &n)) {
|
||||
char nb[32]; std::snprintf(nb, sizeof(nb), "%d", n);
|
||||
val = nb;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FK_FLOAT: {
|
||||
double d = std::atof(val.c_str());
|
||||
if (ImGui::InputDouble("##v", &d, 0.0, 0.0, "%.6g")) {
|
||||
char nb[64]; std::snprintf(nb, sizeof(nb), "%.10g", d);
|
||||
val = nb;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FK_ENUM: {
|
||||
if (fs && !fs->enum_values.empty()) {
|
||||
int cur = -1;
|
||||
for (size_t e = 0; e < fs->enum_values.size(); ++e) {
|
||||
bool is_sel = (int)e == cur;
|
||||
if (ImGui::Selectable(fs->enum_values[e].c_str(), is_sel)) {
|
||||
val = fs->enum_values[e];
|
||||
changed = true;
|
||||
}
|
||||
if (is_sel) ImGui::SetItemDefaultFocus();
|
||||
if (fs->enum_values[e] == val) { cur = (int)e; break; }
|
||||
}
|
||||
if (ImGui::BeginCombo("##v", val.c_str())) {
|
||||
for (size_t e = 0; e < fs->enum_values.size(); ++e) {
|
||||
bool is_sel = (int)e == cur;
|
||||
if (ImGui::Selectable(fs->enum_values[e].c_str(), is_sel)) {
|
||||
val = fs->enum_values[e];
|
||||
changed = true;
|
||||
}
|
||||
if (is_sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
if (ImGui::InputText("##v", buf, sizeof(buf))) {
|
||||
val = buf;
|
||||
changed = true;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
// Sin valores: tratar como string
|
||||
if (ImGui::InputText(label.c_str(), buf, sizeof(buf))) {
|
||||
break;
|
||||
}
|
||||
case FK_URL:
|
||||
if (ImGui::InputText("##v", buf, sizeof(buf))) {
|
||||
val = buf;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FK_URL:
|
||||
if (ImGui::InputText(label.c_str(), buf, sizeof(buf))) {
|
||||
val = buf;
|
||||
changed = true;
|
||||
}
|
||||
if (!val.empty() &&
|
||||
(val.rfind("http://", 0) == 0 || val.rfind("https://", 0) == 0)) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Open##url")) {
|
||||
if (!val.empty() &&
|
||||
(val.rfind("http://", 0) == 0 || val.rfind("https://", 0) == 0)) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TI_EXTERNAL_LINK "##url")) {
|
||||
#if defined(_WIN32)
|
||||
std::string cmd = "start \"\" \"" + val + "\"";
|
||||
std::string cmd = "start \"\" \"" + val + "\"";
|
||||
#else
|
||||
std::string cmd = "xdg-open '" + val + "' >/dev/null 2>&1 &";
|
||||
std::string cmd = "xdg-open '" + val + "' >/dev/null 2>&1 &";
|
||||
#endif
|
||||
int rc = std::system(cmd.c_str()); (void)rc;
|
||||
int rc = std::system(cmd.c_str()); (void)rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FK_DATE:
|
||||
case FK_STRING:
|
||||
default:
|
||||
if (ImGui::InputTextWithHint(label.c_str(),
|
||||
kind == FK_DATE ? "YYYY-MM-DD" : "",
|
||||
buf, sizeof(buf))) {
|
||||
val = buf;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_extra) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TI_TRASH "##rm")) {
|
||||
app.insp_field_keys.erase(app.insp_field_keys.begin() + i);
|
||||
app.insp_field_values.erase(app.insp_field_values.begin() + i);
|
||||
app.insp_is_extra.erase(app.insp_is_extra.begin() + i);
|
||||
ImGui::PopID();
|
||||
any_change = true;
|
||||
--i;
|
||||
continue;
|
||||
break;
|
||||
case FK_DATE:
|
||||
case FK_STRING:
|
||||
default:
|
||||
if (ImGui::InputTextWithHint("##v",
|
||||
kind == FK_DATE ? "YYYY-MM-DD" : "",
|
||||
buf, sizeof(buf))) {
|
||||
val = buf;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_extra) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TI_TRASH "##rm")) {
|
||||
app.insp_field_keys.erase(app.insp_field_keys.begin() + i);
|
||||
app.insp_field_values.erase(app.insp_field_values.begin() + i);
|
||||
app.insp_is_extra.erase(app.insp_is_extra.begin() + i);
|
||||
ImGui::PopID();
|
||||
any_change = true;
|
||||
--i;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (changed) any_change = true;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (changed) any_change = true;
|
||||
ImGui::PopID();
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user