Implement executing tasks
This commit is contained in:
@@ -377,6 +377,9 @@
|
||||
<a class="navbar-item" href="{% url 'sessions' type='page' %}">
|
||||
Sessions
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'command_routing' %}#bp-documents">
|
||||
Documents
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -401,6 +404,9 @@
|
||||
<a class="navbar-item" href="{% url 'tasks_settings' %}">
|
||||
Task Settings
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'availability_settings' %}">
|
||||
Availability
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'translation_settings' %}">
|
||||
Translation
|
||||
</a>
|
||||
|
||||
128
core/templates/pages/availability-settings.html
Normal file
128
core/templates/pages/availability-settings.html
Normal file
@@ -0,0 +1,128 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title is-4">Availability Settings</h1>
|
||||
<form method="post" class="box">
|
||||
{% csrf_token %}
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-3"><label class="checkbox"><input type="checkbox" name="enabled" {% if settings_row.enabled %}checked{% endif %}> Enabled</label></div>
|
||||
<div class="column is-3"><label class="checkbox"><input type="checkbox" name="show_in_chat" {% if settings_row.show_in_chat %}checked{% endif %}> Show In Chat</label></div>
|
||||
<div class="column is-3"><label class="checkbox"><input type="checkbox" name="show_in_groups" {% if settings_row.show_in_groups %}checked{% endif %}> Show In Groups</label></div>
|
||||
<div class="column is-3"><label class="checkbox"><input type="checkbox" name="inference_enabled" {% if settings_row.inference_enabled %}checked{% endif %}> Inference Enabled</label></div>
|
||||
<div class="column is-3">
|
||||
<label class="label is-size-7">Retention Days</label>
|
||||
<input class="input is-small" type="number" min="1" name="retention_days" value="{{ settings_row.retention_days }}">
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label is-size-7">Fade Threshold (seconds)</label>
|
||||
<input class="input is-small" type="number" min="30" name="fade_threshold_seconds" value="{{ settings_row.fade_threshold_seconds }}">
|
||||
</div>
|
||||
</div>
|
||||
<button class="button is-link is-small" type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
<form method="get" class="box">
|
||||
<h2 class="title is-6">Timeline Filters</h2>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-3">
|
||||
<label class="label is-size-7">Person</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="person">
|
||||
<option value="">All</option>
|
||||
{% for person in people %}
|
||||
<option value="{{ person.id }}" {% if filters.person == person.id|stringformat:"s" %}selected{% endif %}>{{ person.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Service</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="service">
|
||||
<option value="">All</option>
|
||||
{% for item in service_choices %}
|
||||
<option value="{{ item }}" {% if filters.service == item %}selected{% endif %}>{{ item }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">State</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="state">
|
||||
<option value="">All</option>
|
||||
{% for item in state_choices %}
|
||||
<option value="{{ item }}" {% if filters.state == item %}selected{% endif %}>{{ item }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Source</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="source_kind">
|
||||
<option value="">All</option>
|
||||
{% for item in source_kind_choices %}
|
||||
<option value="{{ item }}" {% if filters.source_kind == item %}selected{% endif %}>{{ item }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Start (ISO)</label>
|
||||
<input class="input is-small" type="datetime-local" name="start" value="{{ filters.start }}">
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">End (ISO)</label>
|
||||
<input class="input is-small" type="datetime-local" name="end" value="{{ filters.end }}">
|
||||
</div>
|
||||
</div>
|
||||
<button class="button is-light is-small" type="submit">Apply</button>
|
||||
</form>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-6">Availability Events</h2>
|
||||
<table class="table is-fullwidth is-striped is-size-7">
|
||||
<thead><tr><th>ts</th><th>person</th><th>service</th><th>source</th><th>state</th><th>confidence</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in events %}
|
||||
<tr>
|
||||
<td>{{ row.ts }}</td>
|
||||
<td>{{ row.person.name }}</td>
|
||||
<td>{{ row.service }}</td>
|
||||
<td>{{ row.source_kind }}</td>
|
||||
<td>{{ row.availability_state }}</td>
|
||||
<td>{{ row.confidence|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6">No events in range.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-6">Availability Spans</h2>
|
||||
<table class="table is-fullwidth is-striped is-size-7">
|
||||
<thead><tr><th>person</th><th>service</th><th>state</th><th>start</th><th>end</th><th>confidence</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in spans %}
|
||||
<tr>
|
||||
<td>{{ row.person.name }}</td>
|
||||
<td>{{ row.service }}</td>
|
||||
<td>{{ row.state }}</td>
|
||||
<td>{{ row.start_ts }}</td>
|
||||
<td>{{ row.end_ts }}</td>
|
||||
<td>{{ row.confidence_start|floatformat:2 }} -> {{ row.confidence_end|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6">No spans in range.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -150,10 +150,10 @@
|
||||
{% if variant.warn_verbatim_plan %}
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<p class="help has-text-warning-dark">
|
||||
Warning: <strong>{{ variant.variant_label }}</strong> is in <code>verbatim</code> mode with plan fanout enabled.
|
||||
<article class="command-variant-warning">
|
||||
<strong>Warning:</strong> <strong>{{ variant.variant_label }}</strong> is in <code>verbatim</code> mode with plan fanout enabled.
|
||||
Recipients will get raw transcript-style output.
|
||||
</p>
|
||||
</article>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
@@ -188,12 +188,15 @@
|
||||
|
||||
<h4 class="title is-7" style="margin-top: 0.95rem;">Effective Destinations</h4>
|
||||
{% if profile.enabled_egress_bindings %}
|
||||
<ul class="is-size-7">
|
||||
<ul class="command-destination-list is-size-7">
|
||||
{% for row in profile.enabled_egress_bindings %}
|
||||
<li>{{ row.service }} · <code>{{ row.channel_identifier }}</code></li>
|
||||
<li class="command-destination-item">
|
||||
<span class="tag is-link is-light is-rounded is-small">{{ row.service }}</span>
|
||||
<code>{{ row.channel_identifier }}</code>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="help">{{ profile.enabled_egress_bindings|length }} enabled egress destination{{ profile.enabled_egress_bindings|length|pluralize }}.</p>
|
||||
<p class="command-destination-summary">{{ profile.enabled_egress_bindings|length }} enabled egress destination{{ profile.enabled_egress_bindings|length|pluralize }}.</p>
|
||||
{% else %}
|
||||
<article class="notification is-warning is-light is-size-7">No enabled egress destinations. Plan fanout will show sent:0.</article>
|
||||
{% endif %}
|
||||
@@ -383,7 +386,7 @@
|
||||
<article class="notification is-light">No command profiles configured.</article>
|
||||
{% endfor %}
|
||||
|
||||
<article class="box">
|
||||
<article class="box" id="bp-documents">
|
||||
<h2 class="title is-6">Business Plan Documents</h2>
|
||||
<table class="table is-fullwidth is-striped is-size-7">
|
||||
<thead>
|
||||
@@ -436,5 +439,44 @@
|
||||
.command-order-capsule-form + .command-order-capsule-form .command-order-btn {
|
||||
border-top: 1px solid #dbdbdb;
|
||||
}
|
||||
.command-variant-warning {
|
||||
border: 1px solid rgba(171, 109, 17, 0.45);
|
||||
background: linear-gradient(180deg, rgba(255, 246, 226, 0.98), rgba(255, 238, 204, 0.95));
|
||||
color: #6e450e;
|
||||
border-radius: 8px;
|
||||
padding: 0.48rem 0.62rem;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.command-destination-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.28rem;
|
||||
}
|
||||
.command-destination-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.42rem;
|
||||
background: rgba(244, 248, 255, 0.85);
|
||||
border: 1px solid rgba(58, 103, 165, 0.2);
|
||||
border-radius: 7px;
|
||||
padding: 0.26rem 0.38rem;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
.command-destination-summary {
|
||||
margin-top: 0.44rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(239, 247, 255, 0.95);
|
||||
border: 1px solid rgba(58, 103, 165, 0.25);
|
||||
padding: 0.16rem 0.52rem;
|
||||
font-size: 0.73rem;
|
||||
color: #284d7c;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -316,21 +316,112 @@
|
||||
|
||||
<div class="column is-6">
|
||||
<section class="tasks-panel">
|
||||
<h3 class="title is-7">Provider</h3>
|
||||
<h3 class="title is-7">Providers</h3>
|
||||
<p class="help">Controls outbound sync to external tracking systems. If disabled, tasks are still derived and visible inside GIA only.</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="provider_update">
|
||||
<input type="hidden" name="provider" value="mock">
|
||||
<label class="checkbox"><input type="checkbox" name="enabled" value="1" {% if provider_configs and provider_configs.0.enabled %}checked{% endif %}> Enable mock provider</label>
|
||||
<label class="checkbox"><input type="checkbox" name="enabled" value="1" {% if mock_provider_config and mock_provider_config.enabled %}checked{% endif %}> Enable mock provider</label>
|
||||
<p class="help">Mock provider logs sync events without writing to a real third-party system.</p>
|
||||
<div style="margin-top:0.5rem;">
|
||||
<button class="button is-small is-link is-light" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="provider_update">
|
||||
<input type="hidden" name="provider" value="codex_cli">
|
||||
<label class="checkbox"><input type="checkbox" name="enabled" value="1" {% if codex_provider_config and codex_provider_config.enabled %}checked{% endif %}> Enable Codex CLI provider</label>
|
||||
<p class="help">Codex task-sync runs in a dedicated worker (<code>python manage.py codex_worker</code>).</p>
|
||||
<div class="field" style="margin-top:0.5rem;">
|
||||
<label class="label is-size-7">Command</label>
|
||||
<input class="input is-small" name="command" value="{{ codex_provider_settings.command }}" placeholder="codex">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Workspace Root</label>
|
||||
<input class="input is-small" name="workspace_root" value="{{ codex_provider_settings.workspace_root }}" placeholder="/code/xf">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Default Profile</label>
|
||||
<input class="input is-small" name="default_profile" value="{{ codex_provider_settings.default_profile }}" placeholder="default">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Timeout Seconds</label>
|
||||
<input class="input is-small" type="number" min="1" name="timeout_seconds" value="{{ codex_provider_settings.timeout_seconds }}">
|
||||
</div>
|
||||
<div style="margin-top:0.5rem;">
|
||||
<button class="button is-small is-link is-light" type="submit">Save Codex Provider</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="help">Browse all derived tasks in <a href="{% url 'tasks_hub' %}">Tasks Hub</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<section class="tasks-panel">
|
||||
<h3 class="title is-7">External Chat Links</h3>
|
||||
<p class="help">Map a contact to an external Codex chat/session ID for task-sync metadata.</p>
|
||||
<form method="post" class="block">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="external_chat_link_upsert">
|
||||
<div class="columns tasks-settings-inline-columns">
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Provider</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="provider">
|
||||
<option value="codex_cli" selected>codex_cli</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<label class="label is-size-7">Contact Identifier</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="person_identifier_id">
|
||||
<option value="">Unlinked</option>
|
||||
{% for row in person_identifiers %}
|
||||
<option value="{{ row.id }}">{{ row.person.name }} · {{ row.service }} · {{ row.identifier }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label is-size-7">External Chat ID</label>
|
||||
<input class="input is-small" name="external_chat_id" placeholder="codex-chat-...">
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Enabled</label>
|
||||
<label class="checkbox"><input type="checkbox" name="enabled" value="1" checked> Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="button is-small is-link is-light" type="submit">Save Link</button>
|
||||
</form>
|
||||
<table class="table is-fullwidth is-striped is-size-7">
|
||||
<thead><tr><th>Provider</th><th>Person</th><th>Identifier</th><th>External Chat</th><th>Enabled</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in external_chat_links %}
|
||||
<tr>
|
||||
<td>{{ row.provider }}</td>
|
||||
<td>{% if row.person %}{{ row.person.name }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.person_identifier %}{{ row.person_identifier.service }} · {{ row.person_identifier.identifier }}{% else %}-{% endif %}</td>
|
||||
<td>{{ row.external_chat_id }}</td>
|
||||
<td>{{ row.enabled }}</td>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="external_chat_link_delete">
|
||||
<input type="hidden" name="external_link_id" value="{{ row.id }}">
|
||||
<button class="button is-danger is-light is-small" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6">No external chat links.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +142,18 @@
|
||||
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
|
||||
<span>AI Workspace</span>
|
||||
</a>
|
||||
{% if ai_workspace_graphs_url and ai_workspace_info_url %}
|
||||
<span class="compose-insights-capsule">
|
||||
<a class="compose-insights-link" href="{{ ai_workspace_graphs_url }}">
|
||||
<span class="icon is-small"><i class="fa-solid fa-chart-line"></i></span>
|
||||
<span>Graphs</span>
|
||||
</a>
|
||||
<a class="compose-insights-link" href="{{ ai_workspace_info_url }}">
|
||||
<span class="icon is-small"><i class="fa-solid fa-circle-info"></i></span>
|
||||
<span>Info</span>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if ai_workspace_widget_url %}
|
||||
<button
|
||||
type="button"
|
||||
@@ -354,6 +366,20 @@
|
||||
aria-label="Export conversation history"></textarea>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="{{ panel_id }}-availability"
|
||||
class="compose-availability-lane{% if not availability_enabled %} is-hidden{% endif %}"
|
||||
data-slices='{{ availability_slices_json|default:"[]"|escapejs }}'
|
||||
aria-label="Contact availability timeline">
|
||||
{% for row in availability_slices %}
|
||||
<span
|
||||
class="compose-availability-chip is-{{ row.state }}"
|
||||
title="{{ row.state|title }} via {{ row.service|upper }} ({{ row.confidence_end|floatformat:2 }})">
|
||||
{{ row.state|title }} · {{ row.service|upper }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="{{ panel_id }}-thread"
|
||||
class="compose-thread"
|
||||
@@ -553,6 +579,45 @@
|
||||
padding: 0.65rem;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.7), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
#{{ panel_id }} .compose-availability-lane {
|
||||
margin-top: 0.42rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.24rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-availability-lane.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
#{{ panel_id }} .compose-availability-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
padding: 0.06rem 0.45rem;
|
||||
font-size: 0.62rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #35465a;
|
||||
line-height: 1.2;
|
||||
}
|
||||
#{{ panel_id }} .compose-availability-chip.is-available {
|
||||
border-color: rgba(28, 144, 77, 0.4);
|
||||
background: rgba(228, 249, 237, 0.98);
|
||||
color: #1a6f3d;
|
||||
}
|
||||
#{{ panel_id }} .compose-availability-chip.is-fading {
|
||||
border-color: rgba(187, 119, 18, 0.4);
|
||||
background: rgba(255, 247, 230, 0.98);
|
||||
color: #8a5b13;
|
||||
}
|
||||
#{{ panel_id }} .compose-availability-chip.is-unavailable {
|
||||
border-color: rgba(194, 37, 37, 0.35);
|
||||
background: rgba(255, 236, 236, 0.98);
|
||||
color: #8f1e1e;
|
||||
}
|
||||
#{{ panel_id }} .compose-body,
|
||||
#{{ panel_id }} .compose-reaction-chip {
|
||||
font-family: inherit, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
|
||||
}
|
||||
#{{ panel_id }} .compose-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -572,6 +637,32 @@
|
||||
#{{ panel_id }} .compose-row.is-out {
|
||||
align-items: flex-end;
|
||||
}
|
||||
#{{ panel_id }} .compose-insights-capsule {
|
||||
display: inline-flex;
|
||||
border: 1px solid rgba(38, 77, 127, 0.28);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, rgba(240, 246, 255, 0.96), rgba(233, 242, 255, 0.9));
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.85) inset;
|
||||
}
|
||||
#{{ panel_id }} .compose-insights-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.24rem;
|
||||
padding: 0.32rem 0.62rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #1f4f82;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
}
|
||||
#{{ panel_id }} .compose-insights-link + .compose-insights-link {
|
||||
border-left: 1px solid rgba(38, 77, 127, 0.22);
|
||||
}
|
||||
#{{ panel_id }} .compose-insights-link:hover {
|
||||
background: rgba(219, 234, 255, 0.92);
|
||||
color: #153a63;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-group-middle,
|
||||
#{{ panel_id }} .compose-row.is-group-first {
|
||||
margin-bottom: 0.16rem;
|
||||
@@ -1759,6 +1850,7 @@
|
||||
const exportCopy = document.getElementById(panelId + "-export-copy");
|
||||
const exportClear = document.getElementById(panelId + "-export-clear");
|
||||
const exportBuffer = document.getElementById(panelId + "-export-buffer");
|
||||
const availabilityLane = document.getElementById(panelId + "-availability");
|
||||
const csrfToken = "{{ csrf_token }}";
|
||||
if (lightbox && lightbox.parentElement !== document.body) {
|
||||
document.body.appendChild(lightbox);
|
||||
@@ -3025,6 +3117,40 @@
|
||||
updateExportBuffer();
|
||||
};
|
||||
|
||||
const renderAvailabilitySlices = function (slices) {
|
||||
if (!availabilityLane) {
|
||||
return;
|
||||
}
|
||||
const rows = Array.isArray(slices) ? slices : [];
|
||||
availabilityLane.innerHTML = "";
|
||||
if (!rows.length) {
|
||||
availabilityLane.classList.add("is-hidden");
|
||||
return;
|
||||
}
|
||||
rows.forEach(function (item) {
|
||||
const chip = document.createElement("span");
|
||||
const state = String((item && item.state) || "unknown").toLowerCase();
|
||||
const service = String((item && item.service) || "").toUpperCase();
|
||||
const confidence = Number((item && item.confidence_end) || 0);
|
||||
const payload = (item && typeof item.payload === "object" && item.payload) ? item.payload : {};
|
||||
const inferredFrom = String(payload.inferred_from || payload.extended_by || "").trim();
|
||||
const lastSeenTs = Number(payload.last_seen_ts || 0);
|
||||
chip.className = "compose-availability-chip is-" + state;
|
||||
chip.textContent = (state.charAt(0).toUpperCase() + state.slice(1)) + (service ? (" · " + service) : "");
|
||||
const meta = [];
|
||||
meta.push("confidence " + confidence.toFixed(2));
|
||||
if (inferredFrom) {
|
||||
meta.push("source " + inferredFrom);
|
||||
}
|
||||
if (lastSeenTs > 0) {
|
||||
meta.push("last seen " + new Date(lastSeenTs).toLocaleString());
|
||||
}
|
||||
chip.title = chip.textContent + " (" + meta.join(", ") + ")";
|
||||
availabilityLane.appendChild(chip);
|
||||
});
|
||||
availabilityLane.classList.remove("is-hidden");
|
||||
};
|
||||
|
||||
const applyTyping = function (typingPayload) {
|
||||
if (!typingNode || !typingPayload || typeof typingPayload !== "object") {
|
||||
return;
|
||||
@@ -3059,6 +3185,9 @@
|
||||
if (payload.typing) {
|
||||
applyTyping(payload.typing);
|
||||
}
|
||||
if (payload.availability_slices) {
|
||||
renderAvailabilitySlices(payload.availability_slices);
|
||||
}
|
||||
if (payload.last_ts !== undefined && payload.last_ts !== null) {
|
||||
lastTs = Math.max(lastTs, toInt(payload.last_ts));
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
@@ -3096,6 +3225,9 @@
|
||||
if (payload.typing) {
|
||||
applyTyping(payload.typing);
|
||||
}
|
||||
if (payload.availability_slices) {
|
||||
renderAvailabilitySlices(payload.availability_slices);
|
||||
}
|
||||
if (payload.last_ts !== undefined && payload.last_ts !== null) {
|
||||
lastTs = Math.max(lastTs, toInt(payload.last_ts));
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
@@ -3136,8 +3268,14 @@
|
||||
}
|
||||
updateManualSafety();
|
||||
try {
|
||||
const initialTyping = JSON.parse("{{ typing_state_json|escapejs }}");
|
||||
applyTyping(initialTyping);
|
||||
const initialTyping = JSON.parse("{{ typing_state_json|escapejs }}");
|
||||
applyTyping(initialTyping);
|
||||
try {
|
||||
const initialSlices = JSON.parse(String((availabilityLane && availabilityLane.dataset.slices) || "[]"));
|
||||
renderAvailabilitySlices(initialSlices);
|
||||
} catch (err) {
|
||||
renderAvailabilitySlices([]);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore invalid initial typing state payload.
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<th>account</th>
|
||||
<th>name</th>
|
||||
<th>person</th>
|
||||
<th>availability</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
@@ -36,6 +37,13 @@
|
||||
{% if item.chat %}{{ item.chat.source_name }}{% else %}{{ item.name }}{% endif %}
|
||||
</td>
|
||||
<td>{{ item.person_name|default:"-" }}</td>
|
||||
<td>
|
||||
{% if item.availability_label %}
|
||||
<span class="tag is-light">{{ item.availability_label }}</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
{% if not item.is_group %}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<th>chat</th>
|
||||
<th>identifier</th>
|
||||
<th>person</th>
|
||||
<th>availability</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
@@ -25,6 +26,13 @@
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.person_name|default:"-" }}</td>
|
||||
<td>
|
||||
{% if item.availability_label %}
|
||||
<span class="tag is-light">{{ item.availability_label }}</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
{% if type == 'page' %}
|
||||
@@ -52,7 +60,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="has-text-grey">No WhatsApp chats discovered yet.</td>
|
||||
<td colspan="5" class="has-text-grey">No WhatsApp chats discovered yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user