Fix Signal messages and replies

This commit is contained in:
2026-03-03 15:51:58 +00:00
parent 56c620473f
commit d6bd56dace
31 changed files with 3317 additions and 668 deletions

View File

@@ -398,6 +398,9 @@
<a class="navbar-item" href="{% url 'command_routing' %}">
Command Routing
</a>
<a class="navbar-item" href="{% url 'tasks_settings' %}">
Task Settings
</a>
<a class="navbar-item" href="{% url 'translation_settings' %}">
Translation
</a>

View File

@@ -4,30 +4,44 @@
<section class="section">
<div class="container">
<h1 class="title is-4">Command Routing</h1>
<p class="subtitle is-6">Manage command profiles, channel bindings, business-plan outputs, and translation bridges.</p>
<p class="subtitle is-6">Configure commands, channel bindings, and per-command delivery in a predictable way.</p>
{% if scope_service and scope_identifier %}
<article class="notification is-info is-light">
Scoped to this chat only: <strong>{{ scope_service }}</strong> · <code>{{ scope_identifier }}</code>
</article>
{% endif %}
<article class="box">
<h2 class="title is-6">Create Command Profile</h2>
<p class="help">Create reusable command behavior. Example: <code>#bp#</code> reply command for business-plan extraction.</p>
<p class="help">Create reusable command behavior. <code>bp set</code> and <code>bp set range</code> are fixed bp subcommands and will appear automatically.</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">
<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>
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<div class="columns is-multiline">
<div class="column is-4">
<label class="label is-size-7" for="create_command_slug">Command</label>
<div class="select is-small is-fullwidth">
<select id="create_command_slug" name="command_slug">
{% for value, label in command_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="column">
<div class="column is-4">
<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">
<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 class="column is-4">
<label class="label is-size-7" for="create_trigger_token">Primary Trigger Token</label>
<input id="create_trigger_token" class="input is-small" name="trigger_token" value="#bp#" readonly>
</div>
</div>
<label class="label is-size-7" for="create_template_text">Template Text</label>
<label class="label is-size-7" for="create_template_text">BP Template (used only by <code>bp</code> in AI mode)</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>
@@ -37,53 +51,33 @@
<article class="box">
<h2 class="title is-6">{{ profile.name }} ({{ profile.slug }})</h2>
<div class="content is-size-7" style="margin-bottom: 0.6rem;">
<p><strong>Flag Definitions</strong></p>
<p><strong>Help</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>
<li><strong>Send plan to egress</strong>: posts generated plan to enabled egress bindings.</li>
<li><strong>Send status to source</strong>: posts a short confirmation message in the source chat.</li>
<li><strong>Send status to egress</strong>: posts a short confirmation to egress channels.</li>
<li><strong>Template support</strong>: only <code>bp</code> uses the template, and only in AI mode.</li>
</ul>
{% if profile.slug == "bp" %}
<p><strong>Supported Triggers (BP)</strong></p>
<ul>
<li><code>#bp#</code>: primary BP trigger (uses the standard BP extraction flow).</li>
<li><code>#bp set#</code>: deterministic no-AI set/update from reply/addendum text.</li>
<li><code>#bp set range#</code>: deterministic no-AI set/update from reply-anchor to trigger range.</li>
</ul>
{% endif %}
</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 }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<div class="columns is-multiline">
<div class="column is-3">
<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" 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 class="column is-3">
<label class="label is-size-7" for="trigger_token_{{ profile.id }}">Primary Trigger</label>
<input id="trigger_token_{{ profile.id }}" class="input is-small" name="trigger_token" value="{{ profile.trigger_token }}" readonly>
</div>
<div class="column is-2">
<label class="label is-size-7" for="visibility_mode_{{ profile.id }}">Visibility</label>
<div class="select is-small is-fullwidth">
<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">
<div class="column is-6">
<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>
@@ -93,26 +87,148 @@
</div>
</div>
<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>
<textarea id="template_text_{{ profile.id }}" class="textarea is-small" name="template_text" rows="4">{{ 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>
</form>
<div class="columns">
<div class="columns is-variable is-5">
<div class="column">
<h3 class="title is-7">Variant Policies</h3>
<p class="help">Delivery switches control where plan/status are posted. Egress bindings define destinations.</p>
<p class="help">Turn off <strong>Save Document</strong> to run/fanout without storing a business plan artifact.</p>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr>
<th>Variant</th>
<th>Trigger</th>
<th>Enabled</th>
<th>Generation</th>
<th>Save Document</th>
<th>Plan -> Egress</th>
<th>Status -> Source</th>
<th>Status -> Egress</th>
<th></th>
</tr>
</thead>
<tbody>
{% for variant in profile.variant_rows %}
<tr>
<form method="post" aria-label="Update variant policy {{ variant.variant_label }} for {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="variant_policy_update">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
<input type="hidden" name="variant_key" value="{{ variant.variant_key }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<td>
{{ variant.variant_label }}
{% if not variant.template_supported %}
<span class="tag is-warning is-light is-small has-text-dark">no template</span>
{% endif %}
</td>
<td><code>{{ variant.trigger_token }}</code></td>
<td><input type="checkbox" name="enabled" value="1" {% if variant.row.enabled %}checked{% endif %}></td>
<td>
<div class="select is-small">
<select name="generation_mode">
<option value="ai" {% if variant.row.generation_mode == 'ai' %}selected{% endif %}>AI</option>
<option value="verbatim" {% if variant.row.generation_mode == 'verbatim' %}selected{% endif %}>Verbatim</option>
</select>
</div>
</td>
<td><input type="checkbox" name="store_document" value="1" {% if variant.row.store_document %}checked{% endif %}></td>
<td><input type="checkbox" name="send_plan_to_egress" value="1" {% if variant.row.send_plan_to_egress %}checked{% endif %}></td>
<td><input type="checkbox" name="send_status_to_source" value="1" {% if variant.row.send_status_to_source %}checked{% endif %}></td>
<td><input type="checkbox" name="send_status_to_egress" value="1" {% if variant.row.send_status_to_egress %}checked{% endif %}></td>
<td><button class="button is-small is-link is-light" type="submit">Save</button></td>
</form>
</tr>
{% 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.
Recipients will get raw transcript-style output.
</p>
</td>
</tr>
{% endif %}
{% empty %}
<tr><td colspan="9">No variants configured.</td></tr>
{% endfor %}
</tbody>
</table>
<div class="is-flex is-justify-content-space-between is-align-items-center" style="gap: 0.75rem; margin-top: 0.75rem; flex-wrap: wrap;">
<form method="post" aria-label="Preview delivery for {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="variant_preview">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<button class="button is-small is-info is-light" type="submit">Dry Run Preview</button>
</form>
<form method="post" aria-label="Reset variant defaults for {{ profile.name }}">
{% csrf_token %}
<input type="hidden" name="action" value="variant_policy_reset_defaults">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<button class="button is-small is-warning is-light" type="submit">Reset Variant Defaults</button>
</form>
</div>
<h4 class="title is-7" style="margin-top: 0.95rem;">Effective Destinations</h4>
{% if profile.enabled_egress_bindings %}
<ul class="is-size-7">
{% for row in profile.enabled_egress_bindings %}
<li>{{ row.service }} · <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>
{% else %}
<article class="notification is-warning is-light is-size-7">No enabled egress destinations. Plan fanout will show sent:0.</article>
{% endif %}
{% if preview_profile_id and preview_profile_id == profile.id|stringformat:"s" %}
<article class="notification is-info is-light is-size-7" style="margin-top: 0.65rem;">
<p><strong>Dry Run Preview</strong></p>
<ul>
{% for variant in profile.variant_rows %}
<li>
{{ variant.variant_label }}: {% if variant.row.enabled %}enabled{% else %}disabled{% endif %}, mode={{ variant.row.generation_mode }},
save_document={{ variant.row.store_document }},
plan->egress={{ variant.row.send_plan_to_egress }},
status->source={{ variant.row.send_status_to_source }},
status->egress={{ variant.row.send_status_to_egress }}
</li>
{% endfor %}
</ul>
</article>
{% endif %}
</div>
<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>
<p class="help">Ingress accepts triggers. Egress receives plan/status fanout if enabled in variant policy.</p>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<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 %}
{% for binding in profile.visible_bindings %}
<tr>
<td>
{% if binding.direction == "ingress" %}Ingress (Accept Triggers)
{% elif binding.direction == "egress" %}Egress (Post Results)
{% elif binding.direction == "egress" %}Egress (Delivery Destination)
{% else %}Scratchpad Mirror
{% endif %}
</td>
@@ -123,6 +239,10 @@
{% csrf_token %}
<input type="hidden" name="action" value="binding_delete">
<input type="hidden" name="binding_id" value="{{ binding.id }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<button class="button is-danger is-light is-small" type="submit">Delete</button>
</form>
</td>
@@ -136,6 +256,10 @@
{% csrf_token %}
<input type="hidden" name="action" value="binding_create">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<div class="columns">
<div class="column">
<label class="label is-size-7" for="binding_direction_{{ profile.id }}">Direction</label>
@@ -144,7 +268,7 @@
{% for value in directions %}
<option value="{{ value }}">
{% if value == "ingress" %}Ingress (Accept Triggers)
{% elif value == "egress" %}Egress (Post Results)
{% elif value == "egress" %}Egress (Delivery Destination)
{% else %}Scratchpad Mirror
{% endif %}
</option>
@@ -155,16 +279,19 @@
<div class="column">
<label class="label is-size-7" for="binding_service_{{ profile.id }}">Service</label>
<div class="select is-small is-fullwidth">
<select id="binding_service_{{ profile.id }}" name="service">
<select id="binding_service_{{ profile.id }}" name="service" {% if scope_service %}disabled{% endif %}>
{% for value in channel_services %}
<option value="{{ value }}">{{ value }}</option>
<option value="{{ value }}" {% if value == scope_service %}selected{% endif %}>{{ value }}</option>
{% endfor %}
</select>
</div>
{% if scope_service %}
<input type="hidden" name="service" value="{{ scope_service }}">
{% endif %}
</div>
<div class="column">
<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">
<input id="binding_channel_identifier_{{ profile.id }}" class="input is-small" name="channel_identifier" placeholder="channel identifier" value="{{ scope_identifier }}" {% if scope_identifier %}readonly{% endif %}>
</div>
<div class="column is-narrow">
<button class="button is-link is-small" type="submit">Add</button>
@@ -172,13 +299,15 @@
</div>
</form>
</div>
</div>
<div class="columns">
<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>
<p class="help">Enable/disable each step and use the reorder capsule to change execution order.</p>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr><th scope="col">Type</th><th scope="col">Enabled</th><th scope="col">Order</th><th scope="col">Actions</th></tr>
<tr><th scope="col">Type</th><th scope="col">Enabled</th><th scope="col">Reorder</th><th scope="col">Actions</th></tr>
</thead>
<tbody>
{% for action_row in profile.actions.all %}
@@ -191,28 +320,41 @@
{% 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 }}">
<span class="command-order-capsule">
<form method="post" class="command-order-capsule-form" 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>
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<button class="button is-small is-light command-order-btn" type="submit" {% if forloop.first %}disabled{% endif %} aria-label="Move up"></button>
</form>
<form method="post" style="display:inline;" aria-label="Move action {{ action_row.action_type }} down for {{ profile.name }}">
<form method="post" class="command-order-capsule-form" 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>
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<button class="button is-small is-light command-order-btn" type="submit" {% if forloop.last %}disabled{% endif %} aria-label="Move down"></button>
</form>
</div>
</span>
</td>
<td>
<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 }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<label class="checkbox is-size-7"><input type="checkbox" name="enabled" value="1" {% if action_row.enabled %}checked{% endif %}> enabled</label>
<button class="button is-link is-light is-small" type="submit">Save</button>
</form>
@@ -230,6 +372,10 @@
{% csrf_token %}
<input type="hidden" name="action" value="profile_delete">
<input type="hidden" name="profile_id" value="{{ profile.id }}">
{% if scope_service and scope_identifier %}
<input type="hidden" name="service" value="{{ scope_service }}">
<input type="hidden" name="identifier" value="{{ scope_identifier }}">
{% endif %}
<button class="button is-danger is-light is-small" type="submit" aria-label="Delete profile {{ profile.name }}">Delete Profile</button>
</form>
</article>
@@ -261,4 +407,34 @@
</div>
</section>
<style>
.command-order-capsule {
display: inline-flex;
flex-direction: column;
border: 1px solid #dbdbdb;
border-radius: 6px;
overflow: hidden;
white-space: nowrap;
flex-wrap: nowrap;
width: 2.15rem;
min-width: 2.15rem;
}
.command-order-capsule-form {
margin: 0;
line-height: 1;
}
.command-order-btn {
border: 0;
border-radius: 0;
width: 100%;
min-width: 2.15rem;
height: 1.35rem;
padding: 0;
font-size: 0.72rem;
line-height: 1;
}
.command-order-capsule-form + .command-order-capsule-form .command-order-btn {
border-top: 1px solid #dbdbdb;
}
</style>
{% endblock %}

