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

@@ -390,11 +390,17 @@
Notifications
</a>
<a class="navbar-item" href="{% url 'ais' type='page' %}">
AI
Models
</a>
<a class="navbar-item" href="{% url 'command_routing' %}">
Command Routing
</a>
<a class="navbar-item" href="{% url 'translation_settings' %}">
Translation
</a>
<a class="navbar-item" href="{% url 'ai_execution_log' %}">
AI Execution Log
</a>
{% if user.is_superuser %}
<a class="navbar-item" href="{% url 'system_settings' %}">
System

View File

@@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block content %}
<style>
.ai-stat-box {
height: 100%;
min-height: 92px;
margin: 0;
}
</style>
<section class="section">
<div class="container">
<h1 class="title is-4">AI Execution Log</h1>
<p class="subtitle is-6">Tracked model calls and usage metrics for this account.</p>
<article class="box">
<div class="columns is-multiline">
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Total Runs</p><p class="title is-6">{{ stats.total_runs }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">OK</p><p class="title is-6 has-text-success">{{ stats.total_ok }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Failed</p><p class="title is-6 has-text-danger">{{ stats.total_failed }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Success Rate</p><p class="title is-6 has-text-info">{{ stats.success_rate }}%</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">24h Runs</p><p class="title is-6">{{ stats.last_24h_runs }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">24h Failed</p><p class="title is-6 has-text-warning">{{ stats.last_24h_failed }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">7d Runs</p><p class="title is-6">{{ stats.last_7d_runs }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Avg Duration</p><p class="title is-6">{{ stats.avg_duration_ms }}ms</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Prompt Chars</p><p class="title is-6">{{ stats.total_prompt_chars }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Response Chars</p><p class="title is-6">{{ stats.total_response_chars }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Avg Prompt</p><p class="title is-6">{{ stats.avg_prompt_chars }}</p></div></div>
<div class="column is-6-mobile is-4-tablet is-3-desktop"><div class="box ai-stat-box"><p class="heading">Avg Response</p><p class="title is-6">{{ stats.avg_response_chars }}</p></div></div>
</div>
</article>
<div class="columns">
<div class="column is-6">
<article class="box">
<h2 class="title is-6">By Operation</h2>
<table class="table is-fullwidth is-size-7 is-striped">
<thead>
<tr><th>Operation</th><th>Total</th><th>OK</th><th>Failed</th></tr>
</thead>
<tbody>
{% for row in operation_breakdown %}
<tr>
<td>{{ row.operation|default:"(none)" }}</td>
<td>{{ row.total }}</td>
<td>{{ row.ok }}</td>
<td>{{ row.failed }}</td>
</tr>
{% empty %}
<tr><td colspan="4">No runs yet.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
</div>
<div class="column is-6">
<article class="box">
<h2 class="title is-6">By Model</h2>
<table class="table is-fullwidth is-size-7 is-striped">
<thead>
<tr><th>Model</th><th>Total</th><th>OK</th><th>Failed</th></tr>
</thead>
<tbody>
{% for row in model_breakdown %}
<tr>
<td>{{ row.model|default:"(none)" }}</td>
<td>{{ row.total }}</td>
<td>{{ row.ok }}</td>
<td>{{ row.failed }}</td>
</tr>
{% empty %}
<tr><td colspan="4">No runs yet.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
</div>
</div>
<article class="box">
<h2 class="title is-6">Recent Runs</h2>
<div class="table-container">
<table class="table is-fullwidth is-size-7 is-striped">
<thead>
<tr>
<th>Started</th>
<th>Status</th>
<th>Operation</th>
<th>Model</th>
<th>Messages</th>
<th>Prompt</th>
<th>Response</th>
<th>Duration</th>
<th>Error</th>
</tr>
</thead>
<tbody>
{% for run in runs %}
<tr>
<td>{{ run.started_at }}</td>
<td>{{ run.status }}</td>
<td>{{ run.operation|default:"-" }}</td>
<td>{{ run.model|default:"-" }}</td>
<td>{{ run.message_count }}</td>
<td>{{ run.prompt_chars }}</td>
<td>{{ run.response_chars }}</td>
<td>{% if run.duration_ms %}{{ run.duration_ms }}ms{% else %}-{% endif %}</td>
<td style="max-width: 26rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="{{ run.error }}">{{ run.error|default:"-" }}</td>
</tr>
{% empty %}
<tr><td colspan="9">No runs yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
</div>
</section>
{% endblock %}

View File

@@ -8,59 +8,84 @@
<article class="box">
<h2 class="title is-6">Create Command Profile</h2>
<form method="post">
<p class="help">Create reusable command behavior. Example: <code>#bp#</code> reply command for business-plan extraction.</p>
<form method="post" aria-label="Create command profile">
{% csrf_token %}
<input type="hidden" name="action" value="profile_create">
<div class="columns">
<div class="column">
<input class="input is-small" name="slug" placeholder="slug (bp)" value="bp">
<label class="label is-size-7" for="create_slug">Slug</label>
<input id="create_slug" class="input is-small" name="slug" placeholder="slug (bp)" value="bp" aria-describedby="create_slug_help">
<p id="create_slug_help" class="help">Stable command id, e.g. <code>bp</code>.</p>
</div>
<div class="column">
<input class="input is-small" name="name" placeholder="name" value="Business Plan">
<label class="label is-size-7" for="create_name">Name</label>
<input id="create_name" class="input is-small" name="name" placeholder="name" value="Business Plan">
</div>
<div class="column">
<input class="input is-small" name="trigger_token" placeholder="trigger token" value="#bp#">
<label class="label is-size-7" for="create_trigger_token">Trigger Token</label>
<input id="create_trigger_token" class="input is-small" name="trigger_token" placeholder="trigger token" value="#bp#">
</div>
</div>
<textarea class="textarea is-small" name="template_text" rows="4" placeholder="Business plan template"></textarea>
<button class="button is-link is-small" type="submit">Create Profile</button>
<label class="label is-size-7" for="create_template_text">Template Text</label>
<textarea id="create_template_text" class="textarea is-small" name="template_text" rows="4" placeholder="Business plan template"></textarea>
<button class="button is-link is-small" style="margin-top: 0.75rem;" type="submit">Create Profile</button>
</form>
</article>
{% for profile in profiles %}
<article class="box">
<h2 class="title is-6">{{ profile.name }} ({{ profile.slug }})</h2>
<form method="post" style="margin-bottom: 0.75rem;">
<div class="content is-size-7" style="margin-bottom: 0.6rem;">
<p><strong>Flag Definitions</strong></p>
<ul>
<li><strong>enabled</strong>: master on/off switch for this command profile.</li>
<li><strong>reply required</strong>: command only runs when the trigger message is sent as a reply to another message.</li>
<li><strong>exact match</strong>: message text must be exactly the trigger token (for example <code>#bp#</code>) with no extra text.</li>
<li><strong>visibility = status_in_source</strong>: post command status updates back into the source channel.</li>
<li><strong>visibility = silent</strong>: do not post status updates in the source channel.</li>
<li><strong>binding direction ingress</strong>: channels where trigger messages are accepted.</li>
<li><strong>binding direction egress</strong>: channels where command outputs are posted.</li>
<li><strong>binding direction scratchpad_mirror</strong>: scratchpad/mirror channel used for relay-only behavior.</li>
<li><strong>action extract_bp</strong>: run AI extraction to produce business plan content.</li>
<li><strong>action save_document</strong>: save/editable document and revision history.</li>
<li><strong>action post_result</strong>: fan out generated result to enabled egress bindings.</li>
<li><strong>position</strong>: execution order (lower runs first).</li>
</ul>
</div>
<form method="post" style="margin-bottom: 0.75rem;" aria-label="Update command profile {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="profile_update">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
<div class="columns is-multiline">
<div class="column is-3">
<label class="label is-size-7">Name</label>
<input class="input is-small" name="name" value="{{ profile.name }}">
<label class="label is-size-7" for="profile_name_{{ profile.id }}">Name</label>
<input id="profile_name_{{ profile.id }}" class="input is-small" name="name" value="{{ profile.name }}">
</div>
<div class="column is-2">
<label class="label is-size-7">Trigger</label>
<input class="input is-small" name="trigger_token" value="{{ profile.trigger_token }}">
<label class="label is-size-7" for="trigger_token_{{ profile.id }}">Trigger</label>
<input id="trigger_token_{{ profile.id }}" class="input is-small" name="trigger_token" value="{{ profile.trigger_token }}">
</div>
<div class="column is-2">
<label class="label is-size-7">Visibility</label>
<label class="label is-size-7" for="visibility_mode_{{ profile.id }}">Visibility</label>
<div class="select is-small is-fullwidth">
<select name="visibility_mode">
<option value="status_in_source" {% if profile.visibility_mode == 'status_in_source' %}selected{% endif %}>status_in_source</option>
<option value="silent" {% if profile.visibility_mode == 'silent' %}selected{% endif %}>silent</option>
<select id="visibility_mode_{{ profile.id }}" name="visibility_mode">
<option value="status_in_source" {% if profile.visibility_mode == 'status_in_source' %}selected{% endif %}>Show Status In Source Chat</option>
<option value="silent" {% if profile.visibility_mode == 'silent' %}selected{% endif %}>Silent (No Status Message)</option>
</select>
</div>
</div>
<div class="column is-5">
<label class="label is-size-7">Flags</label>
<label class="checkbox is-size-7"><input type="checkbox" name="enabled" value="1" {% if profile.enabled %}checked{% endif %}> enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.6rem;"><input type="checkbox" name="reply_required" value="1" {% if profile.reply_required %}checked{% endif %}> reply required</label>
<label class="checkbox is-size-7" style="margin-left: 0.6rem;"><input type="checkbox" name="exact_match_only" value="1" {% if profile.exact_match_only %}checked{% endif %}> exact match</label>
<fieldset>
<legend class="label is-size-7">Flags</legend>
<label class="checkbox is-size-7"><input type="checkbox" name="enabled" value="1" {% if profile.enabled %}checked{% endif %}> enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.6rem;"><input type="checkbox" name="reply_required" value="1" {% if profile.reply_required %}checked{% endif %}> reply required</label>
<label class="checkbox is-size-7" style="margin-left: 0.6rem;"><input type="checkbox" name="exact_match_only" value="1" {% if profile.exact_match_only %}checked{% endif %}> exact match</label>
</fieldset>
</div>
</div>
<label class="label is-size-7">BP Template</label>
<textarea class="textarea is-small" name="template_text" rows="5">{{ profile.template_text }}</textarea>
<label class="label is-size-7" for="template_text_{{ profile.id }}">BP Template</label>
<textarea id="template_text_{{ profile.id }}" class="textarea is-small" name="template_text" rows="5">{{ profile.template_text }}</textarea>
<div class="buttons" style="margin-top: 0.6rem;">
<button class="button is-link is-small" type="submit">Save Profile</button>
</div>
@@ -69,18 +94,24 @@
<div class="columns">
<div class="column">
<h3 class="title is-7">Channel Bindings</h3>
<p class="help">A command runs only when the source channel is in <code>ingress</code>. Output is sent to all enabled <code>egress</code> bindings.</p>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr><th>Direction</th><th>Service</th><th>Channel</th><th></th></tr>
<tr><th scope="col">Direction</th><th scope="col">Service</th><th scope="col">Channel</th><th scope="col">Actions</th></tr>
</thead>
<tbody>
{% for binding in profile.channel_bindings.all %}
<tr>
<td>{{ binding.direction }}</td>
<td>
{% if binding.direction == "ingress" %}Ingress (Accept Triggers)
{% elif binding.direction == "egress" %}Egress (Post Results)
{% else %}Scratchpad Mirror
{% endif %}
</td>
<td>{{ binding.service }}</td>
<td>{{ binding.channel_identifier }}</td>
<td>
<form method="post">
<form method="post" aria-label="Delete binding {{ binding.direction }} {{ binding.service }} {{ binding.channel_identifier }}">
{% csrf_token %}
<input type="hidden" name="action" value="binding_delete">
<input type="hidden" name="binding_id" value="{{ binding.id }}">
@@ -93,23 +124,30 @@
{% endfor %}
</tbody>
</table>
<form method="post">
<form method="post" aria-label="Add channel binding for {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="binding_create">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
<div class="columns">
<div class="column">
<label class="label is-size-7" for="binding_direction_{{ profile.id }}">Direction</label>
<div class="select is-small is-fullwidth">
<select name="direction">
<select id="binding_direction_{{ profile.id }}" name="direction">
{% for value in directions %}
<option value="{{ value }}">{{ value }}</option>
<option value="{{ value }}">
{% if value == "ingress" %}Ingress (Accept Triggers)
{% elif value == "egress" %}Egress (Post Results)
{% else %}Scratchpad Mirror
{% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column">
<label class="label is-size-7" for="binding_service_{{ profile.id }}">Service</label>
<div class="select is-small is-fullwidth">
<select name="service">
<select id="binding_service_{{ profile.id }}" name="service">
{% for value in channel_services %}
<option value="{{ value }}">{{ value }}</option>
{% endfor %}
@@ -117,7 +155,8 @@
</div>
</div>
<div class="column">
<input class="input is-small" name="channel_identifier" placeholder="channel identifier">
<label class="label is-size-7" for="binding_channel_identifier_{{ profile.id }}">Channel Identifier</label>
<input id="binding_channel_identifier_{{ profile.id }}" class="input is-small" name="channel_identifier" placeholder="channel identifier">
</div>
<div class="column is-narrow">
<button class="button is-link is-small" type="submit">Add</button>
@@ -128,23 +167,45 @@
<div class="column">
<h3 class="title is-7">Actions</h3>
<p class="help">Enable/disable each step and set execution order with <code>position</code>.</p>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr><th>Type</th><th>Enabled</th><th>Position</th><th></th></tr>
<tr><th scope="col">Type</th><th scope="col">Enabled</th><th scope="col">Order</th><th scope="col">Actions</th></tr>
</thead>
<tbody>
{% for action_row in profile.actions.all %}
<tr>
<td>{{ action_row.action_type }}</td>
<td>{{ action_row.enabled }}</td>
<td>{{ action_row.position }}</td>
<td>
<form method="post">
{% if action_row.action_type == "extract_bp" %}Extract Business Plan
{% elif action_row.action_type == "save_document" %}Save Document
{% elif action_row.action_type == "post_result" %}Post Result
{% else %}{{ action_row.action_type }}
{% endif %}
</td>
<td>{{ action_row.enabled }}</td>
<td>{{ forloop.counter }}</td>
<td>
<div class="buttons are-small" style="margin-bottom: 0.35rem;">
<form method="post" style="display:inline;" aria-label="Move action {{ action_row.action_type }} up for {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="action_move">
<input type="hidden" name="command_action_id" value="{{ action_row.id }}">
<input type="hidden" name="direction" value="up">
<button class="button is-light" type="submit" {% if forloop.first %}disabled{% endif %} aria-label="Move up">Up</button>
</form>
<form method="post" style="display:inline;" aria-label="Move action {{ action_row.action_type }} down for {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="action_move">
<input type="hidden" name="command_action_id" value="{{ action_row.id }}">
<input type="hidden" name="direction" value="down">
<button class="button is-light" type="submit" {% if forloop.last %}disabled{% endif %} aria-label="Move down">Down</button>
</form>
</div>
<form method="post" aria-label="Update action {{ action_row.action_type }} for {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="action_update">
<input type="hidden" name="command_action_id" value="{{ action_row.id }}">
<label class="checkbox is-size-7"><input type="checkbox" name="enabled" value="1" {% if action_row.enabled %}checked{% endif %}> enabled</label>
<input class="input is-small" style="width: 5rem;" name="position" value="{{ action_row.position }}">
<button class="button is-link is-light is-small" type="submit">Save</button>
</form>
</td>
@@ -157,11 +218,11 @@
</div>
</div>
<form method="post" style="margin-top: 0.75rem;">
<form method="post" style="margin-top: 0.75rem;" aria-label="Delete profile {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="profile_delete">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
<button class="button is-danger is-light is-small" type="submit">Delete Profile</button>
<button class="button is-danger is-light is-small" type="submit" aria-label="Delete profile {{ profile.name }}">Delete Profile</button>
</form>
</article>
{% empty %}
@@ -172,7 +233,7 @@
<h2 class="title is-6">Business Plan Documents</h2>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr><th>Title</th><th>Status</th><th>Source</th><th>Updated</th><th></th></tr>
<tr><th scope="col">Title</th><th scope="col">Status</th><th scope="col">Source</th><th scope="col">Updated</th><th scope="col">Actions</th></tr>
</thead>
<tbody>
{% for doc in documents %}
@@ -181,7 +242,7 @@
<td>{{ doc.status }}</td>
<td>{{ doc.source_service }} · {{ doc.source_channel_identifier }}</td>
<td>{{ doc.updated_at }}</td>
<td><a class="button is-small is-link is-light" href="{% url 'business_plan_editor' doc_id=doc.id %}">Open</a></td>
<td><a class="button is-small is-link is-light" href="{% url 'business_plan_editor' doc_id=doc.id %}" aria-label="Open business plan document {{ doc.title }}">Open</a></td>
</tr>
{% empty %}
<tr><td colspan="5">No business plan documents yet.</td></tr>
@@ -190,78 +251,6 @@
</table>
</article>
<article class="box">
<h2 class="title is-6">Translation Bridges</h2>
<form method="post" style="margin-bottom: 0.75rem;">
{% csrf_token %}
<input type="hidden" name="action" value="bridge_create">
<div class="columns is-multiline">
<div class="column is-2"><input class="input is-small" name="name" placeholder="name"></div>
<div class="column is-2"><input class="input is-small" name="quick_mode_title" placeholder="quick mode: en|es"></div>
<div class="column is-2">
<div class="select is-small is-fullwidth"><select name="a_service">{% for value in channel_services %}<option value="{{ value }}">{{ value }}</option>{% endfor %}</select></div>
</div>
<div class="column is-2"><input class="input is-small" name="a_channel_identifier" placeholder="A channel"></div>
<div class="column is-1"><input class="input is-small" name="a_language" value="en"></div>
<div class="column is-2">
<div class="select is-small is-fullwidth"><select name="b_service">{% for value in channel_services %}<option value="{{ value }}">{{ value }}</option>{% endfor %}</select></div>
</div>
<div class="column is-2"><input class="input is-small" name="b_channel_identifier" placeholder="B channel"></div>
<div class="column is-1"><input class="input is-small" name="b_language" value="es"></div>
<div class="column is-2">
<div class="select is-small is-fullwidth"><select name="direction">{% for value in bridge_directions %}<option value="{{ value }}">{{ value }}</option>{% endfor %}</select></div>
</div>
<div class="column is-1"><button class="button is-link is-small" type="submit">Add</button></div>
</div>
</form>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr><th>Name</th><th>A</th><th>B</th><th>Direction</th><th></th></tr>
</thead>
<tbody>
{% for bridge in bridges %}
<tr>
<td>{{ bridge.name }}</td>
<td>{{ bridge.a_service }} · {{ bridge.a_channel_identifier }} · {{ bridge.a_language }}</td>
<td>{{ bridge.b_service }} · {{ bridge.b_channel_identifier }} · {{ bridge.b_language }}</td>
<td>{{ bridge.direction }}</td>
<td>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="bridge_delete">
<input type="hidden" name="bridge_id" value="{{ bridge.id }}">
<button class="button is-danger is-light is-small" type="submit">Delete</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="5">No translation bridges configured.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
<article class="box">
<h2 class="title is-6">Translation Event Log</h2>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr><th>Bridge</th><th>Status</th><th>Target</th><th>Error</th><th>At</th></tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td>{{ event.bridge.name }}</td>
<td>{{ event.status }}</td>
<td>{{ event.target_service }} · {{ event.target_channel }}</td>
<td>{{ event.error|default:"-" }}</td>
<td>{{ event.created_at }}</td>
</tr>
{% empty %}
<tr><td colspan="5">No events yet.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
</div>
</section>
{% endblock %}

View File

@@ -1,17 +1,20 @@
{% extends "base.html" %}
{% block content %}
<div class="columns is-centered">
<div class="column is-11-tablet is-10-desktop is-9-widescreen">
<div class="is-flex is-justify-content-space-between is-align-items-center">
<div>
<h1 class="title is-4">Search</h1>
<p class="subtitle is-6">
Search across OSINT objects with sortable, paginated results.
</p>
<section class="section">
<div class="container is-max-desktop">
<div class="mb-4">
<h1 class="title is-4 mb-2">Search</h1>
<p class="subtitle is-6 mb-3">
Unified lookup across contacts, identifiers, and messages, with advanced filters for source, date range, sentiment, sort, dedup, and reverse.
</p>
<div class="tags">
<span class="tag is-light">Default Scope: All</span>
<span class="tag is-light">Contacts + Messages</span>
<span class="tag is-light">SIQTSRSS/ADR Controls</span>
</div>
</div>
{% include "partials/osint/search-panel.html" %}
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title is-4">Translation Settings</h1>
<p class="subtitle is-6">Configure translation bridges, routing direction, and inspect sync events.</p>
<article class="box">
<h2 class="title is-6">Translation Bridges</h2>
<form method="post" style="margin-bottom: 0.75rem;" aria-label="Create translation bridge">
{% csrf_token %}
<input type="hidden" name="action" value="bridge_create">
<div class="columns is-multiline">
<div class="column is-2"><label class="label is-size-7" for="bridge_name">Name</label><input id="bridge_name" class="input is-small" name="name" placeholder="name"></div>
<div class="column is-2"><label class="label is-size-7" for="bridge_quick_title">Quick Mode Hint</label><input id="bridge_quick_title" class="input is-small" name="quick_mode_title" placeholder="quick mode: en|es"></div>
<div class="column is-2">
<label class="label is-size-7" for="bridge_a_service">A Service</label>
<div class="select is-small is-fullwidth"><select id="bridge_a_service" name="a_service">{% for value in channel_services %}<option value="{{ value }}">{{ value }}</option>{% endfor %}</select></div>
</div>
<div class="column is-2"><label class="label is-size-7" for="bridge_a_channel">A Channel</label><input id="bridge_a_channel" class="input is-small" name="a_channel_identifier" placeholder="A channel"></div>
<div class="column is-1"><label class="label is-size-7" for="bridge_a_language">A Lang</label><input id="bridge_a_language" class="input is-small" name="a_language" value="en"></div>
<div class="column is-2">
<label class="label is-size-7" for="bridge_b_service">B Service</label>
<div class="select is-small is-fullwidth"><select id="bridge_b_service" name="b_service">{% for value in channel_services %}<option value="{{ value }}">{{ value }}</option>{% endfor %}</select></div>
</div>
<div class="column is-2"><label class="label is-size-7" for="bridge_b_channel">B Channel</label><input id="bridge_b_channel" class="input is-small" name="b_channel_identifier" placeholder="B channel"></div>
<div class="column is-1"><label class="label is-size-7" for="bridge_b_language">B Lang</label><input id="bridge_b_language" class="input is-small" name="b_language" value="es"></div>
<div class="column is-2">
<label class="label is-size-7" for="bridge_direction">Direction</label>
<div class="select is-small is-fullwidth"><select id="bridge_direction" name="direction">{% for value in bridge_directions %}<option value="{{ value }}">{{ value }}</option>{% endfor %}</select></div>
</div>
<div class="column is-1"><button class="button is-link is-small" type="submit">Add</button></div>
</div>
</form>
<table class="table is-fullwidth is-striped is-size-7">
<caption>Configured translation bridges</caption>
<thead>
<tr><th scope="col">Name</th><th scope="col">A</th><th scope="col">B</th><th scope="col">Direction</th><th scope="col">Actions</th></tr>
</thead>
<tbody>
{% for bridge in bridges %}
<tr>
<td>{{ bridge.name }}</td>
<td>{{ bridge.a_service }} · {{ bridge.a_channel_identifier }} · {{ bridge.a_language }}</td>
<td>{{ bridge.b_service }} · {{ bridge.b_channel_identifier }} · {{ bridge.b_language }}</td>
<td>
{% if bridge.direction == "a_to_b" %}A to B
{% elif bridge.direction == "b_to_a" %}B to A
{% else %}Bidirectional
{% endif %}
</td>
<td>
<form method="post" aria-label="Delete translation bridge {{ bridge.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="bridge_delete">
<input type="hidden" name="bridge_id" value="{{ bridge.id }}">
<button class="button is-danger is-light is-small" type="submit">Delete</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="5">No translation bridges configured.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
<article class="box">
<h2 class="title is-6">Translation Event Log</h2>
<table class="table is-fullwidth is-striped is-size-7">
<caption>Recent translation sync events</caption>
<thead>
<tr><th scope="col">Bridge</th><th scope="col">Status</th><th scope="col">Target</th><th scope="col">Error</th><th scope="col">At</th></tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td>{{ event.bridge.name }}</td>
<td>{{ event.status }}</td>
<td>{{ event.target_service }} · {{ event.target_channel }}</td>
<td>{{ event.error|default:"-" }}</td>
<td>{{ event.created_at }}</td>
</tr>
{% empty %}
<tr><td colspan="5">No events yet.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
</div>
</section>
{% endblock %}

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>