diff --git a/app/settings.py b/app/settings.py index 9e6ceaf..d2ccc74 100644 --- a/app/settings.py +++ b/app/settings.py @@ -116,6 +116,7 @@ TEMPLATES = [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "core.util.django_settings_export.settings_export", + "core.context_processors.settings_hierarchy_nav", ], }, }, diff --git a/app/urls.py b/app/urls.py index 9c0cb05..452f8f1 100644 --- a/app/urls.py +++ b/app/urls.py @@ -17,8 +17,10 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth.views import LogoutView +from django.views.generic import RedirectView from django.urls import include, path from two_factor.urls import urlpatterns as tf_urls +from two_factor.views.profile import ProfileView from core.views import ( ais, @@ -49,21 +51,69 @@ urlpatterns = [ path("__debug__/", include("debug_toolbar.urls")), path("", base.Home.as_view(), name="home"), path("admin/", admin.site.urls), + path( + "account/two_factor/", + RedirectView.as_view(pattern_name="security_2fa", permanent=False), + ), # 2FA login urls path("", include(tf_urls)), path("accounts/signup/", base.Signup.as_view(), name="signup"), path("accounts/logout/", LogoutView.as_view(), name="logout"), # Notifications + path( + "notifications/page/update/", + RedirectView.as_view(pattern_name="notifications_settings", permanent=False), + ), path( "notifications//update/", notifications.NotificationsUpdate.as_view(), name="notifications_update", ), + path( + "settings/notifications/", + notifications.NotificationsUpdate.as_view(), + {"type": "page"}, + name="notifications_settings", + ), path( "settings/security/", - system.SecurityPage.as_view(), + system.SecurityPage.as_view(page_mode="encryption"), 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( "settings/system/", system.SystemSettings.as_view(), @@ -115,10 +165,24 @@ urlpatterns = [ name="command_routing", ), path( - "settings/ai-execution/", + "settings/ai/traces/", automation.AIExecutionLogSettings.as_view(), name="ai_execution_log", ), + path( + "settings/ai/traces/run//", + automation.AIExecutionRunDetailView.as_view(), + name="ai_execution_run_detail", + ), + path( + "settings/ai/traces/run//tab//", + 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( "settings/translation/", automation.TranslationSettings.as_view(), @@ -500,6 +564,20 @@ urlpatterns = [ workspace.AIWorkspaceUpdatePlanMeta.as_view(), 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( "ai//", ais.AIList.as_view(), diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..8871b7b --- /dev/null +++ b/core/context_processors.py @@ -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} diff --git a/core/migrations/0041_useraccessibilitysettings.py b/core/migrations/0041_useraccessibilitysettings.py new file mode 100644 index 0000000..eead8c3 --- /dev/null +++ b/core/migrations/0041_useraccessibilitysettings.py @@ -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, + ), + ), + ], + ), + ] diff --git a/core/models.py b/core/models.py index 7dbd1b0..8ba8a4f 100644 --- a/core/models.py +++ b/core/models.py @@ -2925,6 +2925,17 @@ class UserXmppSecuritySettings(models.Model): 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): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="task_completion_patterns") diff --git a/core/templates/base.html b/core/templates/base.html index ea8a441..ad07418 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,6 +1,7 @@ {% load static %} {% load cache %} {% load page_title %} +{% load accessibility %} @@ -290,10 +291,26 @@ .osint-search-form .button.is-fullwidth { 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; + } - + {% get_accessibility_settings request.user as a11y_settings %} +