Files
GIA/core/templates/pages/system-settings.html

323 lines
15 KiB
HTML

{% extends "base.html" %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title is-4">System Maintenance</h1>
<p class="subtitle is-6">Superuser tools for data cleanup (current user scope).</p>
{% if notice_message %}
<article class="notification is-{{ notice_level|default:'info' }} is-light">
{{ notice_message }}
</article>
{% endif %}
<div class="columns is-multiline">
<div class="column is-12">
<article class="box">
<h2 class="title is-6">Purge Non-OSINT Data</h2>
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.65rem;">
Removes message/workspace/AI/mitigation runtime rows but keeps OSINT setup objects.
</p>
<div class="tags" style="margin-bottom: 0.75rem;">
<span class="tag is-light">Chat Sessions: {{ counts.chat_sessions }}</span>
<span class="tag is-light">Messages: {{ counts.messages }}</span>
<span class="tag is-light">Queued: {{ counts.queued_messages }}</span>
<span class="tag is-light">Events: {{ counts.message_events }}</span>
<span class="tag is-light">Workspace: {{ counts.workspace_conversations }}</span>
<span class="tag is-light">Snapshots: {{ counts.workspace_snapshots }}</span>
<span class="tag is-light">AI Requests: {{ counts.ai_requests }}</span>
<span class="tag is-light">AI Results: {{ counts.ai_results }}</span>
<span class="tag is-light">Memory: {{ counts.memory_items }}</span>
<span class="tag is-light">Mitigation Plans: {{ counts.mitigation_plans }}</span>
</div>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="purge_non_osint">
<button type="submit" class="button is-danger is-light">Purge Non-OSINT</button>
</form>
</article>
</div>
<div class="column is-12">
<article class="box">
<h2 class="title is-6">Diagnostics Quick Checks</h2>
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.65rem;">
Run projection shadow, event ledger smoke, and trace diagnostics from one place.
</p>
<div class="columns is-multiline">
<div class="column is-12-tablet is-3-desktop">
<form id="projection-shadow-form">
<label class="label is-size-7">Projection Shadow</label>
<div class="field">
<div class="control">
<input class="input is-small" name="session_lookup" placeholder="name | id | service | identifier" list="diagnostics-session-options" required>
<input type="hidden" name="session_id">
</div>
<p class="help is-size-7" title="Use this when a thread looks wrong in Compose and you need to compare DB messages vs event projection.">Pick a session to compare message table vs event projection.</p>
</div>
<div class="field">
<div class="control">
<input class="input is-small" name="detail_limit" value="25" type="number" min="0" max="200">
</div>
</div>
<button class="button is-small is-link is-light" type="submit">Run Shadow</button>
</form>
</div>
<div class="column is-12-tablet is-3-desktop">
<form id="event-ledger-smoke-form">
<label class="label is-size-7">Event Ledger Smoke</label>
<div class="field">
<div class="control">
<input class="input is-small" name="minutes" value="120" type="number" min="1" max="10080">
</div>
<p class="help is-size-7" title="Use this to check that recent traffic is actually writing canonical events.">Checks whether recent actions were written to `ConversationEvent`.</p>
</div>
<div class="field">
<div class="control">
<input class="input is-small" name="service" placeholder="service" list="diagnostics-service-options">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-small" name="require_types" placeholder="message_created,reaction_added" list="diagnostics-event-type-options">
</div>
<p class="help is-size-7" title="If set, response includes missing required event types so you can quickly spot dual-write gaps.">Optional required event types (comma-separated).</p>
</div>
<button class="button is-small is-link is-light" type="submit">Run Smoke</button>
</form>
</div>
<div class="column is-12-tablet is-3-desktop">
<form id="trace-diagnostics-form">
<label class="label is-size-7">Trace Diagnostics</label>
<div class="field">
<div class="control">
<input class="input is-small" name="trace_id" placeholder="select recent trace id or paste one" list="diagnostics-trace-options" required>
</div>
<p class="help is-size-7" title="Use this to reconstruct ingress -> persistence -> fanout path and jump to projection-shadow for linked sessions.">Use a trace id from the dropdown (recent traces), Event Ledger Smoke `sample[].trace_id`, or UR logs.</p>
</div>
<button class="button is-small is-link is-light" type="submit">Lookup Trace</button>
</form>
</div>
<div class="column is-12-tablet is-3-desktop">
<form id="memory-search-form">
<label class="label is-size-7">Memory Search</label>
<div class="field">
<div class="control">
<input class="input is-small" name="q" placeholder="search memory text" required>
</div>
<p class="help is-size-7" title="Use this for fast retrieval over AI memory/wiki text; backend can be django or manticore.">Query memory index and inspect top matches.</p>
</div>
<div class="field">
<div class="control">
<input class="input is-small" name="statuses" placeholder="active" value="active">
</div>
</div>
<button class="button is-small is-link is-light" type="submit">Query Memory</button>
<button class="button is-small is-light" type="button" id="memory-search-status">Backend Status</button>
</form>
</div>
</div>
<datalist id="diagnostics-session-options">
{% for row in diagnostics_options.sessions %}
<option value="{{ row.label }}" data-session-id="{{ row.id }}"></option>
{% endfor %}
</datalist>
<datalist id="diagnostics-trace-options">
{% for trace_id in diagnostics_options.trace_ids %}
<option value="{{ trace_id }}"></option>
{% endfor %}
</datalist>
<datalist id="diagnostics-service-options">
{% for service in diagnostics_options.services %}
<option value="{{ service }}"></option>
{% endfor %}
</datalist>
<datalist id="diagnostics-event-type-options">
{% for event_type in diagnostics_options.event_types %}
<option value="{{ event_type }}"></option>
{% endfor %}
</datalist>
<div class="buttons are-small" style="margin-bottom: 0.5rem;">
<button id="diagnostics-select-all" type="button" class="button is-light">Select All</button>
<button id="diagnostics-copy" type="button" class="button is-light">Copy</button>
</div>
<pre id="diagnostics-output" class="is-size-7" style="max-height: 20rem; overflow:auto; background:#f7f7f7; padding:0.75rem; border-radius:8px;"></pre>
</article>
</div>
<div class="column is-12">
<article class="box">
<h2 class="title is-6">Purge OSINT Setup Categories</h2>
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.65rem;">
Category-specific cleanup controls.
</p>
<div class="buttons are-small" style="margin-bottom: 0.75rem;">
<form method="post" style="margin: 0;">
{% csrf_token %}
<input type="hidden" name="action" value="purge_osint_people">
<button type="submit" class="button is-warning is-light">Purge People ({{ counts.osint_people }})</button>
</form>
<form method="post" style="margin: 0;">
{% csrf_token %}
<input type="hidden" name="action" value="purge_osint_identifiers">
<button type="submit" class="button is-warning is-light">Purge Identifiers ({{ counts.osint_identifiers }})</button>
</form>
<form method="post" style="margin: 0;">
{% csrf_token %}
<input type="hidden" name="action" value="purge_osint_groups">
<button type="submit" class="button is-warning is-light">Purge Groups ({{ counts.osint_groups }})</button>
</form>
<form method="post" style="margin: 0;">
{% csrf_token %}
<input type="hidden" name="action" value="purge_osint_personas">
<button type="submit" class="button is-warning is-light">Purge Personas ({{ counts.osint_personas }})</button>
</form>
</div>
</article>
</div>
</div>
</div>
</section>
<script>
(function () {
const out = document.getElementById("diagnostics-output");
const shadowForm = document.getElementById("projection-shadow-form");
const smokeForm = document.getElementById("event-ledger-smoke-form");
const traceForm = document.getElementById("trace-diagnostics-form");
const memoryForm = document.getElementById("memory-search-form");
const memoryStatusBtn = document.getElementById("memory-search-status");
const selectAllBtn = document.getElementById("diagnostics-select-all");
const copyBtn = document.getElementById("diagnostics-copy");
const sessionOptionMap = new Map();
const sessionDatalist = document.getElementById("diagnostics-session-options");
if (sessionDatalist) {
sessionDatalist.querySelectorAll("option").forEach(function (opt) {
const key = (opt.value || "").trim();
const sessionId = (opt.dataset.sessionId || "").trim();
if (key && sessionId) {
sessionOptionMap.set(key, sessionId);
}
});
}
function write(text) {
if (out) {
out.textContent = text;
}
}
function outputText() {
return out ? (out.textContent || "") : "";
}
function selectOutputText() {
if (!out) return;
const range = document.createRange();
range.selectNodeContents(out);
const selection = window.getSelection();
if (!selection) return;
selection.removeAllRanges();
selection.addRange(range);
}
async function copyOutputText() {
const text = outputText();
if (!text) return;
try {
await navigator.clipboard.writeText(text);
write(text + "\n\n[Copied]");
return;
} catch (err) {
const area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.focus();
area.select();
try {
document.execCommand("copy");
write(text + "\n\n[Copied]");
} finally {
document.body.removeChild(area);
}
}
}
async function runGet(url, params) {
const query = new URLSearchParams(params);
const response = await fetch(`${url}?${query.toString()}`, { headers: { "Accept": "application/json" } });
const payload = await response.json();
return { status: response.status, payload };
}
shadowForm.addEventListener("submit", async function (ev) {
ev.preventDefault();
write("Running projection shadow...");
const form = new FormData(shadowForm);
const sessionLookup = (form.get("session_lookup") || "").toString().trim();
const sessionId = sessionOptionMap.get(sessionLookup) || sessionLookup;
const result = await runGet("{% url 'system_projection_shadow' %}", {
session_id: sessionId,
detail_limit: (form.get("detail_limit") || "25").toString().trim(),
});
write(JSON.stringify(result, null, 2));
});
smokeForm.addEventListener("submit", async function (ev) {
ev.preventDefault();
write("Running event ledger smoke...");
const form = new FormData(smokeForm);
const result = await runGet("{% url 'system_event_ledger_smoke' %}", {
minutes: (form.get("minutes") || "120").toString().trim(),
service: (form.get("service") || "").toString().trim(),
require_types: (form.get("require_types") || "").toString().trim(),
});
write(JSON.stringify(result, null, 2));
});
traceForm.addEventListener("submit", async function (ev) {
ev.preventDefault();
write("Running trace diagnostics...");
const form = new FormData(traceForm);
const result = await runGet("{% url 'system_trace_diagnostics' %}", {
trace_id: (form.get("trace_id") || "").toString().trim(),
});
write(JSON.stringify(result, null, 2));
});
memoryForm.addEventListener("submit", async function (ev) {
ev.preventDefault();
write("Running memory search...");
const form = new FormData(memoryForm);
const result = await runGet("{% url 'system_memory_search_query' %}", {
q: (form.get("q") || "").toString().trim(),
statuses: (form.get("statuses") || "active").toString().trim(),
limit: "20",
});
write(JSON.stringify(result, null, 2));
});
if (memoryStatusBtn) {
memoryStatusBtn.addEventListener("click", async function () {
write("Checking memory search backend...");
const result = await runGet("{% url 'system_memory_search_status' %}", {});
write(JSON.stringify(result, null, 2));
});
}
if (selectAllBtn) {
selectAllBtn.addEventListener("click", function () {
selectOutputText();
});
}
if (copyBtn) {
copyBtn.addEventListener("click", function () {
copyOutputText();
});
}
})();
</script>
{% endblock %}