View File

@@ -3,402 +3,345 @@
<section class="section">
<div class="container tasks-settings-page">
<h1 class="title is-4">Task Settings</h1>
<p class="subtitle is-6">Configure task derivation, chat mapping, completion parsing, and external sync behavior.</p>
<p class="subtitle is-6">Project defaults flow into channel overrides. Use Quick Setup for normal operation; open Advanced Setup for full controls.</p>
<article class="box">
<h2 class="title is-6">Setting Definitions</h2>
<div class="notification is-light">
<div class="content is-size-7">
<p><strong>Projects</strong>: top-level containers for derived tasks. A single group can map to any project.</p>
<p><strong>Epics</strong>: optional sub-grouping inside a project. Use these for parallel workstreams in the same project.</p>
<p><strong>Group Mapping</strong>: binds a chat channel (service + channel identifier) to a project and optional epic. Task extraction only runs where mappings exist.</p>
<p><strong>Matching Hierarchy</strong>: channel mapping flags override project flags. Project flags are defaults; mapping flags are per-chat precision controls.</p>
<p><strong>False-Positive Controls</strong>: defaults are safe: <code>match_mode=strict</code>, <code>require_prefix=true</code>, and prefixes <code>task:</code>/<code>todo:</code>. Freeform matching is off by default.</p>
<p><strong>Task ID Announcements</strong>: when enabled, newly derived tasks post an in-chat confirmation containing the new task reference (for example <code>#17</code>). Default is off.</p>
<p><strong>Legacy Backfill</strong>: opening this page applies safe defaults to older project and mapping rows created before strict prefix-only matching.</p>
<p><strong>Completion Phrases</strong>: explicit trigger words used to detect completion markers like <code>done #12</code>, <code>completed #12</code>, <code>fixed #12</code>.</p>
<p><strong>Provider</strong>: external sync adapter toggle. In current setup, mock provider validates append-only sync flow and retry behavior.</p>
<p><strong>Sync Event Log</strong>: audit of provider sync attempts and outcomes. Retry replays the event without mutating immutable task source records.</p>
</div>
</article>
{% if prefill_service and prefill_identifier %}
<article class="box">
<h2 class="title is-6">Quick Setup For Current Chat</h2>
<p class="help">Prefilled from compose for <code>{{ prefill_service }}</code> · <code>{{ prefill_identifier }}</code>. Create/update project + epic + channel mapping in one step.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="quick_setup">
<input type="hidden" name="service" value="{{ prefill_service }}">
<input type="hidden" name="channel_identifier" value="{{ prefill_identifier }}">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<div class="columns tasks-settings-inline-columns">
<div class="column">
<label class="label is-size-7">Project</label>
<input class="input is-small" name="project_name" placeholder="Project name">
</div>
<div class="column">
<label class="label is-size-7">Epic (optional)</label>
<input class="input is-small" name="epic_name" placeholder="Epic name">
</div>
<div class="column">
<label class="label is-size-7">Match Mode</label>
<div class="select is-small is-fullwidth">
<select name="source_match_mode">
<option value="strict">strict</option>
<option value="balanced">balanced</option>
<option value="broad">broad</option>
</select>
</div>
</div>
<div class="column">
<label class="label is-size-7">Prefixes</label>
<input class="input is-small" name="source_allowed_prefixes" value="task:,todo:">
</div>
</div>
<label class="checkbox is-size-7"><input type="checkbox" name="source_require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_announce_task_id" value="1"> Announce Task ID</label>
<button class="button is-small is-link" type="submit" style="margin-left: 0.75rem;">Apply Quick Setup</button>
</form>
</article>
{% endif %}
<div class="columns is-multiline tasks-settings-grid">
<div class="column is-6">
<article class="box">
<h2 class="title is-6">Projects</h2>
<p class="help">Create project scopes used by group mappings and derived tasks.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="project_create">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<div class="field">
<label class="label is-size-7">Name</label>
<input class="input is-small" name="name" placeholder="Project name">
</div>
<div class="field">
<label class="label is-size-7">Match Mode</label>
<div class="select is-small is-fullwidth">
<select name="match_mode">
<option value="strict">strict</option>
<option value="balanced">balanced</option>
<option value="broad">broad</option>
</select>
</div>
</div>
<div class="field">
<label class="label is-size-7">Allowed Prefixes (comma-separated)</label>
<input class="input is-small" name="allowed_prefixes" value="task:,todo:">
</div>
<label class="checkbox is-size-7"><input type="checkbox" name="require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="announce_task_id" value="1"> Announce Task ID</label>
<button class="button is-small is-link" type="submit">Add Project</button>
</form>
<ul class="tasks-settings-list">
{% for row in projects %}
<li>
{{ row.name }}
<span class="has-text-grey">
mode={{ row.settings_effective.match_mode }},
prefixes={{ row.allowed_prefixes_csv }},
require_prefix={{ row.settings_effective.require_prefix }},
announce_id={{ row.settings_effective.announce_task_id }}
</span>
</li>
{% empty %}
<li>No projects.</li>
{% endfor %}
</ul>
</article>
</div>
<div class="column is-6">
<article class="box">
<h2 class="title is-6">Epics</h2>
<p class="help">Create project-local epics to refine routing and reporting.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="epic_create">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<div class="field">
<label class="label is-size-7">Project</label>
<div class="select is-small is-fullwidth">
<select name="project_id">
{% for p in projects %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}
</select>
</div>
</div>
<div class="field">
<label class="label is-size-7">Name</label>
<input class="input is-small" name="name" placeholder="Epic name">
</div>
<button class="button is-small is-link" type="submit">Add Epic</button>
</form>
</article>
</div>
<div class="column is-12">
<article class="box">
<h2 class="title is-6">Group Mapping (Chat -> Project/Epic)</h2>
<p class="help">Each mapped group becomes eligible for derived task extraction and completion tracking.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="source_create">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<div class="columns tasks-settings-inline-columns">
<div class="column">
<label class="label is-size-7">Service</label>
<div class="select is-small is-fullwidth">
<select name="service">
<option {% if prefill_service == 'web' %}selected{% endif %}>web</option>
<option {% if prefill_service == 'xmpp' %}selected{% endif %}>xmpp</option>
<option {% if prefill_service == 'signal' %}selected{% endif %}>signal</option>
<option {% if prefill_service == 'whatsapp' %}selected{% endif %}>whatsapp</option>
</select>
</div>
</div>
<div class="column">
<label class="label is-size-7">Channel Identifier</label>
<input class="input is-small" name="channel_identifier" placeholder="service-native group/channel id" value="{{ prefill_identifier }}">
</div>
<div class="column">
<label class="label is-size-7">Project</label>
<div class="select is-small is-fullwidth">
<select name="project_id">
{% for p in projects %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}
</select>
</div>
</div>
<div class="column">
<label class="label is-size-7">Epic (optional)</label>
<div class="select is-small is-fullwidth">
<select name="epic_id">
<option value="">-</option>
{% for e in epics %}<option value="{{ e.id }}">{{ e.project.name }} / {{ e.name }}</option>{% endfor %}
</select>
</div>
</div>
<div class="column is-narrow">
<button class="button is-small is-link" type="submit" style="margin-top: 1.8rem;">Add</button>
</div>
</div>
<div class="columns tasks-settings-inline-columns">
<div class="column">
<label class="label is-size-7">Match Mode</label>
<div class="select is-small is-fullwidth">
<select name="source_match_mode">
<option value="strict">strict</option>
<option value="balanced">balanced</option>
<option value="broad">broad</option>
</select>
</div>
</div>
<div class="column">
<label class="label is-size-7">Allowed Prefixes</label>
<input class="input is-small" name="source_allowed_prefixes" value="task:,todo:">
</div>
<div class="column">
<label class="label is-size-7">Min Chars</label>
<input class="input is-small" name="source_min_chars" value="3">
</div>
</div>
<label class="checkbox is-size-7"><input type="checkbox" name="source_require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_ai_title_enabled" value="1" checked> AI Title Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_announce_task_id" value="1"> Announce Task ID</label>
</form>
<table class="table is-fullwidth is-size-7">
<thead><tr><th>Chat</th><th>Project</th><th>Epic</th><th>Match</th><th>Announce</th></tr></thead>
<tbody>
{% for row in sources %}
<tr>
<td>{{ row.service }} · {{ row.channel_identifier }}</td>
<td>{{ row.project.name }}</td>
<td>{{ row.epic.name }}</td>
<td>{{ row.settings_effective.match_mode }}{% if row.settings_effective.require_prefix %} +prefix{% endif %}</td>
<td>{{ row.settings_effective.announce_task_id }}</td>
</tr>
{% empty %}
<tr><td colspan="5">No mappings.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
</div>
<div class="column is-6">
<article class="box">
<h2 class="title is-6">Project Matching Flags</h2>
<p class="help">Project defaults apply to all mapped chats unless channel-level override changes them.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="project_flags_update">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<div class="field">
<label class="label is-size-7">Project</label>
<div class="select is-small is-fullwidth">
<select name="project_id">
{% for p in projects %}
<option value="{{ p.id }}">{{ p.name }} · mode={{ p.settings_effective.match_mode }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field">
<label class="label is-size-7">Match Mode</label>
<div class="select is-small is-fullwidth">
<select name="match_mode">
<option value="strict">strict</option>
<option value="balanced">balanced</option>
<option value="broad">broad</option>
</select>
</div>
</div>
<div class="field">
<label class="label is-size-7">Allowed Prefixes</label>
<input class="input is-small" name="allowed_prefixes" value="task:,todo:">
</div>
<div class="field">
<label class="label is-size-7">Min Chars</label>
<input class="input is-small" name="min_chars" value="3">
</div>
<label class="checkbox is-size-7"><input type="checkbox" name="require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="announce_task_id" value="1"> Announce Task ID</label>
<button class="button is-small is-link is-light" type="submit" style="margin-left: 0.75rem;">Save Project Flags</button>
</form>
</article>
<article class="box">
<h2 class="title is-6">Channel Override Flags</h2>
<p class="help">These flags override project defaults for one mapped chat only.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="source_flags_update">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<div class="field">
<label class="label is-size-7">Mapped Channel</label>
<div class="select is-small is-fullwidth">
<select name="source_id">
{% for s in sources %}
<option value="{{ s.id }}">{{ s.service }} · {{ s.channel_identifier }} · {{ s.project.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field">
<label class="label is-size-7">Match Mode</label>
<div class="select is-small is-fullwidth">
<select name="source_match_mode">
<option value="strict">strict</option>
<option value="balanced">balanced</option>
<option value="broad">broad</option>
</select>
</div>
</div>
<div class="field">
<label class="label is-size-7">Allowed Prefixes</label>
<input class="input is-small" name="source_allowed_prefixes" value="task:,todo:">
</div>
<div class="field">
<label class="label is-size-7">Min Chars</label>
<input class="input is-small" name="source_min_chars" value="3">
</div>
<label class="checkbox is-size-7"><input type="checkbox" name="source_require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_ai_title_enabled" value="1" checked> AI Title Enabled</label>
<label class="checkbox is-size-7" style="margin-left: 0.75rem;"><input type="checkbox" name="source_announce_task_id" value="1"> Announce Task ID</label>
<button class="button is-small is-link is-light" type="submit" style="margin-left: 0.75rem;">Save Channel Flags</button>
</form>
</article>
</div>
<div class="column is-6">
<article class="box">
<h2 class="title is-6">Completion Phrases</h2>
<p class="help">Add parser phrases for completion statements followed by a task reference, e.g. <code>done #12</code>.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="pattern_create">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<div class="field">
<label class="label is-size-7">Phrase</label>
<input class="input is-small" name="phrase" placeholder="done">
</div>
<button class="button is-small is-link" type="submit">Add Phrase</button>
</form>
<ul class="tasks-settings-list">
{% for row in patterns %}<li>{{ row.phrase }}</li>{% empty %}<li>No phrases.</li>{% endfor %}
</ul>
</article>
</div>
<div class="column is-6">
<article class="box">
<h2 class="title is-6">Provider</h2>
<p class="help">Enable/disable external sync adapter and review recent provider event outcomes.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="provider_update">
<input type="hidden" name="provider" value="mock">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<label class="checkbox"><input type="checkbox" name="enabled" value="1" {% if provider_configs and provider_configs.0.enabled %}checked{% endif %}> Enable mock provider</label>
<button class="button is-small is-link is-light" type="submit">Save</button>
</form>
<table class="table is-fullwidth is-size-7 tasks-settings-table">
<thead><tr><th>Updated</th><th>Provider</th><th>Status</th><th></th></tr></thead>
<tbody>
{% for row in sync_events %}
<tr>
<td>{{ row.updated_at }}</td>
<td>{{ row.provider }}</td>
<td>{{ row.status }}</td>
<td>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="sync_retry">
<input type="hidden" name="event_id" value="{{ row.id }}">
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
<button class="button is-small is-light" type="submit">Retry</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="4">No sync events.</td></tr>
{% endfor %}
</tbody>
</table>
</article>
<p><strong>How Matching Works</strong></p>
<p><strong>Safe default behavior</strong>: strict matching, required prefixes, completion parsing enabled, and task-id announcements disabled.</p>
<p><strong>Hierarchy</strong>: <strong>Project</strong> flags are defaults. A mapped <strong>channel</strong> can override those defaults without changing project-wide behavior.</p>
<p><strong>Matching modes</strong>: <code>strict</code> (prefix only), <code>balanced</code> (prefix + limited hints), <code>broad</code> (more permissive, higher false-positive risk).</p>
</div>
</div>
<section class="block box">
<h2 class="title is-6">Quick Setup</h2>
<p class="help">Creates or updates project + optional epic + channel mapping in one submission.</p>
<p class="help">After setup, view tasks in <a href="{% url 'tasks_hub' %}">Tasks Hub</a>{% if prefill_service and prefill_identifier %} or <a href="{% url 'tasks_group' service=prefill_service identifier=prefill_identifier %}">this group task view</a>{% endif %}.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="quick_setup">
<div class="columns tasks-settings-inline-columns">
<div class="column">
<label class="label is-size-7">Service</label>
<div class="select is-small is-fullwidth">
<select name="service">
<option value="web" {% if prefill_service == 'web' %}selected{% endif %}>web</option>
<option value="xmpp" {% if prefill_service == 'xmpp' %}selected{% endif %}>xmpp</option>
<option value="signal" {% if prefill_service == 'signal' %}selected{% endif %}>signal</option>
<option value="whatsapp" {% if prefill_service == 'whatsapp' or not prefill_service %}selected{% endif %}>whatsapp</option>
</select>
</div>
<p class="help">Platform to watch for task extraction.</p>
</div>
<div class="column">
<label class="label is-size-7">Channel Identifier</label>
<input class="input is-small" name="channel_identifier" value="{{ prefill_identifier }}" placeholder="120...@g.us">
<p class="help">Exact chat/group id where messages are monitored.</p>
</div>
<div class="column">
<label class="label is-size-7">Project</label>
<input class="input is-small" name="project_name" placeholder="Project name">
<p class="help">Top-level container for derived tasks.</p>
</div>
<div class="column">
<label class="label is-size-7">Epic (optional)</label>
<input class="input is-small" name="epic_name" placeholder="Epic name">
<p class="help">Optional sub-container within a project.</p>
</div>
</div>
<div class="columns tasks-settings-inline-columns">
<div class="column">
<label class="label is-size-7">Match Mode</label>
<div class="select is-small is-fullwidth">
<select name="source_match_mode">
<option value="strict" selected>strict</option>
<option value="balanced">balanced</option>
<option value="broad">broad</option>
</select>
</div>
<p class="help">strict = safest, balanced = moderate, broad = permissive.</p>
</div>
<div class="column">
<label class="label is-size-7">Allowed Prefixes</label>
<input id="quick-prefixes" class="input is-small" name="source_allowed_prefixes" value="task:,todo:">
<p class="help">Click to add:
<button type="button" class="button is-small is-light prefix-chip" data-target="quick-prefixes" data-prefix="task:">task:</button>
<button type="button" class="button is-small is-light prefix-chip" data-target="quick-prefixes" data-prefix="todo:">todo:</button>
<button type="button" class="button is-small is-light prefix-chip" data-target="quick-prefixes" data-prefix="action:">action:</button>
</p>
</div>
<div class="column is-narrow">
<label class="label is-size-7">Min Chars</label>
<input class="input is-small" name="source_min_chars" value="3">
<p class="help">Minimum length after prefix.</p>
</div>
</div>
<div class="field is-grouped is-grouped-multiline">
<label class="checkbox is-size-7"><input type="checkbox" name="source_require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_ai_title_enabled" value="1" checked> AI Title Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_announce_task_id" value="1"> Announce Task ID</label>
</div>
<p class="help">
<strong>Require Prefix</strong>: only prefixed messages can create tasks.
<strong>Derivation Enabled</strong>: master on/off for extraction.
<strong>Completion Enabled</strong>: parse completion phrases like <code>done #12</code>.
<strong>AI Title Enabled</strong>: normalize task titles using AI.
<strong>Announce Task ID</strong>: send bot confirmation on creation.
</p>
<button class="button is-link is-small mt-3" type="submit">Apply Quick Setup</button>
</form>
</section>
<details class="tasks-advanced box" {% if not prefill_identifier %}open{% endif %}>
<summary class="title is-6">Advanced Setup</summary>
<p class="help">Manual controls for creating hierarchy entities, mapping channels, and overriding behavior.</p>
<div class="columns is-multiline tasks-settings-grid">
<div class="column is-6">
<section class="tasks-panel">
<h3 class="title is-7">Projects</h3>
<p class="help">Create projects and review their effective defaults.</p>
<form method="post" class="block">
{% csrf_token %}
<input type="hidden" name="action" value="project_create">
<div class="field has-addons">
<div class="control is-expanded"><input class="input is-small" name="name" placeholder="Project name"></div>
<div class="control"><button class="button is-small is-link" type="submit">Add Project</button></div>
</div>
<p class="help">Project names should describe a long-running stream of work.</p>
</form>
<table class="table is-fullwidth is-striped is-size-7">
<thead><tr><th>Project</th><th>Defaults</th></tr></thead>
<tbody>
{% for row in projects %}
<tr>
<td>{{ row.name }}</td>
<td>mode={{ row.settings_effective.match_mode }}, prefixes={{ row.allowed_prefixes_csv }}, announce_id={{ row.settings_effective.announce_task_id }}</td>
</tr>
{% empty %}
<tr><td colspan="2">No projects.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
</div>
<div class="column is-6">
<section class="tasks-panel">
<h3 class="title is-7">Epics</h3>
<p class="help">Epics are optional subdivisions under a project.</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="epic_create">
<div class="field">
<label class="label is-size-7">Project + Epic</label>
</div>
<div class="field has-addons">
<div class="control">
<div class="select is-small">
<select name="project_id">{% for p in projects %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}</select>
</div>
</div>
<div class="control is-expanded"><input class="input is-small" name="name" placeholder="Epic name"></div>
<div class="control"><button class="button is-small is-link" type="submit">Add Epic</button></div>
</div>
<p class="help">Choose the parent project first, then add the epic name.</p>
</form>
</section>
</div>
<div class="column is-12">
<section class="tasks-panel">
<h3 class="title is-7">Group Mapping</h3>
<p class="help">Map a channel to a project/epic. Channel flags can later override project defaults.</p>
<form method="post" class="block">
{% csrf_token %}
<input type="hidden" name="action" value="source_create">
<div class="columns tasks-settings-inline-columns">
<div class="column">
<div class="field">
<label class="label is-size-7">Service</label>
<div class="control">
<div class="select is-small is-fullwidth"><select name="service"><option>web</option><option>xmpp</option><option>signal</option><option>whatsapp</option></select></div>
</div>
<p class="help">Service/platform for this mapping.</p>
</div>
</div>
<div class="column">
<div class="field">
<label class="label is-size-7">Channel Identifier</label>
<div class="control">
<input class="input is-small" name="channel_identifier" value="{{ prefill_identifier }}" placeholder="channel">
</div>
<p class="help">Exact identifier for the chat/group.</p>
</div>
</div>
<div class="column">
<div class="field">
<label class="label is-size-7">Project</label>
<div class="control">
<div class="select is-small is-fullwidth"><select name="project_id">{% for p in projects %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}</select></div>
</div>
<p class="help">Project receiving derived tasks.</p>
</div>
</div>
<div class="column">
<div class="field">
<label class="label is-size-7">Epic (optional)</label>
<div class="control">
<div class="select is-small is-fullwidth"><select name="epic_id"><option value="">-</option>{% for e in epics %}<option value="{{ e.id }}">{{ e.project.name }} / {{ e.name }}</option>{% endfor %}</select></div>
</div>
<p class="help">Optional epic within that project.</p>
</div>
</div>
<div class="column is-narrow"><button class="button is-small is-link" type="submit">Add</button></div>
</div>
</form>
<table class="table is-fullwidth is-striped is-size-7">
<thead><tr><th>Chat</th><th>Project</th><th>Epic</th><th>Match</th><th>Announce</th><th></th></tr></thead>
<tbody>
{% for row in sources %}
<tr>
<td>{{ row.service }} · {{ row.channel_identifier }}</td>
<td>{{ row.project.name }}</td>
<td>{{ row.epic.name }}</td>
<td>{{ row.settings_effective.match_mode }}{% if row.settings_effective.require_prefix %} +prefix{% endif %}</td>
<td>{{ row.settings_effective.announce_task_id }}</td>
<td>
<form method="post" aria-label="Delete mapping {{ row.service }} {{ row.channel_identifier }}">
{% csrf_token %}
<input type="hidden" name="action" value="source_delete">
<input type="hidden" name="source_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 mappings.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
</div>
<div class="column is-6">
<section class="tasks-panel">
<h3 class="title is-7">Project Defaults (All Mapped Chats)</h3>
<p class="help">Set baseline extraction behavior for a project. Every mapped chat inherits this unless overridden below.</p>
<form method="post" class="tasks-flag-form">
{% csrf_token %}
<input type="hidden" name="action" value="project_flags_update">
<div class="field"><label class="label is-size-7">Project</label><div class="select is-small is-fullwidth"><select name="project_id">{% for p in projects %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}</select></div></div>
<div class="field"><label class="label is-size-7">Match Mode</label><div class="select is-small is-fullwidth"><select name="match_mode"><option value="strict" selected>strict</option><option value="balanced">balanced</option><option value="broad">broad</option></select></div></div>
<div class="field"><label class="label is-size-7">Allowed Prefixes</label><input id="proj-prefixes" class="input is-small" name="allowed_prefixes" value="task:,todo:"></div>
<div class="field"><label class="label is-size-7">Min Chars</label><input class="input is-small" name="min_chars" value="3"></div>
<div class="field is-grouped is-grouped-multiline">
<label class="checkbox is-size-7"><input type="checkbox" name="require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7"><input type="checkbox" name="derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="announce_task_id" value="1"> Announce Task ID</label>
</div>
<p class="help">
<button type="button" class="button is-small is-light prefix-chip" data-target="proj-prefixes" data-prefix="task:">task:</button>
<button type="button" class="button is-small is-light prefix-chip" data-target="proj-prefixes" data-prefix="todo:">todo:</button>
<button type="button" class="button is-small is-light prefix-chip" data-target="proj-prefixes" data-prefix="action:">action:</button>
</p>
<button class="button is-small is-link is-light" type="submit">Save Project Flags</button>
</form>
<p class="help">
<strong>Require Prefix</strong>: allow task creation only with configured prefixes.
<strong>Derivation Enabled</strong>: turn extraction on/off for this project.
<strong>Completion Enabled</strong>: enable completion phrase parser.
<strong>Announce Task ID</strong>: emit confirmation messages on task creation.
</p>
</section>
</div>
<div class="column is-6">
<section class="tasks-panel">
<h3 class="title is-7">Channel Override Flags</h3>
<p class="help">Channel-level override. Use only where this chat should behave differently from the project default.</p>
<form method="post" class="tasks-flag-form">
{% csrf_token %}
<input type="hidden" name="action" value="source_flags_update">
<div class="field"><label class="label is-size-7">Mapped Channel</label><div class="select is-small is-fullwidth"><select name="source_id">{% for s in sources %}<option value="{{ s.id }}">{{ s.service }} · {{ s.channel_identifier }} · {{ s.project.name }}</option>{% endfor %}</select></div></div>
<div class="field"><label class="label is-size-7">Match Mode</label><div class="select is-small is-fullwidth"><select name="source_match_mode"><option value="strict" selected>strict</option><option value="balanced">balanced</option><option value="broad">broad</option></select></div></div>
<div class="field"><label class="label is-size-7">Allowed Prefixes</label><input id="source-prefixes" class="input is-small" name="source_allowed_prefixes" value="task:,todo:"></div>
<div class="field"><label class="label is-size-7">Min Chars</label><input class="input is-small" name="source_min_chars" value="3"></div>
<div class="field is-grouped is-grouped-multiline">
<label class="checkbox is-size-7"><input type="checkbox" name="source_require_prefix" value="1" checked> Require Prefix</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_derive_enabled" value="1" checked> Derivation Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_completion_enabled" value="1" checked> Completion Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_ai_title_enabled" value="1" checked> AI Title Enabled</label>
<label class="checkbox is-size-7"><input type="checkbox" name="source_announce_task_id" value="1"> Announce Task ID</label>
</div>
<p class="help">
<button type="button" class="button is-small is-light prefix-chip" data-target="source-prefixes" data-prefix="task:">task:</button>
<button type="button" class="button is-small is-light prefix-chip" data-target="source-prefixes" data-prefix="todo:">todo:</button>
<button type="button" class="button is-small is-light prefix-chip" data-target="source-prefixes" data-prefix="action:">action:</button>
</p>
<button class="button is-small is-link is-light" type="submit">Save Channel Flags</button>
</form>
<p class="help">
<strong>Require Prefix</strong>: enforce prefixes in this channel.
<strong>Derivation Enabled</strong>: extraction on/off for this channel only.
<strong>Completion Enabled</strong>: completion phrase parser in this channel.
<strong>AI Title Enabled</strong>: AI title normalization in this channel.
<strong>Announce Task ID</strong>: confirmation message in this channel.
</p>
</section>
</div>
<div class="column is-6">
<section class="tasks-panel">
<h3 class="title is-7">Completion Phrases</h3>
<p class="help">Add parser phrases for completion statements followed by a task reference, e.g. <code>done #12</code>.</p>
<form method="post" class="block">
{% csrf_token %}
<input type="hidden" name="action" value="pattern_create">
<div class="field has-addons">
<div class="control is-expanded"><input class="input is-small" name="phrase" placeholder="done"></div>
<div class="control"><button class="button is-small is-link" type="submit">Add Phrase</button></div>
</div>
</form>
<ul class="tasks-settings-list">{% for row in patterns %}<li>{{ row.phrase }}</li>{% empty %}<li>No phrases.</li>{% endfor %}</ul>
</section>
</div>
<div class="column is-6">
<section class="tasks-panel">
<h3 class="title is-7">Provider</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>
<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>
<p class="help">Browse all derived tasks in <a href="{% url 'tasks_hub' %}">Tasks Hub</a>.</p>
</section>
</div>
</div>
</details>
</div>
</section>
<style>
.tasks-settings-page .tasks-settings-grid .column > .box {
height: 100%;
display: flex;
flex-direction: column;
}
.tasks-settings-page .tasks-settings-inline-columns {
margin-left: 0;
margin-right: 0;
margin-top: 0;
margin-bottom: 0.4rem;
}
.tasks-settings-page .tasks-settings-inline-columns > .column {
padding-left: 0;
@@ -407,11 +350,79 @@
.tasks-settings-page .tasks-settings-inline-columns > .column:last-child {
padding-right: 0;
}
.tasks-settings-page .tasks-settings-inline-columns .help {
display: block;
margin-top: 0.3rem;
line-height: 1.25;
}
.tasks-settings-page .tasks-settings-inline-columns .field {
margin-bottom: 0;
}
.tasks-settings-page .tasks-settings-list {
margin-top: 0.75rem;
}
.tasks-settings-page .tasks-settings-table {
margin-top: 0.75rem;
.tasks-settings-page .prefix-chip {
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
.tasks-settings-page .tasks-advanced {
margin-top: 1.25rem;
padding: 1rem;
}
.tasks-settings-page .tasks-advanced > summary {
cursor: pointer;
list-style: none;
margin-bottom: 0.75rem;
}
.tasks-settings-page .tasks-advanced > summary::-webkit-details-marker {
display: none;
}
.tasks-settings-page .tasks-panel {
height: 100%;
margin-bottom: 0;
border-top: 1px solid #ececec;
padding-top: 0.75rem;
padding-right: 0.1rem;
overflow-x: hidden;
}
.tasks-settings-page .tasks-settings-grid > .column {
display: flex;
}
.tasks-settings-page .tasks-settings-grid > .column > .tasks-panel {
width: 100%;
}
.tasks-settings-page .tasks-settings-grid .table {
table-layout: fixed;
}
</style>
<script>
(function () {
const chips = Array.from(document.querySelectorAll('.tasks-settings-page .prefix-chip'));
if (!chips.length) {
return;
}
const addPrefix = function (targetId, prefix) {
const input = document.getElementById(targetId);
if (!input) {
return;
}
const current = String(input.value || '').split(',').map(function (v) { return String(v || '').trim(); }).filter(Boolean);
const value = String(prefix || '').trim();
if (!value) {
return;
}
if (!current.includes(value)) {
current.push(value);
}
input.value = current.join(',');
input.dispatchEvent(new Event('change', { bubbles: true }));
};
chips.forEach(function (btn) {
btn.addEventListener('click', function () {
addPrefix(String(btn.dataset.target || ''), String(btn.dataset.prefix || ''));
});
});
})();
</script>
{% endblock %}

View File

@@ -81,20 +81,33 @@
<span>Commands</span>
</button>
<div class="compose-command-menu-panel is-hidden">
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.45rem;">
This chat currently has {{ bp_binding_summary.ingress_count }} ingress and {{ bp_binding_summary.egress_count }} egress bindings for bp.
</p>
{% for option in command_options %}
<label class="compose-command-option">
<input
type="checkbox"
class="compose-command-toggle"
data-command-slug="{{ option.slug }}"
data-command-slug="{{ option.toggle_slug|default:option.slug }}"
data-command-row-slug="{{ option.slug }}"
{% if option.enabled_here %}checked{% endif %}>
<span class="compose-command-option-title">{{ option.name }}</span>
{% if option.mode_label %}
<span class="tag is-light is-info is-rounded compose-command-option-badge">{{ option.mode_label }}</span>
{% endif %}
{% if option.enabled_label %}
<span class="tag is-light is-rounded compose-command-option-badge">{{ option.enabled_label }}</span>
{% endif %}
{% if option.trigger_token %}
<span class="compose-command-option-token">{{ option.trigger_token }}</span>
{% endif %}
</label>
{% endfor %}
<a class="compose-command-settings-link" href="{% url 'command_routing' %}">Open command routing</a>
<p class="help" style="margin-top: 0.5rem;">
Enabling a command in this menu enables ingress and egress for this chat identifier.
</p>
<a class="compose-command-settings-link" href="{% if command_routing_scoped_url %}{{ command_routing_scoped_url }}{% else %}{% url 'command_routing' %}{% endif %}">Open command routing</a>
</div>
</div>
<button type="button" class="button is-light is-rounded compose-export-toggle" aria-expanded="false">
@@ -153,6 +166,14 @@
</div>
</div>
{% if signal_ingest_warning %}
<article class="message is-warning is-light" style="margin-top: 0.75rem; margin-bottom: 0.75rem;">
<div class="message-body is-size-7">
{{ signal_ingest_warning }}
</div>
</article>
{% endif %}
<div id="{{ panel_id }}-status" class="compose-status">
{% include "partials/compose-send-status.html" %}
</div>
@@ -903,6 +924,12 @@
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.68rem;
}
#{{ panel_id }} .compose-command-option-badge {
font-size: 0.62rem;
padding: 0 0.42rem;
height: 1.15rem;
line-height: 1.15rem;
}
#{{ panel_id }} .compose-command-settings-link {
margin-top: 0.2rem;
font-size: 0.72rem;
@@ -2154,6 +2181,13 @@
return;
}
const shouldEnable = !!checkbox.checked;
if (shouldEnable) {
const confirmText = "Enable this command for this chat and route output to configured egress channels?";
if (!window.confirm(confirmText)) {
checkbox.checked = false;
return;
}
}
checkbox.disabled = true;
try {
const params = queryParams({
@@ -2173,6 +2207,9 @@
String(result.message || (slug + (shouldEnable ? " enabled." : " disabled."))),
"success"
);
menu.querySelectorAll('.compose-command-toggle[data-command-slug=\"' + slug + '\"]').forEach(function (peer) {
peer.checked = shouldEnable;
});
} catch (err) {
checkbox.checked = !shouldEnable;
setStatus("Failed to update command binding.", "danger");

View File

@@ -22,22 +22,24 @@
<td>
<div class="buttons">
<button
type="button"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
{% if account_unlink_url_name %}
hx-delete="{% url account_unlink_url_name type=type account=item %}"
hx-post="{% url account_unlink_url_name type=type account=item %}"
hx-trigger="click"
hx-target="#{{ context_object_name|slugify }}-panel"
hx-swap="outerHTML"
{% endif %}
{% if account_unlink_url_name %}
hx-confirm="Are you sure you wish to unlink {{ item }}?"
hx-confirm="Unlink {{ item }} from this bridge client so you can relink by scanning a new QR code?"
{% endif %}
class="button"
class="button is-light"
{% if not account_unlink_url_name %}disabled{% endif %}>
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
<i class="fa-solid fa-rotate"></i>
</span>
<span>{{ account_unlink_label|default:"Unlink" }}</span>
</span>
</button>
{% if show_contact_actions %}
@@ -97,9 +99,15 @@
{% endfor %}
</table>
{% if account_unlink_label == "Relink" %}
<p class="help" style="margin-bottom: 0.6rem;">
Relink flow: click <strong>Relink</strong> on the current account, then use
<strong>Add account</strong> below to generate and scan a fresh QR code.
</p>
{% endif %}
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url account_add_url_name type=type %}"
hx-post="{% url account_add_url_name type=account_add_type|default:type %}"
hx-target="{% if account_add_target %}{{ account_add_target }}{% else %}#widgets-here{% endif %}"
hx-swap="{% if account_add_swap %}{{ account_add_swap }}{% else %}innerHTML{% endif %}">
{% csrf_token %}

View File

@@ -1,6 +1,4 @@
{% load cache %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_signal_chats request.user.id object_list type %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
@@ -146,4 +144,3 @@
{% endfor %}
</table>
{% endcache %}