Implement Manticore fully and re-theme

This commit is contained in:
2026-03-11 02:19:08 +00:00
parent da044be68c
commit cbedcd67f6
46 changed files with 3444 additions and 944 deletions

View File

@@ -6,10 +6,50 @@
<!DOCTYPE html>
<html lang="en-GB">
<head>
<script>
(function () {
var storedTheme = "light";
try {
storedTheme = localStorage.getItem("theme") || "light";
} catch (error) {
}
var resolvedTheme = storedTheme === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = resolvedTheme;
if (resolvedTheme === "dark") {
document.documentElement.style.backgroundColor = "#090d16";
document.documentElement.style.color = "#f8fafc";
} else {
document.documentElement.style.backgroundColor = "#f3f4f6";
document.documentElement.style.color = "#1b2433";
}
var faviconHref = resolvedTheme === "dark"
? "{% static 'favicon-dark.svg' %}"
: "{% static 'favicon-light.svg' %}";
document.querySelectorAll("link[data-gia-favicon]").forEach(function (link) {
link.setAttribute("href", faviconHref);
});
document.querySelectorAll(".js-theme-logo").forEach(function (image) {
image.setAttribute("src", faviconHref);
});
})();
</script>
<style>
html[data-theme="dark"],
html[data-theme="dark"] body {
background: #090d16 !important;
color: #f8fafc !important;
}
html[data-theme="light"],
html[data-theme="light"] body {
background: #f3f4f6 !important;
color: #1b2433 !important;
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block browser_title %}{% firstof page_browser_title page_title as explicit_title %}{% if explicit_title %}{{ explicit_title }} · GIA{% else %}{% with route_value=request.resolver_match.url_name|default:request.path_info|humanize_route %}{% if route_value %}{{ route_value }} · GIA{% else %}GIA{% endif %}{% endwith %}{% endif %}{% endblock %}</title>
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
<link rel="icon" type="image/svg+xml" href="{% static 'favicon-light.svg' %}" data-gia-favicon>
<link rel="shortcut icon" href="{% static 'favicon-light.svg' %}" data-gia-favicon>
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
@@ -29,6 +69,66 @@
<script src="{% static 'js/gridstack-all.js' %}"></script>
<script defer src="{% static 'js/magnet.min.js' %}"></script>
<script>
(function () {
function applyFavicon(theme) {
var href = theme === "dark"
? "{% static 'favicon-dark.svg' %}"
: "{% static 'favicon-light.svg' %}";
document.querySelectorAll("link[data-gia-favicon]").forEach(function (link) {
link.setAttribute("href", href);
});
document.querySelectorAll(".js-theme-logo").forEach(function (image) {
image.setAttribute("src", href);
});
}
function applyTheme(mode) {
var validMode = mode === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = validMode;
applyFavicon(validMode);
try {
localStorage.setItem("theme", validMode);
} catch (error) {
}
document.querySelectorAll(".js-theme-toggle").forEach(function (button) {
var nextMode = validMode === "dark" ? "light" : "dark";
button.setAttribute("data-theme-mode", validMode);
button.setAttribute("aria-label", "Theme: " + validMode + ". Click to switch to " + nextMode + ".");
button.setAttribute("title", "Theme: " + validMode);
var label = button.querySelector("[data-theme-label]");
if (label) {
label.textContent = validMode === "dark" ? "Dark" : "Light";
}
});
}
function cycleTheme() {
var currentTheme = "light";
try {
currentTheme = localStorage.getItem("theme") || "light";
} catch (error) {
}
if (currentTheme === "dark") {
applyTheme("light");
return;
}
applyTheme("dark");
}
try {
applyTheme(localStorage.getItem("theme") || "light");
} catch (error) {
applyTheme("light");
}
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".js-theme-toggle").forEach(function (button) {
button.addEventListener("click", cycleTheme);
});
applyTheme(document.documentElement.dataset.theme || "light");
});
})();
document.addEventListener("restore-scroll", function () {
var scrollpos = localStorage.getItem("scrollpos");
if (scrollpos) {
@@ -204,6 +304,28 @@
.navbar {
background-color:rgba(0, 0, 0, 0.03) !important;
}
.gia-brand-shell {
display: inline-flex;
align-items: center;
}
.gia-brand-logo {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.45rem 0.75rem;
border-radius: 16px;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(21, 28, 39, 0.08);
box-shadow: 0 10px 24px rgba(21, 28, 39, 0.08);
}
.gia-brand-logo img {
display: block;
}
[data-theme="dark"] .gia-brand-logo {
background: rgba(255, 255, 255, 0.96);
border-color: rgba(255, 255, 255, 0.82);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.34);
}
.section > .container.gia-page-shell,
.section > .container {
max-width: 1340px;
@@ -227,11 +349,13 @@
position: sticky;
top: 0;
background: rgba(248, 250, 252, 0.96) !important;
color: #1b1f2a !important;
backdrop-filter: blur(6px);
z-index: 1;
}
[data-theme="dark"] .table thead th {
background: rgba(44, 44, 44, 0.96) !important;
color: #f3f5f8 !important;
}
.table td,
.table th {
@@ -351,6 +475,26 @@
color: #1f4f99 !important;
font-weight: 600;
}
.theme-toggle-button {
display: inline-flex;
align-items: center;
justify-content: center;
}
.brand-theme-toggle {
min-width: 0;
padding: 0;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
line-height: 1;
width: 3.15rem;
height: 3.15rem;
}
.brand-theme-logo {
display: block;
width: 3.15rem;
height: 3.15rem;
}
.security-page-tabs a {
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
}
@@ -369,9 +513,20 @@
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'home' %}">
<img src="{% static 'logo.svg' %}" width="112" height="28" alt="logo">
</a>
<div class="navbar-item">
<span class="gia-brand-shell">
{% if user.is_authenticated %}
<button class="button is-light theme-toggle-button gia-brand-logo brand-theme-toggle js-theme-toggle" type="button" data-theme-mode="light" aria-label="Theme toggle">
<img class="brand-theme-logo js-theme-logo" src="{% static 'favicon-light.svg' %}" alt="" aria-hidden="true">
<span class="is-sr-only" data-theme-label>Auto</span>
</button>
{% else %}
<a href="{% url 'home' %}" class="gia-brand-logo" aria-label="GIA home">
<img class="brand-theme-logo" src="{% static 'favicon-light.svg' %}" alt="logo">
</a>
{% endif %}
</span>
</div>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="bar">
<span aria-hidden="true"></span>
@@ -473,6 +628,9 @@
System
</a>
{% endif %}
<a class="navbar-item{% if request.resolver_match.url_name == 'accessibility_settings' %} is-current-route{% endif %}" href="{% url 'accessibility_settings' %}">
Accessibility
</a>
<hr class="navbar-divider">
<div class="navbar-item has-text-weight-semibold is-size-7 has-text-grey">
Security
@@ -512,8 +670,8 @@
<a class="navbar-item{% if request.resolver_match.url_name == 'translation_settings' %} is-current-route{% endif %}" href="{% url 'translation_settings' %}">
Translation
</a>
<a class="navbar-item{% if request.resolver_match.url_name == 'availability_settings' %} is-current-route{% endif %}" href="{% url 'availability_settings' %}">
Availability
<a class="navbar-item{% if request.resolver_match.url_name == 'availability_settings' or request.resolver_match.url_name == 'behavioral_signals_settings' %} is-current-route{% endif %}" href="{% url 'behavioral_signals_settings' %}">
Behavioral Signals
</a>
<hr class="navbar-divider">
<div class="navbar-item has-text-weight-semibold is-size-7 has-text-grey">
@@ -525,10 +683,6 @@
<a class="navbar-item{% if request.resolver_match.url_name == 'osint_workspace' %} is-current-route{% endif %}" href="{% url 'osint_workspace' %}">
OSINT Workspace
</a>
<hr class="navbar-divider">
<a class="navbar-item{% if request.resolver_match.url_name == 'accessibility_settings' %} is-current-route{% endif %}" href="{% url 'accessibility_settings' %}">
Accessibility
</a>
</div>
</div>
{% endif %}

View File

@@ -3,7 +3,8 @@
{% block content %}
<section class="section">
<div class="container">
<h1 class="title is-4">Availability Settings</h1>
<h1 class="title is-4">Behavioral Signals</h1>
<p class="subtitle is-6">Presence is only one slice. This page exposes the broader behavioral event surface used for timing, read, typing, and response analysis.</p>
<form method="post" class="box">
{% csrf_token %}
<div class="columns is-multiline">
@@ -24,44 +25,92 @@
</form>
<div class="box">
<h2 class="title is-6">Availability Event Statistics Per Contact</h2>
<div class="level mb-3">
<div class="level-left">
<div>
<h2 class="title is-6 mb-1">Behavioral Event Statistics</h2>
<p class="help is-size-7">Primary source is `gia_events` in Manticore. When unavailable, this page falls back to Django `ConversationEvent` shadow rows.</p>
</div>
</div>
<div class="level-right">
<span class="tag is-light">Source: {{ behavioral_stats_source }}</span>
</div>
</div>
<div class="tags mb-4">
<span class="tag is-info is-light">Contacts: {{ behavioral_totals.contacts }}</span>
<span class="tag is-light">Events: {{ behavioral_totals.total_events }}</span>
<span class="tag is-light">Presence: {{ behavioral_totals.presence_events }}</span>
<span class="tag is-light">Read: {{ behavioral_totals.read_events }}</span>
<span class="tag is-light">Typing: {{ behavioral_totals.typing_events }}</span>
<span class="tag is-light">Messages: {{ behavioral_totals.message_events }}</span>
<span class="tag is-light">Abandoned: {{ behavioral_totals.abandoned_events }}</span>
</div>
<h3 class="title is-7">By Transport</h3>
<table class="table is-fullwidth is-striped is-size-7 mb-5">
<thead>
<tr>
<th>Service</th>
<th>Contacts</th>
<th>Total</th>
<th>Presence</th>
<th>Read</th>
<th>Typing</th>
<th>Messages</th>
<th>Abandoned</th>
<th>Last Event TS</th>
</tr>
</thead>
<tbody>
{% for row in transport_stats %}
<tr>
<td>{{ row.service }}</td>
<td>{{ row.contacts }}</td>
<td>{{ row.total_events }}</td>
<td>{{ row.presence_events }}</td>
<td>{{ row.read_events }}</td>
<td>{{ row.typing_events }}</td>
<td>{{ row.message_events }}</td>
<td>{{ row.abandoned_events }}</td>
<td>{{ row.last_event_ts }}</td>
</tr>
{% empty %}
<tr><td colspan="9">No behavioral transport summaries available.</td></tr>
{% endfor %}
</tbody>
</table>
<h3 class="title is-7">By Contact</h3>
<table class="table is-fullwidth is-striped is-size-7">
<thead>
<tr>
<th>Contact</th>
<th>Service</th>
<th>Total</th>
<th>Available</th>
<th>Fading</th>
<th>Unavailable</th>
<th>Unknown</th>
<th>Native</th>
<th>Presence</th>
<th>Read</th>
<th>Typing</th>
<th>Msg Activity</th>
<th>Timeout</th>
<th>Messages</th>
<th>Abandoned</th>
<th>Last Event TS</th>
</tr>
</thead>
<tbody>
{% for row in contact_stats %}
{% for row in behavioral_stats %}
<tr>
<td>{{ row.person__name }}</td>
<td>{{ row.person_name }}</td>
<td>{{ row.service }}</td>
<td>{{ row.total_events }}</td>
<td>{{ row.available_events }}</td>
<td>{{ row.fading_events }}</td>
<td>{{ row.unavailable_events }}</td>
<td>{{ row.unknown_events }}</td>
<td>{{ row.native_presence_events }}</td>
<td>{{ row.read_receipt_events }}</td>
<td>{{ row.presence_events }}</td>
<td>{{ row.read_events }}</td>
<td>{{ row.typing_events }}</td>
<td>{{ row.message_activity_events }}</td>
<td>{{ row.inferred_timeout_events }}</td>
<td>{{ row.message_events }}</td>
<td>{{ row.abandoned_events }}</td>
<td>{{ row.last_event_ts }}</td>
</tr>
{% empty %}
<tr><td colspan="13">No availability events found.</td></tr>
<tr><td colspan="9">No behavioral events found in either Manticore or ConversationEvent shadow rows.</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -24,6 +24,8 @@
<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-info is-light">Behavioral Events: {{ counts.behavioral_event_rows }}</span>
<span class="tag is-light">Event Shadow Rows: {{ counts.conversation_event_shadow_rows }}</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>
@@ -71,7 +73,7 @@
<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>
<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 the behavioral event ledger in Manticore, with Django shadow rows as fallback context.</p>
</div>
<div class="field">
<div class="control">