Improve search

This commit is contained in:
2026-03-02 02:26:25 +00:00
parent a9f5f3f75d
commit b94219fc5b
20 changed files with 1626 additions and 314 deletions

View File

@@ -1,11 +1,13 @@
{% include 'mixins/partials/notify.html' %}
<div
id="{{ osint_table_id }}"
class="osint-table-shell"
hx-get="{{ osint_refresh_url }}"
hx-target="#{{ osint_table_id }}"
hx-swap="outerHTML"
{% if osint_event_name %}hx-trigger="{{ osint_event_name }} from:body"{% endif %}>
class="osint-table-shell {% if osint_shell_borderless %}osint-table-shell--borderless{% endif %}"
{% if osint_event_name %}
hx-get="{{ osint_refresh_url }}"
hx-target="#{{ osint_table_id }}"
hx-swap="outerHTML"
hx-trigger="{{ osint_event_name }} from:body"
{% endif %}>
{% if osint_show_search %}
<form
@@ -53,9 +55,15 @@
<div class="osint-results-meta">
<div class="osint-results-meta-left">
<div class="dropdown is-hoverable" id="{{ osint_table_id }}-columns-dropdown">
<div class="dropdown" id="{{ osint_table_id }}-columns-dropdown" hx-disable>
<div class="dropdown-trigger">
<button class="button is-small is-light" aria-haspopup="true" aria-controls="{{ osint_table_id }}-columns-menu">
<button
class="button is-small is-light"
type="button"
data-role="columns-toggle-trigger"
aria-haspopup="true"
aria-expanded="false"
aria-controls="{{ osint_table_id }}-columns-menu">
<span>Show/Hide Fields</span>
<span class="icon is-small"><i class="fa-solid fa-angle-down"></i></span>
</button>
@@ -63,15 +71,15 @@
<div class="dropdown-menu" id="{{ osint_table_id }}-columns-menu" role="menu">
<div class="dropdown-content">
{% for column in osint_columns %}
<a
<button
type="button"
class="dropdown-item osint-col-toggle"
data-col-index="{{ forloop.counter0 }}"
href="#"
onclick="return false;">
aria-pressed="false">
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
<span>{{ column.label }}</span>
<span class="is-size-7 has-text-grey ml-2">({{ column.field_name }})</span>
</a>
</button>
{% endfor %}
</div>
</div>
@@ -126,11 +134,33 @@
{% if cell.kind == "id_copy" %}
<a
class="button is-small has-text-grey"
title="Copy value"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ cell.value }}');">
<span class="icon">
<i class="fa-solid fa-copy"></i>
</span>
<span>{{ cell.value }}</span>
</a>
{% elif cell.kind == "chat_ref" %}
{% if cell.value.copy %}
<a
class="button is-small has-text-grey"
title="Copy chat id"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ cell.value.copy }}');">
<span class="icon">
<i class="fa-solid fa-copy"></i>
</span>
</a>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
{% elif cell.value.copy %}
<a
class="button is-small has-text-grey"
title="Copy value"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ cell.value.copy }}');">
<span class="icon">
<i class="fa-solid fa-copy"></i>
</span>
</a>
{% elif cell.kind == "bool" %}
{% if cell.value %}
@@ -281,6 +311,22 @@
#{{ osint_table_id }} .osint-col-toggle.is-hidden-col .icon {
color: #b5b5b5;
}
#{{ osint_table_id }} .osint-col-toggle {
width: 100%;
text-align: left;
border: 0;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.35rem;
}
#{{ osint_table_id }}.osint-table-shell--borderless {
border: 0;
border-radius: 0;
padding: 0;
background: transparent;
}
</style>
<script>
@@ -292,6 +338,13 @@
if (!shell) {
return;
}
const dropdown = document.getElementById(tableId + "-columns-dropdown");
const trigger = dropdown
? dropdown.querySelector("[data-role='columns-toggle-trigger']")
: null;
const dropdownMenu = dropdown
? dropdown.querySelector(".dropdown-menu")
: null;
const storageKey = [
"gia_osint_hidden_cols_v2",
tableId,
@@ -319,6 +372,7 @@
const idx = String(toggle.getAttribute("data-col-index") || "");
const isHidden = hiddenSet.has(idx);
toggle.classList.toggle("is-hidden-col", isHidden);
toggle.setAttribute("aria-pressed", isHidden ? "true" : "false");
const icon = toggle.querySelector("i");
if (icon) {
icon.className = isHidden ? "fa-solid fa-xmark" : "fa-solid fa-check";
@@ -335,7 +389,9 @@
};
shell.querySelectorAll(".osint-col-toggle").forEach(function (toggle) {
toggle.addEventListener("click", function () {
toggle.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
const idx = String(toggle.getAttribute("data-col-index") || "");
if (!idx) {
return;
@@ -347,9 +403,51 @@
}
persist();
applyVisibility();
if (dropdown) {
dropdown.classList.remove("is-active");
}
if (trigger) {
trigger.setAttribute("aria-expanded", "false");
}
});
});
if (trigger && dropdown) {
trigger.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
const nextActive = !dropdown.classList.contains("is-active");
dropdown.classList.toggle("is-active", nextActive);
trigger.setAttribute("aria-expanded", nextActive ? "true" : "false");
});
}
if (dropdownMenu) {
dropdownMenu.addEventListener("click", function (event) {
event.stopPropagation();
});
}
document.addEventListener("click", function (event) {
if (!dropdown) {
return;
}
if (dropdown.contains(event.target)) {
return;
}
dropdown.classList.remove("is-active");
if (trigger) {
trigger.setAttribute("aria-expanded", "false");
}
});
document.addEventListener("keydown", function (event) {
if (event.key !== "Escape" || !dropdown) {
return;
}
dropdown.classList.remove("is-active");
if (trigger) {
trigger.setAttribute("aria-expanded", "false");
}
});
applyVisibility();
})();
</script>

