Improve settings hierarchy conciseness
This commit is contained in:
@@ -116,6 +116,7 @@ TEMPLATES = [
|
|||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"core.util.django_settings_export.settings_export",
|
"core.util.django_settings_export.settings_export",
|
||||||
|
"core.context_processors.settings_hierarchy_nav",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
82
app/urls.py
82
app/urls.py
@@ -17,8 +17,10 @@ from django.conf import settings
|
|||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import LogoutView
|
||||||
|
from django.views.generic import RedirectView
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from two_factor.urls import urlpatterns as tf_urls
|
from two_factor.urls import urlpatterns as tf_urls
|
||||||
|
from two_factor.views.profile import ProfileView
|
||||||
|
|
||||||
from core.views import (
|
from core.views import (
|
||||||
ais,
|
ais,
|
||||||
@@ -49,21 +51,69 @@ urlpatterns = [
|
|||||||
path("__debug__/", include("debug_toolbar.urls")),
|
path("__debug__/", include("debug_toolbar.urls")),
|
||||||
path("", base.Home.as_view(), name="home"),
|
path("", base.Home.as_view(), name="home"),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path(
|
||||||
|
"account/two_factor/",
|
||||||
|
RedirectView.as_view(pattern_name="security_2fa", permanent=False),
|
||||||
|
),
|
||||||
# 2FA login urls
|
# 2FA login urls
|
||||||
path("", include(tf_urls)),
|
path("", include(tf_urls)),
|
||||||
path("accounts/signup/", base.Signup.as_view(), name="signup"),
|
path("accounts/signup/", base.Signup.as_view(), name="signup"),
|
||||||
path("accounts/logout/", LogoutView.as_view(), name="logout"),
|
path("accounts/logout/", LogoutView.as_view(), name="logout"),
|
||||||
# Notifications
|
# Notifications
|
||||||
|
path(
|
||||||
|
"notifications/page/update/",
|
||||||
|
RedirectView.as_view(pattern_name="notifications_settings", permanent=False),
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"notifications/<str:type>/update/",
|
"notifications/<str:type>/update/",
|
||||||
notifications.NotificationsUpdate.as_view(),
|
notifications.NotificationsUpdate.as_view(),
|
||||||
name="notifications_update",
|
name="notifications_update",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"settings/notifications/",
|
||||||
|
notifications.NotificationsUpdate.as_view(),
|
||||||
|
{"type": "page"},
|
||||||
|
name="notifications_settings",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"settings/security/",
|
"settings/security/",
|
||||||
system.SecurityPage.as_view(),
|
system.SecurityPage.as_view(page_mode="encryption"),
|
||||||
name="security_settings",
|
name="security_settings",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"settings/security/encryption/",
|
||||||
|
system.SecurityPage.as_view(page_mode="encryption"),
|
||||||
|
name="encryption_settings",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/security/permissions/",
|
||||||
|
system.SecurityPage.as_view(page_mode="permission"),
|
||||||
|
name="permission_settings",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/security/permission/",
|
||||||
|
RedirectView.as_view(pattern_name="permission_settings", permanent=False),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/security/2fa/",
|
||||||
|
ProfileView.as_view(),
|
||||||
|
name="security_2fa",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/accessibility/",
|
||||||
|
system.AccessibilitySettings.as_view(),
|
||||||
|
name="accessibility_settings",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/ai/",
|
||||||
|
system.AISettingsPage.as_view(),
|
||||||
|
name="ai_settings",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/modules/",
|
||||||
|
system.ModulesSettingsPage.as_view(),
|
||||||
|
name="modules_settings",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"settings/system/",
|
"settings/system/",
|
||||||
system.SystemSettings.as_view(),
|
system.SystemSettings.as_view(),
|
||||||
@@ -115,10 +165,24 @@ urlpatterns = [
|
|||||||
name="command_routing",
|
name="command_routing",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"settings/ai-execution/",
|
"settings/ai/traces/",
|
||||||
automation.AIExecutionLogSettings.as_view(),
|
automation.AIExecutionLogSettings.as_view(),
|
||||||
name="ai_execution_log",
|
name="ai_execution_log",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"settings/ai/traces/run/<int:run_id>/",
|
||||||
|
automation.AIExecutionRunDetailView.as_view(),
|
||||||
|
name="ai_execution_run_detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/ai/traces/run/<int:run_id>/tab/<str:tab_slug>/",
|
||||||
|
automation.AIExecutionRunDetailTabView.as_view(),
|
||||||
|
name="ai_execution_run_detail_tab",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"settings/ai-execution/",
|
||||||
|
RedirectView.as_view(pattern_name="ai_execution_log", permanent=False),
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"settings/translation/",
|
"settings/translation/",
|
||||||
automation.TranslationSettings.as_view(),
|
automation.TranslationSettings.as_view(),
|
||||||
@@ -500,6 +564,20 @@ urlpatterns = [
|
|||||||
workspace.AIWorkspaceUpdatePlanMeta.as_view(),
|
workspace.AIWorkspaceUpdatePlanMeta.as_view(),
|
||||||
name="ai_workspace_mitigation_meta_save",
|
name="ai_workspace_mitigation_meta_save",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"settings/ai/models/",
|
||||||
|
ais.AIList.as_view(),
|
||||||
|
{"type": "page"},
|
||||||
|
name="ai_models",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ai/models/",
|
||||||
|
RedirectView.as_view(pattern_name="ai_models", permanent=False),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ai/page/",
|
||||||
|
RedirectView.as_view(pattern_name="ai_models", permanent=False),
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"ai/<str:type>/",
|
"ai/<str:type>/",
|
||||||
ais.AIList.as_view(),
|
ais.AIList.as_view(),
|
||||||
|
|||||||
119
core/context_processors.py
Normal file
119
core/context_processors.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
def _tab(label: str, href: str, active: bool) -> dict:
|
||||||
|
return {
|
||||||
|
"label": label,
|
||||||
|
"href": href,
|
||||||
|
"active": bool(active),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def settings_hierarchy_nav(request):
|
||||||
|
match = getattr(request, "resolver_match", None)
|
||||||
|
if match is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
url_name = str(getattr(match, "url_name", "") or "")
|
||||||
|
namespace = str(getattr(match, "namespace", "") or "")
|
||||||
|
path = str(getattr(request, "path", "") or "")
|
||||||
|
|
||||||
|
notifications_href = reverse("notifications_settings")
|
||||||
|
system_href = reverse("system_settings")
|
||||||
|
accessibility_href = reverse("accessibility_settings")
|
||||||
|
encryption_href = reverse("encryption_settings")
|
||||||
|
permissions_href = reverse("permission_settings")
|
||||||
|
security_2fa_href = reverse("security_2fa")
|
||||||
|
ai_models_href = reverse("ai_models")
|
||||||
|
ai_traces_href = reverse("ai_execution_log")
|
||||||
|
commands_href = reverse("command_routing")
|
||||||
|
tasks_href = reverse("tasks_settings")
|
||||||
|
translation_href = reverse("translation_settings")
|
||||||
|
availability_href = reverse("availability_settings")
|
||||||
|
|
||||||
|
general_routes = {
|
||||||
|
"notifications_settings",
|
||||||
|
"notifications_update",
|
||||||
|
"system_settings",
|
||||||
|
"accessibility_settings",
|
||||||
|
}
|
||||||
|
security_routes = {
|
||||||
|
"security_settings",
|
||||||
|
"encryption_settings",
|
||||||
|
"permission_settings",
|
||||||
|
"security_2fa",
|
||||||
|
}
|
||||||
|
ai_routes = {
|
||||||
|
"ai_settings",
|
||||||
|
"ai_models",
|
||||||
|
"ais",
|
||||||
|
"ai_create",
|
||||||
|
"ai_update",
|
||||||
|
"ai_delete",
|
||||||
|
"ai_execution_log",
|
||||||
|
}
|
||||||
|
modules_routes = {
|
||||||
|
"modules_settings",
|
||||||
|
"command_routing",
|
||||||
|
"tasks_settings",
|
||||||
|
"translation_settings",
|
||||||
|
"availability_settings",
|
||||||
|
}
|
||||||
|
|
||||||
|
two_factor_security_routes = {
|
||||||
|
"profile",
|
||||||
|
"setup",
|
||||||
|
"backup_tokens",
|
||||||
|
"disable",
|
||||||
|
"phone_create",
|
||||||
|
"phone_delete",
|
||||||
|
}
|
||||||
|
|
||||||
|
if url_name in general_routes:
|
||||||
|
settings_nav = {
|
||||||
|
"title": "General",
|
||||||
|
"tabs": [
|
||||||
|
_tab("Notifications", notifications_href, path == notifications_href),
|
||||||
|
_tab("System", system_href, path == system_href),
|
||||||
|
_tab("Accessibility", accessibility_href, path == accessibility_href),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
elif url_name in security_routes or (
|
||||||
|
namespace == "two_factor" and url_name in two_factor_security_routes
|
||||||
|
):
|
||||||
|
settings_nav = {
|
||||||
|
"title": "Security",
|
||||||
|
"tabs": [
|
||||||
|
_tab("Encryption", encryption_href, path == encryption_href),
|
||||||
|
_tab("Permissions", permissions_href, path == permissions_href),
|
||||||
|
_tab(
|
||||||
|
"2FA",
|
||||||
|
security_2fa_href,
|
||||||
|
path == security_2fa_href or namespace == "two_factor",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
elif url_name in ai_routes:
|
||||||
|
settings_nav = {
|
||||||
|
"title": "AI",
|
||||||
|
"tabs": [
|
||||||
|
_tab("Models", ai_models_href, path == ai_models_href),
|
||||||
|
_tab("Traces", ai_traces_href, path == ai_traces_href),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
elif url_name in modules_routes:
|
||||||
|
settings_nav = {
|
||||||
|
"title": "Modules",
|
||||||
|
"tabs": [
|
||||||
|
_tab("Commands", commands_href, path == commands_href),
|
||||||
|
_tab("Tasks", tasks_href, path == tasks_href),
|
||||||
|
_tab("Translation", translation_href, path == translation_href),
|
||||||
|
_tab("Availability", availability_href, path == availability_href),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
settings_nav = None
|
||||||
|
|
||||||
|
if not settings_nav:
|
||||||
|
return {}
|
||||||
|
return {"settings_nav": settings_nav}
|
||||||
40
core/migrations/0041_useraccessibilitysettings.py
Normal file
40
core/migrations/0041_useraccessibilitysettings.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-07 15:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0040_commandsecuritypolicy_gatewaycommandevent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserAccessibilitySettings",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("disable_animations", models.BooleanField(default=False)),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="accessibility_settings",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2925,6 +2925,17 @@ class UserXmppSecuritySettings(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccessibilitySettings(models.Model):
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="accessibility_settings",
|
||||||
|
)
|
||||||
|
disable_animations = models.BooleanField(default=False)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
class TaskCompletionPattern(models.Model):
|
class TaskCompletionPattern(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="task_completion_patterns")
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="task_completion_patterns")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load cache %}
|
{% load cache %}
|
||||||
{% load page_title %}
|
{% load page_title %}
|
||||||
|
{% load accessibility %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en-GB">
|
<html lang="en-GB">
|
||||||
@@ -290,10 +291,26 @@
|
|||||||
.osint-search-form .button.is-fullwidth {
|
.osint-search-form .button.is-fullwidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.navbar-dropdown .navbar-item.is-current-route {
|
||||||
|
background-color: rgba(50, 115, 220, 0.14) !important;
|
||||||
|
color: #1f4f99 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.security-page-tabs a {
|
||||||
|
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
.reduced-motion,
|
||||||
|
.reduced-motion * {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
{% get_accessibility_settings request.user as a11y_settings %}
|
||||||
|
<body{% if a11y_settings and a11y_settings.disable_animations %} class="reduced-motion"{% endif %}>
|
||||||
|
|
||||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
@@ -343,6 +360,31 @@
|
|||||||
<a class="navbar-item" href="{% url 'ai_workspace' %}">
|
<a class="navbar-item" href="{% url 'ai_workspace' %}">
|
||||||
AI
|
AI
|
||||||
</a>
|
</a>
|
||||||
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
|
<a class="navbar-link">
|
||||||
|
Security
|
||||||
|
</a>
|
||||||
|
<div class="navbar-dropdown">
|
||||||
|
<a
|
||||||
|
class="navbar-item{% if request.resolver_match.url_name == 'encryption_settings' or request.resolver_match.url_name == 'security_settings' %} is-current-route{% endif %}"
|
||||||
|
href="{% url 'encryption_settings' %}"
|
||||||
|
>
|
||||||
|
Encryption
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="navbar-item{% if request.resolver_match.url_name == 'permission_settings' %} is-current-route{% endif %}"
|
||||||
|
href="{% url 'permission_settings' %}"
|
||||||
|
>
|
||||||
|
Permission
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="navbar-item{% if request.resolver_match.url_name == 'security_2fa' or request.resolver_match.namespace == 'two_factor' %} is-current-route{% endif %}"
|
||||||
|
href="{% url 'security_2fa' %}"
|
||||||
|
>
|
||||||
|
2FA
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<a class="navbar-item" href="{% url 'osint_search' type='page' %}">
|
<a class="navbar-item" href="{% url 'osint_search' type='page' %}">
|
||||||
Search
|
Search
|
||||||
</a>
|
</a>
|
||||||
@@ -400,38 +442,47 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
<a class="navbar-item" href="{% url 'security_settings' %}">
|
<div class="navbar-item has-text-weight-semibold is-size-7 has-text-grey">
|
||||||
Security
|
General
|
||||||
</a>
|
</div>
|
||||||
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
<a class="navbar-item{% if request.resolver_match.url_name == 'notifications_settings' or request.resolver_match.url_name == 'notifications_update' %} is-current-route{% endif %}" href="{% url 'notifications_settings' %}">
|
||||||
2FA
|
|
||||||
</a>
|
|
||||||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
|
||||||
Notifications
|
Notifications
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" href="{% url 'ais' type='page' %}">
|
|
||||||
Models
|
|
||||||
</a>
|
|
||||||
<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 'availability_settings' %}">
|
|
||||||
Availability
|
|
||||||
</a>
|
|
||||||
<a class="navbar-item" href="{% url 'translation_settings' %}">
|
|
||||||
Translation
|
|
||||||
</a>
|
|
||||||
<a class="navbar-item" href="{% url 'ai_execution_log' %}">
|
|
||||||
AI Execution Log
|
|
||||||
</a>
|
|
||||||
{% if user.is_superuser %}
|
{% if user.is_superuser %}
|
||||||
<a class="navbar-item" href="{% url 'system_settings' %}">
|
<a class="navbar-item{% if request.resolver_match.url_name == 'system_settings' %} is-current-route{% endif %}" href="{% url 'system_settings' %}">
|
||||||
System
|
System
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<hr class="navbar-divider">
|
||||||
|
<div class="navbar-item has-text-weight-semibold is-size-7 has-text-grey">
|
||||||
|
AI
|
||||||
|
</div>
|
||||||
|
<a class="navbar-item{% if request.resolver_match.url_name == 'ai_models' or request.resolver_match.url_name == 'ais' %} is-current-route{% endif %}" href="{% url 'ai_models' %}">
|
||||||
|
Models
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item{% if request.resolver_match.url_name == 'ai_execution_log' %} is-current-route{% endif %}" href="{% url 'ai_execution_log' %}">
|
||||||
|
Traces
|
||||||
|
</a>
|
||||||
|
<hr class="navbar-divider">
|
||||||
|
<div class="navbar-item has-text-weight-semibold is-size-7 has-text-grey">
|
||||||
|
Modules
|
||||||
|
</div>
|
||||||
|
<a class="navbar-item{% if request.resolver_match.url_name == 'command_routing' %} is-current-route{% endif %}" href="{% url 'command_routing' %}">
|
||||||
|
Commands
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item{% if request.resolver_match.url_name == 'tasks_settings' %} is-current-route{% endif %}" href="{% url 'tasks_settings' %}">
|
||||||
|
Tasks
|
||||||
|
</a>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -682,8 +733,9 @@
|
|||||||
</script>
|
</script>
|
||||||
{% block outer_content %}
|
{% block outer_content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<section class="section">
|
<div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{% include "partials/settings-hierarchy-nav.html" %}
|
||||||
{% block content_wrapper %}
|
{% block content_wrapper %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -695,6 +747,6 @@
|
|||||||
<div id="widgets-here" style="display: none;">
|
<div id="widgets-here" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
61
core/templates/mixins/window-content/objects.html
Normal file
61
core/templates/mixins/window-content/objects.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{% include 'mixins/partials/notify.html' %}
|
||||||
|
{% if page_title is not None %}
|
||||||
|
<h1 class="title is-4">{{ page_title }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_subtitle is not None %}
|
||||||
|
<h1 class="subtitle">{{ page_subtitle }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
<div class="buttons">
|
||||||
|
|
||||||
|
{% if submit_url is not None %}
|
||||||
|
<button
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-get="{{ submit_url }}"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#modals-here"
|
||||||
|
class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ title_singular }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if delete_all_url is not None %}
|
||||||
|
<button
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-delete="{{ delete_all_url }}"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#modals-here"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="Are you sure you wish to delete all {{ context_object_name }}?"
|
||||||
|
class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</span>
|
||||||
|
<span>Delete all {{ context_object_name }} </span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% for button in extra_buttons %}
|
||||||
|
<button
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-{{ button.method }}="{{ button.url }}"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#modals-here"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
{% if button.confirm %}hx-confirm="Are you sure you wish to {{ button.action }}?"{% endif %}
|
||||||
|
class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="{{ button.icon }}"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ button.label }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include list_template %}
|
||||||
25
core/templates/pages/accessibility-settings.html
Normal file
25
core/templates/pages/accessibility-settings.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title is-4">Accessibility</h1>
|
||||||
|
<div class="box">
|
||||||
|
<h2 class="title is-6">Motion</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" name="disable_animations"{% if accessibility_settings.disable_animations %} checked{% endif %}>
|
||||||
|
Disable animations
|
||||||
|
</label>
|
||||||
|
<p class="help is-size-7 has-text-grey mt-1">
|
||||||
|
Reduces motion by disabling most transitions and animations across the interface.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="button is-link is-small" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="title is-4">AI Execution Log</h1>
|
<h1 class="title is-4">Traces</h1>
|
||||||
<p class="subtitle is-6">Tracked model calls and usage metrics for this account.</p>
|
<p class="subtitle is-6">Tracked model calls and usage metrics for this account.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,6 +157,7 @@
|
|||||||
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th></th>
|
||||||
<th>Started</th>
|
<th>Started</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Operation</th>
|
<th>Operation</th>
|
||||||
@@ -173,6 +172,22 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for run in runs %}
|
{% for run in runs %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="button is-small is-light trace-run-expand"
|
||||||
|
type="button"
|
||||||
|
data-detail-row="trace-run-detail-{{ run.id }}"
|
||||||
|
data-detail-content="trace-run-detail-content-{{ run.id }}"
|
||||||
|
data-expanded-label="Hide"
|
||||||
|
data-collapsed-label="Show"
|
||||||
|
hx-get="{% url 'ai_execution_run_detail' run_id=run.id %}"
|
||||||
|
hx-target="#trace-run-detail-content-{{ run.id }}"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-trigger="click once"
|
||||||
|
>
|
||||||
|
Show
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td>{{ run.started_at }}</td>
|
<td>{{ run.started_at }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if run.status == "ok" %}
|
{% if run.status == "ok" %}
|
||||||
@@ -197,14 +212,90 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr id="trace-run-detail-{{ run.id }}" class="is-hidden">
|
||||||
|
<td colspan="10">
|
||||||
|
<div id="trace-run-detail-content-{{ run.id }}" class="trace-run-detail-shell is-size-7 has-text-grey">
|
||||||
|
Click Show to load run details.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="9">No runs yet.</td></tr>
|
<tr><td colspan="10">No runs yet.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
<script>
|
||||||
</section>
|
(function () {
|
||||||
|
document.querySelectorAll(".trace-run-expand").forEach(function (button) {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
const rowId = String(button.getAttribute("data-detail-row") || "");
|
||||||
|
const row = rowId ? document.getElementById(rowId) : null;
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isHidden = row.classList.contains("is-hidden");
|
||||||
|
row.classList.toggle("is-hidden", !isHidden);
|
||||||
|
button.textContent = isHidden
|
||||||
|
? String(button.getAttribute("data-expanded-label") || "Hide")
|
||||||
|
: String(button.getAttribute("data-collapsed-label") || "Show");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
const trigger = event.target.closest(".trace-run-tab-trigger");
|
||||||
|
if (!trigger) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
const shell = trigger.closest(".trace-run-detail-tabs");
|
||||||
|
if (!shell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetName = String(trigger.getAttribute("data-tab-target") || "");
|
||||||
|
if (!targetName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shell.querySelectorAll(".trace-run-tab-trigger").forEach(function (item) {
|
||||||
|
item.parentElement.classList.toggle("is-active", item === trigger);
|
||||||
|
});
|
||||||
|
shell.querySelectorAll(".trace-run-tab-panel").forEach(function (panel) {
|
||||||
|
const isActive = panel.getAttribute("data-tab-panel") === targetName;
|
||||||
|
panel.classList.toggle("is-hidden", !isActive);
|
||||||
|
});
|
||||||
|
|
||||||
|
const lazyUrl = String(trigger.getAttribute("data-lazy-url") || "");
|
||||||
|
if (!lazyUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const panel = shell.querySelector(
|
||||||
|
'.trace-run-tab-panel[data-tab-panel="' + targetName + '"]'
|
||||||
|
);
|
||||||
|
if (!panel || panel.getAttribute("data-loaded") === "1") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panel.setAttribute("data-loaded", "1");
|
||||||
|
panel.classList.add("is-loading");
|
||||||
|
fetch(lazyUrl, { credentials: "same-origin" })
|
||||||
|
.then(function (response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("tab load failed");
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(function (html) {
|
||||||
|
panel.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
panel.innerHTML =
|
||||||
|
'<p class="has-text-danger">Unable to load this tab.</p>';
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
panel.classList.remove("is-loading");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title is-4">Security</h1>
|
{% if show_encryption %}
|
||||||
|
|
||||||
<div class="columns is-desktop is-variable is-8">
|
<div class="columns is-desktop is-variable is-8">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@@ -102,7 +101,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_permission %}
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2 class="title is-6">Global Scope Override</h2>
|
<h2 class="title is-6">Global Scope Override</h2>
|
||||||
<p class="is-size-7 has-text-grey mb-3">
|
<p class="is-size-7 has-text-grey mb-3">
|
||||||
@@ -194,14 +195,24 @@
|
|||||||
<label class="label is-size-7 mb-1">Require OMEMO</label>
|
<label class="label is-size-7 mb-1">Require OMEMO</label>
|
||||||
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
|
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
|
||||||
<span class="tag is-size-7" data-global-mode-label="require_omemo"></span>
|
<span class="tag is-size-7" data-global-mode-label="require_omemo"></span>
|
||||||
<button type="button" class="button is-small is-light" data-global-change-toggle="require_omemo">Change Global</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-small is-light"
|
||||||
|
data-global-change-toggle="require_omemo"
|
||||||
|
{% if security_settings.require_omemo %}disabled title="Disable 'Require OMEMO encryption' in Encryption settings to edit this override."{% endif %}
|
||||||
|
>
|
||||||
|
Change Global
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons has-addons is-hidden" data-global-mode-picker="require_omemo">
|
<div class="buttons has-addons is-hidden" data-global-mode-picker="require_omemo">
|
||||||
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="per_scope">Per Scope</button>
|
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="per_scope"{% if security_settings.require_omemo %} disabled{% endif %}>Per Scope</button>
|
||||||
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="on">Force On</button>
|
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="on"{% if security_settings.require_omemo %} disabled{% endif %}>Force On</button>
|
||||||
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="off">Force Off</button>
|
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="off"{% if security_settings.require_omemo %} disabled{% endif %}>Force Off</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% if security_settings.require_omemo %}
|
||||||
|
<p class="help is-size-7 has-text-grey">Locked by Encryption setting: disable "Require OMEMO encryption" to edit this override.</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="field mb-3">
|
<div class="field mb-3">
|
||||||
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
||||||
@@ -263,7 +274,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2 class="title is-6">Command Security Scopes</h2>
|
<h2 class="title is-6">Fine-Grained Security Scopes</h2>
|
||||||
<p class="is-size-7 has-text-grey mb-3">
|
<p class="is-size-7 has-text-grey mb-3">
|
||||||
Choose a top-level category, expand a scope, then click <strong>Change</strong> to edit that scope.
|
Choose a top-level category, expand a scope, then click <strong>Change</strong> to edit that scope.
|
||||||
</p>
|
</p>
|
||||||
@@ -319,7 +330,7 @@
|
|||||||
<input class="scope-editable" data-lock-state="{% if row.enabled_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_enabled"{% if row.enabled %} checked{% endif %}{% if row.enabled_locked %} disabled{% endif %}>
|
<input class="scope-editable" data-lock-state="{% if row.enabled_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_enabled"{% if row.enabled %} checked{% endif %}{% if row.enabled_locked %} disabled{% endif %}>
|
||||||
Scope Enabled
|
Scope Enabled
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox mr-4" title="{% if row.require_omemo_locked %}{{ row.lock_help }}{% endif %}">
|
<label class="checkbox mr-4" title="{% if row.require_omemo_locked %}{{ row.require_omemo_lock_help }}{% endif %}">
|
||||||
<input class="scope-editable" data-lock-state="{% if row.require_omemo_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_require_omemo"{% if row.require_omemo %} checked{% endif %}{% if row.require_omemo_locked %} disabled{% endif %}>
|
<input class="scope-editable" data-lock-state="{% if row.require_omemo_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_require_omemo"{% if row.require_omemo %} checked{% endif %}{% if row.require_omemo_locked %} disabled{% endif %}>
|
||||||
Require OMEMO
|
Require OMEMO
|
||||||
</label>
|
</label>
|
||||||
@@ -380,7 +391,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_encryption %}
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2 class="title is-6">OMEMO Enablement Plan</h2>
|
<h2 class="title is-6">OMEMO Enablement Plan</h2>
|
||||||
<p class="is-size-7 has-text-grey mb-3">Complete each step to achieve end-to-end encrypted messaging with the gateway.</p>
|
<p class="is-size-7 has-text-grey mb-3">Complete each step to achieve end-to-end encrypted messaging with the gateway.</p>
|
||||||
@@ -402,6 +415,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
22
core/templates/pages/settings-category.html
Normal file
22
core/templates/pages/settings-category.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title is-4">{{ category_title }}</h1>
|
||||||
|
<p class="subtitle is-6">{{ category_description }}</p>
|
||||||
|
<div class="tabs is-boxed is-small mb-4 security-page-tabs">
|
||||||
|
<ul>
|
||||||
|
{% for tab in category_tabs %}
|
||||||
|
<li class="{% if tab.active %}is-active{% endif %}">
|
||||||
|
<a href="{{ tab.href }}">{{ tab.label }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<p class="is-size-7 has-text-grey">Choose a tab above to open settings in this category.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
7
core/templates/partials/ai-execution-run-detail-tab.html
Normal file
7
core/templates/partials/ai-execution-run-detail-tab.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{% if tab_slug == "error" %}
|
||||||
|
{% if run.error %}
|
||||||
|
<pre style="white-space: pre-wrap; margin: 0;">{{ run.error }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<p class="has-text-grey">No error recorded for this run.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
56
core/templates/partials/ai-execution-run-detail.html
Normal file
56
core/templates/partials/ai-execution-run-detail.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<div class="trace-run-detail-tabs">
|
||||||
|
<div class="tabs is-small is-toggle is-toggle-rounded mb-2">
|
||||||
|
<ul>
|
||||||
|
<li class="is-active">
|
||||||
|
<a href="#" class="trace-run-tab-trigger" data-tab-target="summary">Summary</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="trace-run-tab-trigger" data-tab-target="identifiers">Identifiers</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="trace-run-tab-trigger" data-tab-target="usage">Usage</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="trace-run-tab-trigger"
|
||||||
|
data-tab-target="error"
|
||||||
|
data-lazy-url="{% url 'ai_execution_run_detail_tab' run_id=run.id tab_slug='error' %}"
|
||||||
|
>Error</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="trace-run-tab-panel" data-tab-panel="summary">
|
||||||
|
<table class="table is-fullwidth is-narrow is-size-7">
|
||||||
|
<tbody>
|
||||||
|
<tr><th>Status</th><td>{{ run.status|default:"-" }}</td></tr>
|
||||||
|
<tr><th>Operation</th><td>{{ run.operation|default:"-" }}</td></tr>
|
||||||
|
<tr><th>Model</th><td>{{ run.model|default:"-" }}</td></tr>
|
||||||
|
<tr><th>Started</th><td>{{ run.started_at|default:"-" }}</td></tr>
|
||||||
|
<tr><th>Finished</th><td>{{ run.finished_at|default:"-" }}</td></tr>
|
||||||
|
<tr><th>Duration (ms)</th><td>{{ run.duration_ms|default:"-" }}</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="trace-run-tab-panel is-hidden" data-tab-panel="identifiers">
|
||||||
|
<table class="table is-fullwidth is-narrow is-size-7">
|
||||||
|
<tbody>
|
||||||
|
<tr><th>Run ID</th><td>{{ run.id }}</td></tr>
|
||||||
|
<tr><th>AI ID</th><td>{% if run.ai_id %}{{ run.ai_id }}{% else %}-{% endif %}</td></tr>
|
||||||
|
<tr><th>Base URL</th><td><code>{{ run.base_url|default:"-" }}</code></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="trace-run-tab-panel is-hidden" data-tab-panel="usage">
|
||||||
|
<table class="table is-fullwidth is-narrow is-size-7">
|
||||||
|
<tbody>
|
||||||
|
<tr><th>Message Count</th><td>{{ run.message_count }}</td></tr>
|
||||||
|
<tr><th>Prompt Chars</th><td>{{ run.prompt_chars }}</td></tr>
|
||||||
|
<tr><th>Response Chars</th><td>{{ run.response_chars }}</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="trace-run-tab-panel is-hidden" data-tab-panel="error" data-loaded="0">
|
||||||
|
<p class="has-text-grey">Click the Error tab to load details.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
12
core/templates/partials/settings-hierarchy-nav.html
Normal file
12
core/templates/partials/settings-hierarchy-nav.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% if settings_nav %}
|
||||||
|
<h1 class="title is-4">{{ settings_nav.title }}</h1>
|
||||||
|
<div class="tabs is-boxed is-small mb-4 security-page-tabs">
|
||||||
|
<ul>
|
||||||
|
{% for tab in settings_nav.tabs %}
|
||||||
|
<li class="{% if tab.active %}is-active{% endif %}">
|
||||||
|
<a href="{{ tab.href }}">{{ tab.label }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post">{% csrf_token %}{{ form }}
|
<form method="post">{% csrf_token %}{{ form }}
|
||||||
<a href="{% url 'two_factor:profile'%}"
|
<a href="{% url 'security_2fa' %}"
|
||||||
class="float-right button">{% trans "Back to Account Security" %}</a>
|
class="float-right button">{% trans "Back to Account Security" %}</a>
|
||||||
<button class="button" type="submit">{% trans "Generate Tokens" %}</button>
|
<button class="button" type="submit">{% trans "Generate Tokens" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
authentication.{% endblocktrans %}</p>
|
authentication.{% endblocktrans %}</p>
|
||||||
|
|
||||||
{% if not phone_methods %}
|
{% if not phone_methods %}
|
||||||
<p class="subtitle"><a href="{% url 'two_factor:profile' %}"
|
<p class="subtitle"><a href="{% url 'security_2fa' %}"
|
||||||
class="button">{% trans "Back to Account Security" %}</a></p>
|
class="button">{% trans "Back to Account Security" %}</a></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="subtitle">{% blocktrans trimmed %}However, it might happen that you don't have access to
|
<p class="subtitle">{% blocktrans trimmed %}However, it might happen that you don't have access to
|
||||||
your primary token device. To enable account recovery, add a phone
|
your primary token device. To enable account recovery, add a phone
|
||||||
number.{% endblocktrans %}</p>
|
number.{% endblocktrans %}</p>
|
||||||
|
|
||||||
<a href="{% url 'two_factor:profile' %}"
|
<a href="{% url 'security_2fa' %}"
|
||||||
class="float-right button">{% trans "Back to Account Security" %}</a>
|
class="float-right button">{% trans "Back to Account Security" %}</a>
|
||||||
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
||||||
class="button">{% trans "Add Phone Number" %}</a></p>
|
class="button">{% trans "Add Phone Number" %}</a></p>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="title">{% block title %}{% trans "Account Security" %}{% endblock %}</h1>
|
<h2 class="title is-5">{% block title %}{% trans "Account Security" %}{% endblock %}</h2>
|
||||||
|
|
||||||
{% if default_device %}
|
{% if default_device %}
|
||||||
{% if default_device_type == 'TOTPDevice' %}
|
{% if default_device_type == 'TOTPDevice' %}
|
||||||
|
|||||||
13
core/templatetags/accessibility.py
Normal file
13
core/templatetags/accessibility.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
from core.models import UserAccessibilitySettings
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def get_accessibility_settings(user):
|
||||||
|
if not getattr(user, "is_authenticated", False):
|
||||||
|
return None
|
||||||
|
row, _ = UserAccessibilitySettings.objects.get_or_create(user=user)
|
||||||
|
return row
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.urls import reverse
|
||||||
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||||
|
|
||||||
from core.forms import AIForm
|
from core.forms import AIForm
|
||||||
@@ -11,7 +12,7 @@ log = logs.get_logger(__name__)
|
|||||||
class AIList(LoginRequiredMixin, ObjectList):
|
class AIList(LoginRequiredMixin, ObjectList):
|
||||||
list_template = "partials/ai-list.html"
|
list_template = "partials/ai-list.html"
|
||||||
model = AI
|
model = AI
|
||||||
page_title = "AIs"
|
page_title = None
|
||||||
# page_subtitle = "Add times here in order to permit trading."
|
# page_subtitle = "Add times here in order to permit trading."
|
||||||
|
|
||||||
list_url_name = "ais"
|
list_url_name = "ais"
|
||||||
@@ -19,6 +20,28 @@ class AIList(LoginRequiredMixin, ObjectList):
|
|||||||
|
|
||||||
submit_url_name = "ai_create"
|
submit_url_name = "ai_create"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
current_path = str(getattr(self.request, "path", "") or "")
|
||||||
|
models_path = reverse("ai_models")
|
||||||
|
traces_path = reverse("ai_execution_log")
|
||||||
|
context["settings_nav"] = {
|
||||||
|
"title": "AI",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"label": "Models",
|
||||||
|
"href": models_path,
|
||||||
|
"active": current_path == models_path,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Traces",
|
||||||
|
"href": traces_path,
|
||||||
|
"active": current_path == traces_path,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class AICreate(LoginRequiredMixin, ObjectCreate):
|
class AICreate(LoginRequiredMixin, ObjectCreate):
|
||||||
model = AI
|
model = AI
|
||||||
|
|||||||
@@ -482,12 +482,55 @@ class AIExecutionLogSettings(LoginRequiredMixin, View):
|
|||||||
"runs": runs,
|
"runs": runs,
|
||||||
"operation_breakdown": operation_breakdown,
|
"operation_breakdown": operation_breakdown,
|
||||||
"model_breakdown": model_breakdown,
|
"model_breakdown": model_breakdown,
|
||||||
|
"settings_nav": {
|
||||||
|
"title": "AI",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"label": "Models",
|
||||||
|
"href": reverse("ai_models"),
|
||||||
|
"active": str(getattr(request, "path", "") or "")
|
||||||
|
== reverse("ai_models"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Traces",
|
||||||
|
"href": reverse("ai_execution_log"),
|
||||||
|
"active": str(getattr(request, "path", "") or "")
|
||||||
|
== reverse("ai_execution_log"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return render(request, self.template_name, self._context(request))
|
return render(request, self.template_name, self._context(request))
|
||||||
|
|
||||||
|
|
||||||
|
class AIExecutionRunDetailView(LoginRequiredMixin, View):
|
||||||
|
template_name = "partials/ai-execution-run-detail.html"
|
||||||
|
|
||||||
|
def get(self, request, run_id):
|
||||||
|
run = get_object_or_404(AIRunLog, id=run_id, user=request.user)
|
||||||
|
return render(request, self.template_name, {"run": run})
|
||||||
|
|
||||||
|
|
||||||
|
class AIExecutionRunDetailTabView(LoginRequiredMixin, View):
|
||||||
|
template_name = "partials/ai-execution-run-detail-tab.html"
|
||||||
|
|
||||||
|
def get(self, request, run_id, tab_slug):
|
||||||
|
run = get_object_or_404(AIRunLog, id=run_id, user=request.user)
|
||||||
|
slug = str(tab_slug or "").strip().lower()
|
||||||
|
if slug not in {"error"}:
|
||||||
|
return JsonResponse({"ok": False, "error": "unknown_tab"}, status=404)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
self.template_name,
|
||||||
|
{
|
||||||
|
"run": run,
|
||||||
|
"tab_slug": slug,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BusinessPlanEditor(LoginRequiredMixin, View):
|
class BusinessPlanEditor(LoginRequiredMixin, View):
|
||||||
template_name = "pages/business-plan-editor.html"
|
template_name = "pages/business-plan-editor.html"
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from core.models import (
|
|||||||
PersonIdentifier,
|
PersonIdentifier,
|
||||||
QueuedMessage,
|
QueuedMessage,
|
||||||
CommandSecurityPolicy,
|
CommandSecurityPolicy,
|
||||||
|
UserAccessibilitySettings,
|
||||||
UserXmppOmemoState,
|
UserXmppOmemoState,
|
||||||
UserXmppSecuritySettings,
|
UserXmppSecuritySettings,
|
||||||
WorkspaceConversation,
|
WorkspaceConversation,
|
||||||
@@ -489,6 +490,7 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
"""Security settings page for OMEMO and command-scope policy controls."""
|
"""Security settings page for OMEMO and command-scope policy controls."""
|
||||||
|
|
||||||
template_name = "pages/security.html"
|
template_name = "pages/security.html"
|
||||||
|
page_mode = "encryption"
|
||||||
GLOBAL_SCOPE_KEY = "global.override"
|
GLOBAL_SCOPE_KEY = "global.override"
|
||||||
# Allowed Services list used by both Global Scope Override and local scopes.
|
# Allowed Services list used by both Global Scope Override and local scopes.
|
||||||
# Keep this in sync with the UI text on the Security page.
|
# Keep this in sync with the UI text on the Security page.
|
||||||
@@ -520,6 +522,18 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
"other": "Other",
|
"other": "Other",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _show_encryption(self) -> bool:
|
||||||
|
return str(getattr(self, "page_mode", "encryption")).strip().lower() in {
|
||||||
|
"encryption",
|
||||||
|
"all",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _show_permission(self) -> bool:
|
||||||
|
return str(getattr(self, "page_mode", "encryption")).strip().lower() in {
|
||||||
|
"permission",
|
||||||
|
"all",
|
||||||
|
}
|
||||||
|
|
||||||
def _security_settings(self, request):
|
def _security_settings(self, request):
|
||||||
row, _ = UserXmppSecuritySettings.objects.get_or_create(user=request.user)
|
row, _ = UserXmppSecuritySettings.objects.get_or_create(user=request.user)
|
||||||
return row
|
return row
|
||||||
@@ -616,6 +630,7 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
def _scope_rows(self, request):
|
def _scope_rows(self, request):
|
||||||
global_overrides = self._global_override_payload(request)["values"]
|
global_overrides = self._global_override_payload(request)["values"]
|
||||||
|
security_settings = self._security_settings(request)
|
||||||
rows = {
|
rows = {
|
||||||
str(item.scope_key or "").strip().lower(): item
|
str(item.scope_key or "").strip().lower(): item
|
||||||
for item in CommandSecurityPolicy.objects.filter(user=request.user).exclude(
|
for item in CommandSecurityPolicy.objects.filter(user=request.user).exclude(
|
||||||
@@ -637,7 +652,10 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
if not channel_rules:
|
if not channel_rules:
|
||||||
channel_rules = [{"service": "xmpp", "pattern": ""}]
|
channel_rules = [{"service": "xmpp", "pattern": ""}]
|
||||||
enabled_locked = global_overrides["scope_enabled"] != "per_scope"
|
enabled_locked = global_overrides["scope_enabled"] != "per_scope"
|
||||||
require_omemo_locked = global_overrides["require_omemo"] != "per_scope"
|
require_omemo_locked = (
|
||||||
|
global_overrides["require_omemo"] != "per_scope"
|
||||||
|
or bool(security_settings.require_omemo)
|
||||||
|
)
|
||||||
require_trusted_locked = (
|
require_trusted_locked = (
|
||||||
global_overrides["require_trusted_fingerprint"] != "per_scope"
|
global_overrides["require_trusted_fingerprint"] != "per_scope"
|
||||||
)
|
)
|
||||||
@@ -661,6 +679,11 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
"require_omemo_locked": require_omemo_locked,
|
"require_omemo_locked": require_omemo_locked,
|
||||||
"require_trusted_fingerprint_locked": require_trusted_locked,
|
"require_trusted_fingerprint_locked": require_trusted_locked,
|
||||||
"lock_help": "Set this field to 'Per Scope' in Global Scope Override to edit it here.",
|
"lock_help": "Set this field to 'Per Scope' in Global Scope Override to edit it here.",
|
||||||
|
"require_omemo_lock_help": (
|
||||||
|
"Disable 'Require OMEMO encryption' in Encryption settings to edit this field."
|
||||||
|
if bool(security_settings.require_omemo)
|
||||||
|
else "Set this field to 'Per Scope' in Global Scope Override to edit it here."
|
||||||
|
),
|
||||||
"allowed_services": raw_allowed_services,
|
"allowed_services": raw_allowed_services,
|
||||||
"channel_rules": channel_rules,
|
"channel_rules": channel_rules,
|
||||||
})
|
})
|
||||||
@@ -668,6 +691,8 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
def _scope_group_key(self, scope_key: str) -> str:
|
def _scope_group_key(self, scope_key: str) -> str:
|
||||||
key = str(scope_key or "").strip().lower()
|
key = str(scope_key or "").strip().lower()
|
||||||
|
if key in {"tasks.commands", "gateway.tasks"}:
|
||||||
|
return "tasks"
|
||||||
if key in {"command.codex", "command.claude"}:
|
if key in {"command.codex", "command.claude"}:
|
||||||
return "agentic"
|
return "agentic"
|
||||||
if key.startswith("gateway."):
|
if key.startswith("gateway."):
|
||||||
@@ -712,12 +737,17 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
if "require_omemo" in request.POST:
|
if "require_omemo" in request.POST:
|
||||||
row.require_omemo = _to_bool(request.POST.get("require_omemo"), False)
|
row.require_omemo = _to_bool(request.POST.get("require_omemo"), False)
|
||||||
row.save(update_fields=["require_omemo", "updated_at"])
|
row.save(update_fields=["require_omemo", "updated_at"])
|
||||||
redirect_to = HttpResponseRedirect(reverse("security_settings"))
|
redirect_to = HttpResponseRedirect(request.path)
|
||||||
scope_key = str(request.POST.get("scope_key") or "").strip().lower()
|
scope_key = str(request.POST.get("scope_key") or "").strip().lower()
|
||||||
|
if not self._show_permission():
|
||||||
|
return redirect_to
|
||||||
if scope_key == self.GLOBAL_SCOPE_KEY:
|
if scope_key == self.GLOBAL_SCOPE_KEY:
|
||||||
global_row = self._global_override_payload(request)["row"]
|
global_row = self._global_override_payload(request)["row"]
|
||||||
|
security_settings = self._security_settings(request)
|
||||||
settings_payload = dict(global_row.settings or {})
|
settings_payload = dict(global_row.settings or {})
|
||||||
for field in self.GLOBAL_OVERRIDE_FIELDS:
|
for field in self.GLOBAL_OVERRIDE_FIELDS:
|
||||||
|
if field == "require_omemo" and bool(security_settings.require_omemo):
|
||||||
|
continue
|
||||||
settings_payload[field] = self._parse_override_value(
|
settings_payload[field] = self._parse_override_value(
|
||||||
request.POST.get(f"global_{field}")
|
request.POST.get(f"global_{field}")
|
||||||
)
|
)
|
||||||
@@ -742,6 +772,7 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
if str(request.POST.get("scope_change_mode") or "").strip() != "1":
|
if str(request.POST.get("scope_change_mode") or "").strip() != "1":
|
||||||
return redirect_to
|
return redirect_to
|
||||||
global_overrides = self._global_override_payload(request)["values"]
|
global_overrides = self._global_override_payload(request)["values"]
|
||||||
|
security_settings = self._security_settings(request)
|
||||||
allowed_services = [
|
allowed_services = [
|
||||||
str(item or "").strip().lower()
|
str(item or "").strip().lower()
|
||||||
for item in request.POST.getlist("allowed_services")
|
for item in request.POST.getlist("allowed_services")
|
||||||
@@ -756,7 +787,10 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
policy.allowed_channels = allowed_channels
|
policy.allowed_channels = allowed_channels
|
||||||
if global_overrides["scope_enabled"] == "per_scope":
|
if global_overrides["scope_enabled"] == "per_scope":
|
||||||
policy.enabled = _to_bool(request.POST.get("policy_enabled"), True)
|
policy.enabled = _to_bool(request.POST.get("policy_enabled"), True)
|
||||||
if global_overrides["require_omemo"] == "per_scope":
|
if (
|
||||||
|
global_overrides["require_omemo"] == "per_scope"
|
||||||
|
and not bool(security_settings.require_omemo)
|
||||||
|
):
|
||||||
policy.require_omemo = _to_bool(
|
policy.require_omemo = _to_bool(
|
||||||
request.POST.get("policy_require_omemo"), False
|
request.POST.get("policy_require_omemo"), False
|
||||||
)
|
)
|
||||||
@@ -778,13 +812,19 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
return redirect_to
|
return redirect_to
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
xmpp_state = transport.get_runtime_state("xmpp")
|
show_encryption = self._show_encryption()
|
||||||
|
show_permission = self._show_permission()
|
||||||
|
xmpp_state = transport.get_runtime_state("xmpp") if show_encryption else {}
|
||||||
|
omemo_row = None
|
||||||
|
if show_encryption:
|
||||||
try:
|
try:
|
||||||
omemo_row = UserXmppOmemoState.objects.get(user=request.user)
|
omemo_row = UserXmppOmemoState.objects.get(user=request.user)
|
||||||
except UserXmppOmemoState.DoesNotExist:
|
except UserXmppOmemoState.DoesNotExist:
|
||||||
omemo_row = None
|
omemo_row = None
|
||||||
security_settings = self._security_settings(request)
|
security_settings = self._security_settings(request)
|
||||||
sender_jid = _parse_xmpp_jid(getattr(omemo_row, "last_sender_jid", "") or "")
|
sender_jid = _parse_xmpp_jid(getattr(omemo_row, "last_sender_jid", "") or "")
|
||||||
|
omemo_plan = []
|
||||||
|
if show_encryption:
|
||||||
omemo_plan = [
|
omemo_plan = [
|
||||||
{
|
{
|
||||||
"label": "Component OMEMO active",
|
"label": "Component OMEMO active",
|
||||||
@@ -817,4 +857,62 @@ class SecurityPage(LoginRequiredMixin, View):
|
|||||||
"policy_groups": self._grouped_scope_rows(request),
|
"policy_groups": self._grouped_scope_rows(request),
|
||||||
"sender_jid": sender_jid,
|
"sender_jid": sender_jid,
|
||||||
"omemo_plan": omemo_plan,
|
"omemo_plan": omemo_plan,
|
||||||
|
"show_encryption": show_encryption,
|
||||||
|
"show_permission": show_permission,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class AccessibilitySettings(LoginRequiredMixin, View):
|
||||||
|
template_name = "pages/accessibility-settings.html"
|
||||||
|
|
||||||
|
def _row(self, request):
|
||||||
|
row, _ = UserAccessibilitySettings.objects.get_or_create(user=request.user)
|
||||||
|
return row
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
"accessibility_settings": self._row(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
row = self._row(request)
|
||||||
|
row.disable_animations = _to_bool(request.POST.get("disable_animations"), False)
|
||||||
|
row.save(update_fields=["disable_animations", "updated_at"])
|
||||||
|
return HttpResponseRedirect(reverse("accessibility_settings"))
|
||||||
|
|
||||||
|
|
||||||
|
class _SettingsCategoryPage(LoginRequiredMixin, View):
|
||||||
|
template_name = "pages/settings-category.html"
|
||||||
|
category_key = "general"
|
||||||
|
category_title = "General"
|
||||||
|
category_description = ""
|
||||||
|
tabs = ()
|
||||||
|
|
||||||
|
def _tab_rows(self):
|
||||||
|
current_path = str(getattr(self.request, "path", "") or "")
|
||||||
|
rows = []
|
||||||
|
for label, href in self.tabs:
|
||||||
|
rows.append({
|
||||||
|
"label": label,
|
||||||
|
"href": href,
|
||||||
|
"active": current_path == href,
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
"category_key": self.category_key,
|
||||||
|
"category_title": self.category_title,
|
||||||
|
"category_description": self.category_description,
|
||||||
|
"category_tabs": self._tab_rows(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class AISettingsPage(LoginRequiredMixin, View):
|
||||||
|
def get(self, request):
|
||||||
|
return HttpResponseRedirect(reverse("ai_models"))
|
||||||
|
|
||||||
|
|
||||||
|
class ModulesSettingsPage(_SettingsCategoryPage):
|
||||||
|
def get(self, request):
|
||||||
|
return HttpResponseRedirect(reverse("command_routing"))
|
||||||
|
|||||||
Reference in New Issue
Block a user