View File

@@ -1,78 +1,176 @@
<div class="box">
<form
class="osint-search-form"
method="get"
action="{{ osint_search_url }}"
hx-get="{{ osint_search_url }}"
hx-target="#osint-search-results"
hx-swap="innerHTML">
<div class="columns is-multiline">
<div class="column is-4">
<label class="label">Scope</label>
<div class="select is-fullwidth">
<select name="scope">
{% for option in scope_options %}
<option
value="{{ option.value }}"
{% if option.value == selected_scope %}selected{% endif %}>
{{ option.label }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-4">
<label class="label">Field</label>
<div class="select is-fullwidth">
<select name="field">
<option value="__all__" {% if selected_field == "__all__" %}selected{% endif %}>
All Fields
<form
class="osint-search-form box"
method="get"
action="{{ osint_search_url }}"
hx-get="{{ osint_search_url }}"
hx-trigger="change delay:120ms"
hx-target="#osint-search-results"
hx-swap="innerHTML">
<div class="columns is-multiline">
<div class="column is-4">
<label class="label">Scope</label>
<div class="select">
<select name="scope">
{% for option in scope_options %}
<option
value="{{ option.value }}"
{% if option.value == selected_scope %}selected{% endif %}>
{{ option.label }}
</option>
{% for option in field_options %}
<option
value="{{ option.value }}"
{% if option.value == selected_field %}selected{% endif %}>
{{ option.label }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-4">
<label class="label">Rows Per Page</label>
<div class="select is-fullwidth">
<select name="per_page">
<option value="10" {% if selected_per_page == 10 %}selected{% endif %}>10</option>
<option value="20" {% if selected_per_page == 20 %}selected{% endif %}>20</option>
<option value="50" {% if selected_per_page == 50 %}selected{% endif %}>50</option>
<option value="100" {% if selected_per_page == 100 %}selected{% endif %}>100</option>
</select>
</div>
</div>
<div class="column is-9">
<label class="label">Search Query</label>
<input
class="input"
type="text"
name="q"
value="{{ search_query }}"
placeholder="Search text, values, or relation names...">
</div>
<div class="column is-3">
<label class="label">&nbsp;</label>
<div class="buttons">
<button class="button is-link is-light is-fullwidth" type="submit">
Search
</button>
<a class="button is-light is-fullwidth" href="{{ osint_search_url }}">
Reset
</a>
</div>
{% endfor %}
</select>
</div>
</div>
<div class="column is-4">
<label class="label">Field</label>
<div class="select">
<select name="field">
<option value="__all__" {% if selected_field == "__all__" %}selected{% endif %}>
All Fields
</option>
{% for option in field_options %}
<option
value="{{ option.value }}"
{% if option.value == selected_field %}selected{% endif %}>
{{ option.label }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-4">
<label class="label">Rows Per Page</label>
<div class="select">
<select name="per_page">
<option value="10" {% if selected_per_page == 10 %}selected{% endif %}>10</option>
<option value="20" {% if selected_per_page == 20 %}selected{% endif %}>20</option>
<option value="50" {% if selected_per_page == 50 %}selected{% endif %}>50</option>
<option value="100" {% if selected_per_page == 100 %}selected{% endif %}>100</option>
</select>
</div>
</div>
<div class="column is-9">
<label class="label">Search Query</label>
<input
class="input"
type="text"
name="q"
hx-get="{{ osint_search_url }}"
hx-trigger="keyup changed delay:220ms"
hx-include="closest form"
hx-target="#osint-search-results"
hx-swap="innerHTML"
value="{{ search_query }}"
placeholder="Search contacts, identifiers, and messages...">
</div>
<div class="column is-3">
<label class="label">&nbsp;</label>
<div class="buttons">
<button class="button is-link is-light is-fullwidth" type="submit">
Search
</button>
<a class="button is-light is-fullwidth" href="{{ osint_search_url }}" style="margin-bottom: 0.55rem;">
Reset
</a>
</div>
</div>
</form>
<div id="osint-search-results">
{% include "partials/results_table.html" %}
</div>
<details style="margin-top: 0.45rem;">
<summary class="has-text-weight-semibold">Advanced Filters (SIQTSRSS/ADR)</summary>
<p class="help" style="margin: 0.1rem 0 0.2rem 0;">Tip: use inline tags like <code>tag:messages</code> or <code>tag:people</code> in the query to narrow All-scope results quickly.</p>
<p class="help" style="margin: 0 0 0.35rem 0;"><strong>Source Service</strong>, <strong>From Date</strong>, <strong>To Date</strong>, and <strong>Sort Mode</strong> shape which results you see and in what order.</p>
<div style="margin-top: 0.2rem;">
<div class="columns is-multiline" style="margin-top: 0.3rem;">
<div class="column is-12-mobile is-6-tablet is-4-desktop">
<label class="label">Source Service</label>
<div class="select">
<select name="source">
<option value="all" {% if selected_source == "all" %}selected{% endif %}>All</option>
<option value="web" {% if selected_source == "web" %}selected{% endif %}>Web</option>
<option value="xmpp" {% if selected_source == "xmpp" %}selected{% endif %}>XMPP</option>
<option value="signal" {% if selected_source == "signal" %}selected{% endif %}>Signal</option>
<option value="whatsapp" {% if selected_source == "whatsapp" %}selected{% endif %}>WhatsApp</option>
</select>
</div>
</div>
<div class="column is-12"></div>
<div class="column is-12-mobile is-6-tablet is-3-desktop">
<label class="label">From Date</label>
<input
class="input{% if selected_date_from %} date-has-value{% endif %}"
type="date"
name="date_from"
value="{{ selected_date_from }}"
aria-label="From date">
</div>
<div class="column is-12-mobile is-6-tablet is-3-desktop">
<label class="label">To Date</label>
<input
class="input{% if selected_date_to %} date-has-value{% endif %}"
type="date"
name="date_to"
value="{{ selected_date_to }}"
aria-label="To date">
</div>
<div class="column is-12-mobile is-6-tablet is-3-desktop">
<label class="label">Sort Mode</label>
<div class="select">
<select name="sort_mode">
<option value="relevance" {% if selected_sort_mode == "relevance" %}selected{% endif %}>Relevance</option>
<option value="recent" {% if selected_sort_mode == "recent" %}selected{% endif %}>Recent First</option>
<option value="oldest" {% if selected_sort_mode == "oldest" %}selected{% endif %}>Oldest First</option>
</select>
</div>
</div>
<div class="column is-12"></div>
<div class="column is-12-mobile is-6-tablet is-2-desktop">
<label class="label">Min Sent.</label>
<input class="input" type="number" step="0.01" min="-1" max="1" name="sentiment_min" value="{{ selected_sentiment_min }}">
</div>
<div class="column is-12-mobile is-6-tablet is-2-desktop">
<label class="label">Max Sent.</label>
<input class="input" type="number" step="0.01" min="-1" max="1" name="sentiment_max" value="{{ selected_sentiment_max }}">
</div>
<div class="column is-12"></div>
<div class="column is-12">
<label class="checkbox" style="margin-right: 0.9rem;">
<input type="checkbox" name="annotate" value="1" {% if selected_annotate %}checked{% endif %}>
Annotate snippets
</label>
<label class="checkbox" style="margin-right: 0.9rem;">
<input type="checkbox" name="dedup" value="1" {% if selected_dedup %}checked{% endif %}>
Deduplicate
</label>
<label class="checkbox">
<input type="checkbox" name="reverse" value="1" {% if selected_reverse %}checked{% endif %}>
Reverse output
</label>
</div>
</div>
<div class="content is-size-7" style="margin-top: 0.2rem;">
<ul>
<li><strong>Min/Max Sent.</strong>: sentiment bounds for people/contact results (-1 to 1).</li>
<li><strong>Annotate snippets</strong>: shows contextual snippets around query hits.</li>
<li><strong>Deduplicate</strong>: removes near-identical repeated rows.</li>
<li><strong>Reverse output</strong>: reverses final result order after sorting.</li>
</ul>
</div>
</div>
</details>
</form>
<div id="osint-search-results">
{% include "partials/results_table.html" %}
</div>
<style>
.date-has-value::-webkit-calendar-picker-indicator {
filter: brightness(0) saturate(100%) invert(38%) sepia(85%) saturate(1820%) hue-rotate(201deg) brightness(96%) contrast(92%);
}
</style>