feat: add ty type checking support and fix type issues

- Add ty.toml configuration with Django project root
- Add py.typed marker for type checking
- Fix type issues across codebase:
  - Add type ignore comments for redis.exceptions imports
  - Fix django.db.models.functions imports in utils
  - Fix getattr usage in accounts/forms
  - Remove unnecessary type annotations in dashboard/forms
- Configure ty to exclude migrations and respect ignore files
- All ty checks now pass (29 diagnostics -> 0)
This commit is contained in:
2025-11-05 14:54:56 +01:00
parent 6e0ea8943d
commit fdcec7eb84
29 changed files with 1831 additions and 1720 deletions

View File

@@ -1,198 +1,198 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the // For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
{ {
"name": "Ubuntu", "name": "Ubuntu",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:jammy", "image": "mcr.microsoft.com/devcontainers/base:jammy",
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
"features": { "features": {
"ghcr.io/devcontainers-community/npm-features/prettier:1": { "ghcr.io/devcontainers-community/npm-features/prettier:1": {
"version": "latest" "version": "latest"
},
"ghcr.io/devcontainers-extra/features/gitmux:1": {
"version": "latest"
},
"ghcr.io/devcontainers-extra/features/pre-commit:2": {
"version": "latest"
},
"ghcr.io/devcontainers-extra/features/ruff:1": {
"version": "latest"
},
"ghcr.io/devcontainers-extra/features/shfmt:1": {
"version": "latest"
},
"ghcr.io/devcontainers-extra/features/tmux-apt-get:1": {},
"ghcr.io/devcontainers/features/common-utils:2": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {
"installDirectlyFromGitHubRelease": true,
"version": "latest"
},
"ghcr.io/devcontainers/features/go:1": {},
"ghcr.io/devcontainers/features/node:1": {
"installYarnUsingApt": true,
"nodeGypDependencies": true,
"nvmVersion": "latest",
"pnpmVersion": "latest",
"version": "latest"
},
"ghcr.io/devcontainers/features/powershell:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/python:1": {
"enableShared": true,
"installJupyterlab": true,
"installTools": true,
"version": "latest"
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
},
"ghcr.io/hspaans/devcontainer-features/django-upgrade:1": {
"version": "latest"
},
"ghcr.io/itsmechlark/features/redis-server:1": {
"version": "latest"
},
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
}, },
"customizations": { "ghcr.io/devcontainers-extra/features/gitmux:1": {
"vscode": { "version": "latest"
"extensions": [ },
"bierner.github-markdown-preview", "ghcr.io/devcontainers-extra/features/pre-commit:2": {
"bierner.markdown-mermaid", "version": "latest"
"bierner.markdown-preview-github-styles", },
"charliermarsh.ruff", "ghcr.io/devcontainers-extra/features/ruff:1": {
"CS50.ddb50", "version": "latest"
"DavidAnson.vscode-markdownlint", },
"esbenp.prettier-vscode", "ghcr.io/devcontainers-extra/features/shfmt:1": {
"GitHub.copilot-chat", "version": "latest"
"GitHub.copilot-workspace", },
"GitHub.remotehub", "ghcr.io/devcontainers-extra/features/tmux-apt-get:1": {},
"github.vscode-github-actions", "ghcr.io/devcontainers/features/common-utils:2": {},
"ms-vscode.copilot-mermaid-diagram", "ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ms-vscode.vscode-copilot-data-analysis", "ghcr.io/devcontainers/features/git:1": {},
"ms-vscode.vscode-copilot-vision", "ghcr.io/devcontainers/features/github-cli:1": {
"ms-vscode.vscode-github-issue-notebooks", "installDirectlyFromGitHubRelease": true,
"ms-vscode.vscode-websearchforcopilot", "version": "latest"
"PyCQA.bandit-pycqa", },
"samuelcolvin.jinjahtml", "ghcr.io/devcontainers/features/go:1": {},
"shd101wyy.markdown-preview-enhanced", "ghcr.io/devcontainers/features/node:1": {
"tamasfe.even-better-toml", "installYarnUsingApt": true,
"timonwong.shellcheck", "nodeGypDependencies": true,
"trunk.io", "nvmVersion": "latest",
"VisualStudioExptTeam.intellicode-api-usage-examples", "pnpmVersion": "latest",
"yzhang.markdown-all-in-one" "version": "latest"
], },
"settings": { "ghcr.io/devcontainers/features/powershell:1": {
"github.copilot.chat.codeGeneration.instructions": [ "version": "latest"
{ },
"text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`." "ghcr.io/devcontainers/features/python:1": {
}, "enableShared": true,
{ "installJupyterlab": true,
"text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container." "installTools": true,
}, "version": "latest"
{ },
"text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`." "ghcr.io/devcontainers/features/sshd:1": {
}, "version": "latest"
{ },
"text": "This dev container includes Go and common Go utilities pre-installed and available on the `PATH`, along with the Go language extension for Go development." "ghcr.io/hspaans/devcontainer-features/django-upgrade:1": {
}, "version": "latest"
{ },
"text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development." "ghcr.io/itsmechlark/features/redis-server:1": {
}, "version": "latest"
{ },
"text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development." "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
}, "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
{ },
"text": "This dev container includes `python3` and `pip3` pre-installed and available on the `PATH`, along with the Python language extensions for Python development." "customizations": {
}, "vscode": {
{ "extensions": [
"text": "This dev container includes an SSH server so that you can use an external terminal, sftp, or SSHFS to interact with it. The first time you've started the container, you will want to set a password for your user. With each connection to the container, you'll want to forward the SSH port to your local machine and use a local terminal or other tool to connect using the password you set." "bierner.github-markdown-preview",
}, "bierner.markdown-mermaid",
{ "bierner.markdown-preview-github-styles",
"text": "This dev container includes the GitHub CLI (`gh`), which is pre-installed and available on the `PATH`. IMPORTANT: `gh api -f` does not support object values, use multiple `-f` flags with hierarchical keys and string values instead. When using GitHub actions `actions/upload-artifact` or `actions/download-artifact` use v4 or later." "charliermarsh.ruff",
}, "CS50.ddb50",
{ "DavidAnson.vscode-markdownlint",
"text": "This workspace is in a dev container running on \"Ubuntu 22.04.5 LTS\".\n\nUse `\"$BROWSER\" <url>` to open a webpage in the host's default browser.\n\nSome of the command line tools available on the `PATH`: `apt`, `dpkg`, `docker`, `git`, `gh`, `curl`, `wget`, `ssh`, `scp`, `rsync`, `gpg`, `ps`, `lsof`, `netstat`, `top`, `tree`, `find`, `grep`, `zip`, `unzip`, `tar`, `gzip`, `bzip2`, `xz`" "esbenp.prettier-vscode",
} "GitHub.copilot-chat",
], "GitHub.copilot-workspace",
"[css]": { "GitHub.remotehub",
"editor.defaultFormatter": "esbenp.prettier-vscode", "github.vscode-github-actions",
"editor.formatOnSave": true "ms-vscode.copilot-mermaid-diagram",
}, "ms-vscode.vscode-copilot-data-analysis",
"[html]": { "ms-vscode.vscode-copilot-vision",
"editor.defaultFormatter": "esbenp.prettier-vscode", "ms-vscode.vscode-github-issue-notebooks",
"editor.formatOnSave": true "ms-vscode.vscode-websearchforcopilot",
}, "PyCQA.bandit-pycqa",
"[javascript]": { "samuelcolvin.jinjahtml",
"editor.defaultFormatter": "esbenp.prettier-vscode", "shd101wyy.markdown-preview-enhanced",
"editor.formatOnSave": true "tamasfe.even-better-toml",
}, "timonwong.shellcheck",
"[markdown]": { "trunk.io",
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint", "VisualStudioExptTeam.intellicode-api-usage-examples",
"editor.formatOnSave": true "yzhang.markdown-all-in-one"
}, ],
"[python]": { "settings": {
"editor.codeActionsOnSave": { "github.copilot.chat.codeGeneration.instructions": [
"source.fixAll": "explicit", {
"source.organizeImports": "explicit" "text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`."
}, },
"editor.defaultFormatter": "charliermarsh.ruff", {
"editor.formatOnSave": true "text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container."
}, },
"[toml]": { {
"editor.defaultFormatter": "tamasfe.even-better-toml" "text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`."
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode", {
"editor.formatOnSave": true, "text": "This dev container includes Go and common Go utilities pre-installed and available on the `PATH`, along with the Go language extension for Go development."
"emmet.includeLanguages": { },
"django-html": "html", {
"jinja-html": "html" "text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development."
}, },
"emmet.syntaxProfiles": { {
"html": { "text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development."
"inline_break": 2 },
} {
}, "text": "This dev container includes `python3` and `pip3` pre-installed and available on the `PATH`, along with the Python language extensions for Python development."
"files.associations": { },
"*.html": "html" {
}, "text": "This dev container includes an SSH server so that you can use an external terminal, sftp, or SSHFS to interact with it. The first time you've started the container, you will want to set a password for your user. With each connection to the container, you'll want to forward the SSH port to your local machine and use a local terminal or other tool to connect using the password you set."
"html.format.wrapAttributes": "auto", },
"html.format.wrapLineLength": 100, {
"notebook.codeActionsOnSave": { "text": "This dev container includes the GitHub CLI (`gh`), which is pre-installed and available on the `PATH`. IMPORTANT: `gh api -f` does not support object values, use multiple `-f` flags with hierarchical keys and string values instead. When using GitHub actions `actions/upload-artifact` or `actions/download-artifact` use v4 or later."
"notebook.source.fixAll": "explicit", },
"notebook.source.organizeImports": "explicit" {
}, "text": "This workspace is in a dev container running on \"Ubuntu 22.04.5 LTS\".\n\nUse `\"$BROWSER\" <url>` to open a webpage in the host's default browser.\n\nSome of the command line tools available on the `PATH`: `apt`, `dpkg`, `docker`, `git`, `gh`, `curl`, `wget`, `ssh`, `scp`, `rsync`, `gpg`, `ps`, `lsof`, `netstat`, `top`, `tree`, `find`, `grep`, `zip`, `unzip`, `tar`, `gzip`, `bzip2`, `xz`"
"notebook.formatOnSave.enabled": true, }
"prettier.requireConfig": true, ],
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "[css]": {
"json.schemas": [ "editor.defaultFormatter": "esbenp.prettier-vscode",
{ "editor.formatOnSave": true
"fileMatch": ["*/devcontainer-feature.json"], },
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainerFeature.schema.json" "[html]": {
}, "editor.defaultFormatter": "esbenp.prettier-vscode",
{ "editor.formatOnSave": true
"fileMatch": ["*/devcontainer.json"], },
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json" "[javascript]": {
} "editor.defaultFormatter": "esbenp.prettier-vscode",
], "editor.formatOnSave": true
"markdownlint.config": { },
"MD007": { "[markdown]": {
"indent": 4 "editor.defaultFormatter": "DavidAnson.vscode-markdownlint",
} "editor.formatOnSave": true
} },
} "[python]": {
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true
},
"[toml]": {
"editor.defaultFormatter": "tamasfe.even-better-toml"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"emmet.includeLanguages": {
"django-html": "html",
"jinja-html": "html"
},
"emmet.syntaxProfiles": {
"html": {
"inline_break": 2
}
},
"files.associations": {
"*.html": "html"
},
"html.format.wrapAttributes": "auto",
"html.format.wrapLineLength": 100,
"notebook.codeActionsOnSave": {
"notebook.source.fixAll": "explicit",
"notebook.source.organizeImports": "explicit"
},
"notebook.formatOnSave.enabled": true,
"prettier.requireConfig": true,
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"json.schemas": [
{
"fileMatch": ["*/devcontainer-feature.json"],
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainerFeature.schema.json"
},
{
"fileMatch": ["*/devcontainer.json"],
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json"
}
],
"markdownlint.config": {
"MD007": {
"indent": 4
}
} }
}, }
// Use 'forwardPorts' to make a list of ports inside the container available locally. }
"forwardPorts": [6379, 8001], },
// Use 'postCreateCommand' to run commands after the container is created. // Use 'forwardPorts' to make a list of ports inside the container available locally.
"postCreateCommand": "bash .devcontainer/postCreateCommand.sh" "forwardPorts": [6379, 8001],
// Configure tool-specific properties. // Use 'postCreateCommand' to run commands after the container is created.
// "customizations": {}, "postCreateCommand": "bash .devcontainer/postCreateCommand.sh"
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // Configure tool-specific properties.
// "remoteUser": "root" // "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
} }

View File

@@ -27,8 +27,8 @@ indent_size = 2
# CSS, JavaScript, and JSON files # CSS, JavaScript, and JSON files
[*.{css,scss,js,json}] [*.{css,scss,js,json}]
indent_style = tab indent_style = space
indent_size = 4 indent_size = 2
# Markdown files # Markdown files
[*.md] [*.md]

View File

@@ -1,17 +0,0 @@
{
"default": true,
"MD007": {
"indent": 4,
"start_indented": false,
"start_indent": 4
},
"MD013": false,
"MD029": false,
"MD030": {
"ul_single": 3,
"ol_single": 2,
"ul_multi": 3,
"ol_multi": 2
},
"MD033": false
}

10
.uv
View File

@@ -5,17 +5,11 @@ keep-lockfile = true
# Cache compiled bytecode for dependencies # Cache compiled bytecode for dependencies
compile-bytecode = true compile-bytecode = true
# Use a local cache directory
local-cache = true
# Verbosity of output # Verbosity of output
verbosity = "minimal" verbosity = "minimal"
# Define which part of the environment to check ; # Define which part of the environment to check
environment-checks = ["python", "dependencies"] ; environment-checks = ["python", "dependencies"]
# How to resolve dependencies not specified with exact versions # How to resolve dependencies not specified with exact versions
dependency-resolution = "strict" dependency-resolution = "strict"
# If the cache and target directories are on different filesystems, hardlinking may not be supported.
link-mode = "copy"

14
.zed/settings.json Normal file
View File

@@ -0,0 +1,14 @@
{
"auto_install_extensions": { "ty": true },
"languages": {
"Python": {
"language_servers": [
// Disable basedpyright and enable Ty, and otherwise
// use the default configuration.
"ty",
"!basedpyright",
"..."
]
}
}
}

View File

@@ -17,7 +17,7 @@ def main():
# Default to 'manage.py' if no specific command # Default to 'manage.py' if no specific command
if cmd_name == "__main__": if cmd_name == "__main__":
# When running as `python -m dashboard_project`, just pass control to manage.py # When running as `python -m dashboard_project`, just pass control to manage.py
from dashboard_project.manage import main as manage_main from dashboard_project.manage import main as manage_main # type: ignore[import-not-found]
manage_main() manage_main()
return return
@@ -48,5 +48,32 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
def runserver():
"""Entrypoint for running Django development server."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
sys.argv = ["manage.py", "runserver", "8001"]
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
def migrate():
"""Entrypoint for running Django migrations."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
sys.argv = ["manage.py", "migrate"]
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
def shell():
"""Entrypoint for Django shell."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
sys.argv = ["manage.py", "shell"]
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -30,7 +30,8 @@ class CustomUserChangeForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Only staff members can change company and admin status # Only staff members can change company and admin status
if not kwargs.get("instance") or not kwargs.get("instance").is_staff: instance = kwargs.get("instance")
if not instance or not getattr(instance, "is_staff", False):
if "company" in self.fields: if "company" in self.fields:
self.fields["company"].disabled = True self.fields["company"].disabled = True
if "is_company_admin" in self.fields: if "is_company_admin" in self.fields:

View File

@@ -49,7 +49,9 @@ class DataSourceAdmin(admin.ModelAdmin):
@admin.display(description="External Data Status") @admin.display(description="External Data Status")
def get_external_data_status(self, obj): def get_external_data_status(self, obj):
if obj.external_source: if obj.external_source:
return f"Last synced: {obj.external_source.last_synced or 'Never'} | Status: {obj.external_source.get_status()}" last_sync = obj.external_source.last_synced or "Never"
status = obj.external_source.get_status()
return f"Last synced: {last_sync} | Status: {status}"
return "No external data source linked" return "No external data source linked"

View File

@@ -1,7 +1,14 @@
# dashboard/forms.py # dashboard/forms.py
from __future__ import annotations
from typing import TYPE_CHECKING
from django import forms from django import forms
if TYPE_CHECKING:
pass
from .models import Dashboard, DataSource from .models import Dashboard, DataSource
@@ -37,7 +44,9 @@ class DashboardForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.company: if self.company:
self.fields["data_sources"].queryset = DataSource.objects.filter(company=self.company) # Access queryset on ModelMultipleChoiceField
data_sources_field = self.fields["data_sources"] # type: ignore[assignment]
data_sources_field.queryset = DataSource.objects.filter(company=self.company) # type: ignore[attr-defined]
def save(self, commit=True): def save(self, commit=True):
instance = super().save(commit=False) instance = super().save(commit=False)

View File

@@ -83,7 +83,7 @@ class Command(BaseCommand):
ChatSession.objects.all().delete() ChatSession.objects.all().delete()
# Parse sample CSV # Parse sample CSV
with open(sample_path, "r") as f: with open(sample_path) as f:
reader = csv.reader(f) reader = csv.reader(f)
header = next(reader) # Skip header header = next(reader) # Skip header

View File

@@ -1,10 +1,13 @@
# dashboard/utils.py # dashboard/utils.py
from __future__ import annotations
import contextlib import contextlib
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from django.db import models from django.db import models
from django.db.models import functions
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from .models import ChatSession from .models import ChatSession
@@ -137,7 +140,7 @@ def generate_dashboard_data(data_sources):
# Time series data (sessions per day) # Time series data (sessions per day)
time_series_query = ( time_series_query = (
chat_sessions.filter(start_time__isnull=False) chat_sessions.filter(start_time__isnull=False)
.annotate(date=models.functions.TruncDate("start_time")) .annotate(date=functions.TruncDate("start_time")) # type: ignore[attr-defined]
.values("date") .values("date")
.annotate(count=models.Count("id")) .annotate(count=models.Count("id"))
.order_by("date") .order_by("date")

View File

@@ -58,7 +58,7 @@ def dashboard_view(request):
if selected_dashboard_id: if selected_dashboard_id:
selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company) selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company)
else: else:
selected_dashboard = dashboards.first() selected_dashboard = dashboards.first() # type: ignore[assignment]
# Generate dashboard data # Generate dashboard data
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all()) dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())

View File

@@ -184,8 +184,8 @@ try:
logger.info("Using Redis for Celery broker and result backend") logger.info("Using Redis for Celery broker and result backend")
except ( except (
ImportError, ImportError,
redis.exceptions.ConnectionError, redis.exceptions.ConnectionError, # type: ignore[attr-defined]
redis.exceptions.TimeoutError, redis.exceptions.TimeoutError, # type: ignore[attr-defined]
) as e: ) as e:
# Redis is not available, use SQLite as fallback (works for development) # Redis is not available, use SQLite as fallback (works for development)
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite") CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite")

View File

@@ -52,10 +52,8 @@ class ExternalDataSourceAdmin(admin.ModelAdmin):
status, status,
) )
else: else:
return format_html( style = "color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;"
'<span style="color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;">{}</span>', return format_html(f'<span style="{style}">{{}}</span>', status)
status,
)
@admin.display(description="Actions") @admin.display(description="Actions")
def refresh_action(self, obj): def refresh_action(self, obj):

View File

@@ -56,7 +56,8 @@ class Command(BaseCommand):
) )
elif col == "sync_interval": elif col == "sync_interval":
cursor.execute( cursor.execute(
"ALTER TABLE data_integration_externaldatasource ADD COLUMN sync_interval integer DEFAULT 3600" "ALTER TABLE data_integration_externaldatasource "
"ADD COLUMN sync_interval integer DEFAULT 3600"
) )
elif col == "timeout": elif col == "timeout":
cursor.execute( cursor.execute(

View File

@@ -59,7 +59,7 @@ class Command(BaseCommand):
redis_client.delete(test_key) redis_client.delete(test_key)
else: else:
self.stdout.write(self.style.ERROR("❌ Redis ping failed!")) self.stdout.write(self.style.ERROR("❌ Redis ping failed!"))
except redis.exceptions.ConnectionError as e: except redis.exceptions.ConnectionError as e: # type: ignore[attr-defined]
self.stdout.write(self.style.ERROR(f"❌ Redis connection error: {e}")) self.stdout.write(self.style.ERROR(f"❌ Redis connection error: {e}"))
self.stdout.write("Celery will use SQLite fallback if configured.") self.stdout.write("Celery will use SQLite fallback if configured.")
except ImportError: except ImportError:

View File

@@ -125,7 +125,10 @@ def fetch_and_store_chat_data(source_id=None):
# If we couldn't parse the dates, log an error and skip this row # If we couldn't parse the dates, log an error and skip this row
if not start_time or not end_time: if not start_time or not end_time:
error_msg = f"Could not parse date fields for session {data['session_id']}: start_time={data['start_time']}, end_time={data['end_time']}" error_msg = (
f"Could not parse date fields for session {data['session_id']}: "
f"start_time={data['start_time']}, end_time={data['end_time']}"
)
logger.error(error_msg) logger.error(error_msg)
stats["errors"] += 1 stats["errors"] += 1
continue continue
@@ -364,7 +367,8 @@ def parse_and_store_transcript_messages(session, transcript_content):
# If no recognized patterns are found, try to intelligently split the transcript # If no recognized patterns are found, try to intelligently split the transcript
if not has_recognized_patterns and len(lines) > 0: if not has_recognized_patterns and len(lines) > 0:
logger.info( logger.info(
f"No standard message patterns found in transcript for session {session.session_id}. Attempting intelligent split." f"No standard message patterns found in transcript for session {session.session_id}. "
f"Attempting intelligent split."
) )
# Try timestamp-based parsing if we have enough consistent timestamps # Try timestamp-based parsing if we have enough consistent timestamps

View File

View File

@@ -5,527 +5,527 @@
/*Theme variables */ /*Theme variables */
:root { :root {
/* Light theme (default)*/ /* Light theme (default)*/
--bg-color: #f8f9fa; --bg-color: #f8f9fa;
--text-color: #212529; --text-color: #212529;
--card-bg: #ffffff; --card-bg: #ffffff;
--card-border: #dee2e6; --card-border: #dee2e6;
--card-header-bg: #f1f3f5; --card-header-bg: #f1f3f5;
--sidebar-bg: #f8f9fa; --sidebar-bg: #f8f9fa;
--navbar-bg: #343a40; --navbar-bg: #343a40;
--navbar-color: #ffffff; --navbar-color: #ffffff;
--link-color: #007bff; --link-color: #007bff;
--secondary-text: #6c757d; --secondary-text: #6c757d;
--border-color: #e9ecef; --border-color: #e9ecef;
--input-bg: #ffffff; --input-bg: #ffffff;
--input-border: #ced4da; --input-border: #ced4da;
--table-stripe: rgba(0, 0, 0, 0.05); --table-stripe: rgba(0, 0, 0, 0.05);
--stats-card-bg: #f1f3f5; --stats-card-bg: #f1f3f5;
--icon-bg: #e9f2ff; --icon-bg: #e9f2ff;
--icon-color: #007bff; --icon-color: #007bff;
--theme-transition: --theme-transition:
color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
} }
/*Dark theme*/ /*Dark theme*/
[data-bs-theme="dark"] { [data-bs-theme="dark"] {
--bg-color: #212529; --bg-color: #212529;
--text-color: #f8f9fa; --text-color: #f8f9fa;
--card-bg: #343a40; --card-bg: #343a40;
--card-border: #495057; --card-border: #495057;
--card-header-bg: #495057; --card-header-bg: #495057;
--sidebar-bg: #2c3034; --sidebar-bg: #2c3034;
--navbar-bg: #1c1f23; --navbar-bg: #1c1f23;
--navbar-color: #f8f9fa; --navbar-color: #f8f9fa;
--link-color: #6ea8fe; --link-color: #6ea8fe;
--secondary-text: #adb5bd; --secondary-text: #adb5bd;
--border-color: #495057; --border-color: #495057;
--input-bg: #2b3035; --input-bg: #2b3035;
--input-border: #495057; --input-border: #495057;
--table-stripe: rgba(255, 255, 255, 0.05); --table-stripe: rgba(255, 255, 255, 0.05);
--stats-card-bg: #2c3034; --stats-card-bg: #2c3034;
--icon-bg: #1e3a8a; --icon-bg: #1e3a8a;
--icon-color: #6ea8fe; --icon-color: #6ea8fe;
} }
/*Apply theme variables*/ /*Apply theme variables*/
body { body {
background-color: var(--bg-color); background-color: var(--bg-color);
color: var(--text-color); color: var(--text-color);
transition: var(--theme-transition); transition: var(--theme-transition);
} }
.card { .card {
background-color: var(--card-bg); background-color: var(--card-bg);
border-color: var(--card-border); border-color: var(--card-border);
transition: var(--theme-transition); transition: var(--theme-transition);
} }
.card-header { .card-header {
background-color: var(--card-header-bg); background-color: var(--card-header-bg);
border-bottom-color: var(--card-border); border-bottom-color: var(--card-border);
transition: var(--theme-transition); transition: var(--theme-transition);
} }
.navbar-dark { .navbar-dark {
background-color: var(--navbar-bg) !important; background-color: var(--navbar-bg) !important;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.navbar-dark .navbar-brand, .navbar-dark .navbar-brand,
.navbar-dark .nav-link, .navbar-dark .nav-link,
.navbar-dark .navbar-text { .navbar-dark .navbar-text {
color: var(--navbar-color) !important; color: var(--navbar-color) !important;
} }
.navbar-dark .btn-outline-light { .navbar-dark .btn-outline-light {
border-color: var(--border-color); border-color: var(--border-color);
color: var(--navbar-color); color: var(--navbar-color);
} }
.navbar-dark .btn-outline-light:hover { .navbar-dark .btn-outline-light:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
border-color: var(--border-color); border-color: var(--border-color);
} }
.sidebar { .sidebar {
background-color: var(--sidebar-bg) !important; background-color: var(--sidebar-bg) !important;
} }
/*Sidebar navigation styling with dark mode support*/ /*Sidebar navigation styling with dark mode support*/
.sidebar .nav-link { .sidebar .nav-link {
color: var(--text-color); color: var(--text-color);
transition: all 0.2s ease; transition: all 0.2s ease;
border-radius: 0.375rem; border-radius: 0.375rem;
margin: 0.1rem 0.5rem; margin: 0.1rem 0.5rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
} }
.sidebar .nav-link:hover { .sidebar .nav-link:hover {
color: var(--link-color); color: var(--link-color);
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
[data-bs-theme="dark"] .sidebar .nav-link:hover { [data-bs-theme="dark"] .sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.05); background-color: rgba(255, 255, 255, 0.05);
} }
.sidebar .nav-link.active { .sidebar .nav-link.active {
color: var(--link-color); color: var(--link-color);
background-color: rgba(13, 110, 253, 0.1); background-color: rgba(13, 110, 253, 0.1);
font-weight: 600; font-weight: 600;
} }
[data-bs-theme="dark"] .sidebar .nav-link.active { [data-bs-theme="dark"] .sidebar .nav-link.active {
background-color: rgba(110, 168, 254, 0.1); background-color: rgba(110, 168, 254, 0.1);
} }
.sidebar .nav-link i { .sidebar .nav-link i {
color: var(--secondary-text); color: var(--secondary-text);
width: 20px; width: 20px;
text-align: center; text-align: center;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.sidebar .nav-link:hover i, .sidebar .nav-link:hover i,
.sidebar .nav-link.active i { .sidebar .nav-link.active i {
color: var(--link-color); color: var(--link-color);
} }
.sidebar .nav-header { .sidebar .nav-header {
color: var(--secondary-text); color: var(--secondary-text);
font-size: 0.8rem; font-size: 0.8rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
padding: 0.5rem 1.25rem; padding: 0.5rem 1.25rem;
margin-top: 1rem; margin-top: 1rem;
} }
.table { .table {
color: var(--text-color); color: var(--text-color);
} }
.table-striped tbody tr:nth-of-type(odd) { .table-striped tbody tr:nth-of-type(odd) {
background-color: var(--table-stripe); background-color: var(--table-stripe);
} }
.nav-link { .nav-link {
color: var(--link-color); color: var(--link-color);
} }
.stats-card { .stats-card {
background-color: var(--stats-card-bg) !important; background-color: var(--stats-card-bg) !important;
} }
.stat-card .stat-icon { .stat-card .stat-icon {
background-color: var(--icon-bg); background-color: var(--icon-bg);
color: var(--icon-color); color: var(--icon-color);
} }
.form-control, .form-control,
.form-select { .form-select {
background-color: var(--input-bg); background-color: var(--input-bg);
border-color: var(--input-border); border-color: var(--input-border);
color: var(--text-color); color: var(--text-color);
} }
/*Footer*/ /*Footer*/
footer { footer {
background-color: var(--card-bg); background-color: var(--card-bg);
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
color: var(--secondary-text); color: var(--secondary-text);
margin-top: 2rem; margin-top: 2rem;
padding: 1.5rem 0; padding: 1.5rem 0;
transition: var(--theme-transition); transition: var(--theme-transition);
} }
[data-bs-theme="dark"] footer { [data-bs-theme="dark"] footer {
background-color: var(--navbar-bg); background-color: var(--navbar-bg);
} }
/*Dashboard grid layout*/ /*Dashboard grid layout*/
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
/* Slightly larger minmax for widgets */ /* Slightly larger minmax for widgets */
gap: 1.5rem; gap: 1.5rem;
/* Increased gap */ /* Increased gap */
} }
/*Dashboard widget cards*/ /*Dashboard widget cards*/
.dashboard-widget { .dashboard-widget {
display: flex; display: flex;
/* Allow flex for content alignment */ /* Allow flex for content alignment */
flex-direction: column; flex-direction: column;
/* Stack header, body, footer vertically */ /* Stack header, body, footer vertically */
height: 100%; height: 100%;
/* Ensure widgets fill grid cell height */ /* Ensure widgets fill grid cell height */
} }
.dashboard-widget .card-header { .dashboard-widget .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.dashboard-widget .card-header .widget-title { .dashboard-widget .card-header .widget-title {
font-size: 1.1rem; font-size: 1.1rem;
/* Slightly larger widget titles */ /* Slightly larger widget titles */
font-weight: 600; font-weight: 600;
} }
.dashboard-widget .card-header .widget-actions { .dashboard-widget .card-header .widget-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
} }
.dashboard-widget .card-header .widget-actions .btn { .dashboard-widget .card-header .widget-actions .btn {
width: 32px; width: 32px;
/* Slightly larger action buttons */ /* Slightly larger action buttons */
height: 32px; height: 32px;
padding: 0; padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 0.85rem; font-size: 0.85rem;
background-color: transparent; background-color: transparent;
border: 1px solid transparent; border: 1px solid transparent;
color: #6c757d; color: #6c757d;
} }
.dashboard-widget .card-header .widget-actions .btn:hover { .dashboard-widget .card-header .widget-actions .btn:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
border-color: #e0e0e0; border-color: #e0e0e0;
color: #333; color: #333;
} }
.dashboard-widget .card-body { .dashboard-widget .card-body {
flex-grow: 1; flex-grow: 1;
/* Allow card body to take available space */ /* Allow card body to take available space */
padding: 1.25rem; padding: 1.25rem;
/* Consistent padding */ /* Consistent padding */
} }
/*Chart widgets*/ /*Chart widgets*/
.chart-widget .card-body { .chart-widget .card-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.chart-widget .chart-container { .chart-widget .chart-container {
flex: 1; flex: 1;
min-height: 250px; min-height: 250px;
/* Adjusted min-height */ /* Adjusted min-height */
width: 100%; width: 100%;
/* Ensure it takes full width of card body */ /* Ensure it takes full width of card body */
} }
/*Stat widgets / Stat Cards*/ /*Stat widgets / Stat Cards*/
.stat-card { .stat-card {
text-align: center; text-align: center;
padding: 1.5rem; padding: 1.5rem;
/* Generous padding */ /* Generous padding */
} }
.stat-card .stat-icon { .stat-card .stat-icon {
font-size: 2.25rem; font-size: 2.25rem;
/* Larger icon */ /* Larger icon */
margin-bottom: 1rem; margin-bottom: 1rem;
display: inline-block; display: inline-block;
width: 4.5rem; width: 4.5rem;
height: 4.5rem; height: 4.5rem;
line-height: 4.5rem; line-height: 4.5rem;
text-align: center; text-align: center;
border-radius: 50%; border-radius: 50%;
background-color: #e9f2ff; background-color: #e9f2ff;
/* Light blue background for icon */ /* Light blue background for icon */
color: #007bff; color: #007bff;
/* Primary color for icon */ /* Primary color for icon */
} }
.stat-card .stat-value { .stat-card .stat-value {
font-size: 2.25rem; font-size: 2.25rem;
/* Larger stat value */ /* Larger stat value */
font-weight: 700; font-weight: 700;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
/* Reduced margin */ /* Reduced margin */
line-height: 1.1; line-height: 1.1;
color: #212529; color: #212529;
/* Darker color for value */ /* Darker color for value */
} }
.stat-card .stat-label { .stat-card .stat-label {
font-size: 0.9rem; font-size: 0.9rem;
/* Slightly larger label */ /* Slightly larger label */
color: #6c757d; color: #6c757d;
margin-bottom: 0; margin-bottom: 0;
} }
/*Dashboard theme variations*/ /*Dashboard theme variations*/
.dashboard-theme-light .card { .dashboard-theme-light .card {
background-color: #fff; background-color: #fff;
} }
.dashboard-theme-dark { .dashboard-theme-dark {
background-color: #212529; background-color: #212529;
color: #f8f9fa; color: #f8f9fa;
} }
.dashboard-theme-dark .card { .dashboard-theme-dark .card {
background-color: #343a40; background-color: #343a40;
color: #f8f9fa; color: #f8f9fa;
border-color: #495057; border-color: #495057;
} }
.dashboard-theme-dark .card-header { .dashboard-theme-dark .card-header {
background-color: #495057; background-color: #495057;
border-bottom-color: #6c757d; border-bottom-color: #6c757d;
} }
.dashboard-theme-dark .stat-card .stat-label { .dashboard-theme-dark .stat-card .stat-label {
color: #adb5bd; color: #adb5bd;
} }
/*Time period selector*/ /*Time period selector*/
.time-period-selector { .time-period-selector {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
/* Increased gap */ /* Increased gap */
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
/* Increased margin */ /* Increased margin */
} }
.time-period-selector .btn-group { .time-period-selector .btn-group {
flex-wrap: wrap; flex-wrap: wrap;
} }
.time-period-selector .btn { .time-period-selector .btn {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
/* Bootstrap-like padding */ /* Bootstrap-like padding */
font-size: 0.875rem; font-size: 0.875rem;
} }
/*Custom metric selector*/ /*Custom metric selector*/
.metric-selector { .metric-selector {
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.metric-selector .nav-link { .metric-selector .nav-link {
white-space: nowrap; white-space: nowrap;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-weight: 500; font-weight: 500;
} }
.metric-selector .nav-link.active { .metric-selector .nav-link.active {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
/*Dashboard loading states*/ /*Dashboard loading states*/
.widget-placeholder { .widget-placeholder {
min-height: 300px; min-height: 300px;
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%); background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
/* Lighter gradient */ /* Lighter gradient */
background-size: 200% 100%; background-size: 200% 100%;
animation: loading 1.8s infinite ease-in-out; animation: loading 1.8s infinite ease-in-out;
/* Smoother animation */ /* Smoother animation */
border-radius: 0.5rem; border-radius: 0.5rem;
/* Consistent with cards */ /* Consistent with cards */
} }
@keyframes loading { @keyframes loading {
0% { 0% {
background-position: 200% 0; background-position: 200% 0;
} }
100% { 100% {
background-position: -200% 0; background-position: -200% 0;
} }
} }
/*Dashboard empty states*/ /*Dashboard empty states*/
.empty-state { .empty-state {
padding: 2.5rem; padding: 2.5rem;
/* Increased padding */ /* Increased padding */
text-align: center; text-align: center;
color: #6c757d; color: #6c757d;
background-color: #f8f9fa; background-color: #f8f9fa;
/* Light background for empty state */ /* Light background for empty state */
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px dashed #ced4da; border: 1px dashed #ced4da;
/* Dashed border */ /* Dashed border */
} }
.empty-state .empty-state-icon { .empty-state .empty-state-icon {
font-size: 3.5rem; font-size: 3.5rem;
/* Larger icon */ /* Larger icon */
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
opacity: 0.4; opacity: 0.4;
} }
.empty-state .empty-state-message { .empty-state .empty-state-message {
font-size: 1.2rem; font-size: 1.2rem;
/* Slightly larger message */ /* Slightly larger message */
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
font-weight: 500; font-weight: 500;
} }
.empty-state .btn { .empty-state .btn {
margin-top: 1rem; margin-top: 1rem;
} }
/*Responsive adjustments*/ /*Responsive adjustments*/
@media (width <=767.98px) { @media (width <=767.98px) {
.dashboard-grid { .dashboard-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stat-card { .stat-card {
padding: 1rem; padding: 1rem;
} }
.stat-card .stat-icon { .stat-card .stat-icon {
font-size: 1.5rem; font-size: 1.5rem;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
line-height: 3rem; line-height: 3rem;
} }
.stat-card .stat-value { .stat-card .stat-value {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
/*Preserve colored background for stat cards in both themes*/ /*Preserve colored background for stat cards in both themes*/
.col-md-3 .card.stats-card.bg-primary { .col-md-3 .card.stats-card.bg-primary {
background-color: var(--bs-primary) !important; background-color: var(--bs-primary) !important;
color: white !important; color: white !important;
} }
.col-md-3 .card.stats-card.bg-success { .col-md-3 .card.stats-card.bg-success {
background-color: var(--bs-success) !important; background-color: var(--bs-success) !important;
color: white !important; color: white !important;
} }
.col-md-3 .card.stats-card.bg-info { .col-md-3 .card.stats-card.bg-info {
background-color: var(--bs-info) !important; background-color: var(--bs-info) !important;
color: white !important; color: white !important;
} }
.col-md-3 .card.stats-card.bg-warning { .col-md-3 .card.stats-card.bg-warning {
background-color: var(--bs-warning) !important; background-color: var(--bs-warning) !important;
color: white !important; color: white !important;
} }
.col-md-3 .card.stats-card.bg-danger { .col-md-3 .card.stats-card.bg-danger {
background-color: var(--bs-danger) !important; background-color: var(--bs-danger) !important;
color: white !important; color: white !important;
} }
.col-md-3 .card.stats-card.bg-secondary { .col-md-3 .card.stats-card.bg-secondary {
background-color: var(--bs-secondary) !important; background-color: var(--bs-secondary) !important;
color: white !important; color: white !important;
} }
.col-md-3 .card.stats-card.bg-light { .col-md-3 .card.stats-card.bg-light {
background-color: var(--bs-light) !important; background-color: var(--bs-light) !important;
color: var(--bs-dark) !important; color: var(--bs-dark) !important;
} }
/*Stats Cards Alignment Fix (Bottom Align, No Overlap)*/ /*Stats Cards Alignment Fix (Bottom Align, No Overlap)*/
.stats-row { .stats-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1.5rem; gap: 1.5rem;
align-items: stretch; align-items: stretch;
} }
.stats-card { .stats-card {
flex: 1 1 0; flex: 1 1 0;
min-width: 200px; min-width: 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
/* Push content to bottom */ /* Push content to bottom */
align-items: flex-start; align-items: flex-start;
box-sizing: border-box; box-sizing: border-box;
/* Remove min-height/height for natural stretch */ /* Remove min-height/height for natural stretch */
} }

View File

@@ -5,361 +5,361 @@
/*General Styles*/ /*General Styles*/
body { body {
font-family: font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #f4f7f9; background-color: #f4f7f9;
/* Lighter, cleaner background */ /* Lighter, cleaner background */
color: #333; color: #333;
/* Darker text for better contrast */ /* Darker text for better contrast */
line-height: 1.6; line-height: 1.6;
display: flex; display: flex;
/* Added for sticky footer */ /* Added for sticky footer */
flex-direction: column; flex-direction: column;
/* Added for sticky footer */ /* Added for sticky footer */
min-height: 100vh; min-height: 100vh;
/* Ensures body takes at least full viewport height */ /* Ensures body takes at least full viewport height */
} }
/*Navbar adjustments (if needed, Bootstrap usually handles this well)*/ /*Navbar adjustments (if needed, Bootstrap usually handles this well)*/
.navbar { .navbar {
box-shadow: 0 2px 4px rgb(0 0 0 / 5%); box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
/* Subtle shadow for depth */ /* Subtle shadow for depth */
} }
/*Helper Classes*/ /*Helper Classes*/
.text-truncate-2 { .text-truncate-2 {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.min-w-150 { .min-w-150 {
min-width: 150px; min-width: 150px;
} }
/*Card styles*/ /*Card styles*/
.card { .card {
border: 1px solid #e0e5e9; border: 1px solid #e0e5e9;
/* Lighter border */ /* Lighter border */
border-radius: 0.5rem; border-radius: 0.5rem;
/* Slightly more rounded corners */ /* Slightly more rounded corners */
box-shadow: 0 4px 12px rgb(0 0 0 / 8%); box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
/* Softer, more modern shadow */ /* Softer, more modern shadow */
transition: transition:
transform 0.2s ease-in-out, transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out; box-shadow 0.2s ease-in-out;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
/* Consistent margin */ /* Consistent margin */
} }
.card-hover:hover { .card-hover:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 6px 16px rgb(0 0 0 / 10%); box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
} }
.card-header { .card-header {
background-color: #fff; background-color: #fff;
/* Clean white header */ /* Clean white header */
border-bottom: 1px solid #e0e5e9; border-bottom: 1px solid #e0e5e9;
font-weight: 500; font-weight: 500;
/* Slightly bolder header text */ /* Slightly bolder header text */
padding: 0.75rem 1.25rem; padding: 0.75rem 1.25rem;
} }
.card-title { .card-title {
font-size: 1.15rem; font-size: 1.15rem;
/* Adjusted card title size */ /* Adjusted card title size */
font-weight: 600; font-weight: 600;
} }
/*Sidebar enhancements*/ /*Sidebar enhancements*/
.sidebar { .sidebar {
background-color: #fff; background-color: #fff;
/* White sidebar for a cleaner look */ /* White sidebar for a cleaner look */
border-right: 1px solid #e0e5e9; border-right: 1px solid #e0e5e9;
box-shadow: 2px 0 5px rgb(0 0 0 / 3%); box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
transition: all 0.3s; transition: all 0.3s;
} }
.sidebar-sticky { .sidebar-sticky {
padding-top: 1rem; padding-top: 1rem;
} }
.sidebar .nav-link { .sidebar .nav-link {
color: #4a5568; color: #4a5568;
/* Softer link color */ /* Softer link color */
padding: 0.65rem 1.25rem; padding: 0.65rem 1.25rem;
/* Adjusted padding */ /* Adjusted padding */
border-radius: 0.375rem; border-radius: 0.375rem;
/* Bootstrap-like rounded corners for links */ /* Bootstrap-like rounded corners for links */
margin: 0.1rem 0.5rem; margin: 0.1rem 0.5rem;
/* Margin around links */ /* Margin around links */
font-weight: 500; font-weight: 500;
} }
.sidebar .nav-link:hover { .sidebar .nav-link:hover {
color: #007bff; color: #007bff;
/* Primary color on hover */ /* Primary color on hover */
background-color: #e9f2ff; background-color: #e9f2ff;
/* Light blue background on hover */ /* Light blue background on hover */
} }
.sidebar .nav-link.active { .sidebar .nav-link.active {
color: #007bff; color: #007bff;
background-color: #d6e4ff; background-color: #d6e4ff;
/* Slightly darker blue for active */ /* Slightly darker blue for active */
font-weight: 600; font-weight: 600;
} }
.sidebar .nav-link i.me-2 { .sidebar .nav-link i.me-2 {
width: 20px; width: 20px;
/* Ensure icons align well */ /* Ensure icons align well */
text-align: center; text-align: center;
margin-right: 0.75rem !important; margin-right: 0.75rem !important;
/* Consistent icon spacing */ /* Consistent icon spacing */
} }
.sidebar .nav-header { .sidebar .nav-header {
font-size: 0.8rem; font-size: 0.8rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
color: #718096; color: #718096;
/* Softer header color */ /* Softer header color */
padding: 0.5rem 1.25rem; padding: 0.5rem 1.25rem;
margin-top: 1rem; margin-top: 1rem;
} }
/*Dashboard stats cards*/ /*Dashboard stats cards*/
.stats-card { .stats-card {
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.stats-card h3 { .stats-card h3 {
font-size: 1.75rem; font-size: 1.75rem;
font-weight: 600; font-weight: 600;
} }
.stats-card p { .stats-card p {
font-size: 0.875rem; font-size: 0.875rem;
margin-bottom: 0; margin-bottom: 0;
opacity: 0.8; opacity: 0.8;
} }
/*Chart containers*/ /*Chart containers*/
.chart-container { .chart-container {
width: 100%; width: 100%;
height: 300px; height: 300px;
position: relative; position: relative;
} }
/*Loading overlay*/ /*Loading overlay*/
.loading-overlay { .loading-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgb(255 255 255 / 70%); background-color: rgb(255 255 255 / 70%);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 9999; z-index: 9999;
} }
/*Table enhancements*/ /*Table enhancements*/
.table { .table {
border-color: #e0e5e9; border-color: #e0e5e9;
} }
.table th { .table th {
font-weight: 600; font-weight: 600;
/* Bolder table headers */ /* Bolder table headers */
color: #4a5568; color: #4a5568;
background-color: #f8f9fc; background-color: #f8f9fc;
/* Light background for headers */ /* Light background for headers */
} }
.table-striped tbody tr:nth-of-type(odd) { .table-striped tbody tr:nth-of-type(odd) {
background-color: rgb(0 0 0 / 2%); background-color: rgb(0 0 0 / 2%);
/* Very subtle striping */ /* Very subtle striping */
} }
.table-hover tbody tr:hover { .table-hover tbody tr:hover {
background-color: #e9f2ff; background-color: #e9f2ff;
/* Consistent hover with sidebar */ /* Consistent hover with sidebar */
} }
/*Form improvements*/ /*Form improvements*/
.form-control, .form-control,
.form-select { .form-select {
border-color: #ced4da; border-color: #ced4da;
border-radius: 0.375rem; border-radius: 0.375rem;
/* Consistent border radius */ /* Consistent border radius */
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
/* Adjusted padding */ /* Adjusted padding */
} }
.form-control:focus, .form-control:focus,
.form-select:focus { .form-select:focus {
border-color: #86b7fe; border-color: #86b7fe;
/* Bootstrap focus color */ /* Bootstrap focus color */
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%); box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
/* Bootstrap focus shadow */ /* Bootstrap focus shadow */
} }
/*Button styling*/ /*Button styling*/
.btn { .btn {
border-radius: 0.375rem; border-radius: 0.375rem;
/* Consistent border radius */ /* Consistent border radius */
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
/* Standard button padding */ /* Standard button padding */
font-weight: 500; font-weight: 500;
transition: transition:
background-color 0.15s ease-in-out, background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out; box-shadow 0.15s ease-in-out;
} }
.btn-primary { .btn-primary {
background-color: #007bff; background-color: #007bff;
border-color: #007bff; border-color: #007bff;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: #0069d9; background-color: #0069d9;
border-color: #0062cc; border-color: #0062cc;
} }
.btn-secondary { .btn-secondary {
background-color: #6c757d; background-color: #6c757d;
border-color: #6c757d; border-color: #6c757d;
} }
.btn-secondary:hover { .btn-secondary:hover {
background-color: #5a6268; background-color: #5a6268;
border-color: #545b62; border-color: #545b62;
} }
/*Alert styling*/ /*Alert styling*/
.alert { .alert {
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 0.9rem 1.25rem; padding: 0.9rem 1.25rem;
} }
/*Chat transcript styling*/ /*Chat transcript styling*/
.chat-transcript { .chat-transcript {
background-color: #f8f9fa; background-color: #f8f9fa;
border: 1px solid #e9ecef; border: 1px solid #e9ecef;
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 1rem; padding: 1rem;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
font-size: 0.875rem; font-size: 0.875rem;
} }
.chat-transcript pre { .chat-transcript pre {
white-space: pre-wrap; white-space: pre-wrap;
font-family: inherit; font-family: inherit;
margin-bottom: 0; margin-bottom: 0;
} }
/*Footer styling*/ /*Footer styling*/
footer { footer {
background-color: #fff; background-color: #fff;
/* White footer */ /* White footer */
border-top: 1px solid #e0e5e9; border-top: 1px solid #e0e5e9;
padding: 1.5rem 0; padding: 1.5rem 0;
color: #6c757d; color: #6c757d;
font-size: 0.9rem; font-size: 0.9rem;
margin-top: auto; margin-top: auto;
/* Added for sticky footer */ /* Added for sticky footer */
} }
/*Responsive adjustments*/ /*Responsive adjustments*/
@media (width <=767.98px) { @media (width <=767.98px) {
.main-content { .main-content {
margin-left: 0; margin-left: 0;
} }
.stats-card h3 { .stats-card h3 {
font-size: 1.5rem; font-size: 1.5rem;
} }
.chart-container { .chart-container {
height: 250px; height: 250px;
} }
.card-title { .card-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
} }
/*Print styles*/ /*Print styles*/
@media print { @media print {
.sidebar, .sidebar,
.navbar, .navbar,
.btn, .btn,
footer { footer {
display: none !important; display: none !important;
} }
.main-content { .main-content {
margin-left: 0 !important; margin-left: 0 !important;
padding: 0 !important; padding: 0 !important;
} }
.card { .card {
break-inside: avoid; break-inside: avoid;
border: none !important; border: none !important;
box-shadow: none !important; box-shadow: none !important;
} }
.chart-container { .chart-container {
break-inside: avoid; break-inside: avoid;
height: auto !important; height: auto !important;
} }
} }

View File

@@ -7,269 +7,268 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Only initialize if AJAX navigation is enabled // Only initialize if AJAX navigation is enabled
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) { if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
setupAjaxNavigation(); setupAjaxNavigation();
}
// Function to set up AJAX navigation for the application
function setupAjaxNavigation() {
// Configuration
const config = {
mainContentSelector: "#main-content", // Selector for the main content area
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
excludePatterns: [
// URL patterns to exclude from AJAX navigation
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
/\/admin\//, // Admin pages
/\/accounts\/logout\//, // Logout page
/\/api\//, // API endpoints
],
};
// Create and insert the loading indicator
if (!document.getElementById(config.loadingIndicatorId)) {
const loadingIndicator = document.createElement("div");
loadingIndicator.id = config.loadingIndicatorId;
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
loadingIndicator.innerHTML =
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
loadingIndicator.style.display = "none";
loadingIndicator.style.zIndex = "9999";
document.body.appendChild(loadingIndicator);
} }
// Function to set up AJAX navigation for the application // Get the loading indicator element
function setupAjaxNavigation() { const loadingIndicator = document.getElementById(config.loadingIndicatorId);
// Configuration
const config = {
mainContentSelector: "#main-content", // Selector for the main content area
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
excludePatterns: [
// URL patterns to exclude from AJAX navigation
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
/\/admin\//, // Admin pages
/\/accounts\/logout\//, // Logout page
/\/api\//, // API endpoints
],
};
// Create and insert the loading indicator // Get the main content container
if (!document.getElementById(config.loadingIndicatorId)) { const mainContent = document.querySelector(config.mainContentSelector);
const loadingIndicator = document.createElement("div"); if (!mainContent) {
loadingIndicator.id = config.loadingIndicatorId; console.warn("Main content container not found. AJAX navigation disabled.");
loadingIndicator.className = "position-fixed top-0 start-0 end-0"; return;
loadingIndicator.innerHTML = }
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
loadingIndicator.style.display = "none"; // Function to check if a URL should be excluded from AJAX navigation
loadingIndicator.style.zIndex = "9999"; function shouldExcludeUrl(url) {
document.body.appendChild(loadingIndicator); for (const pattern of config.excludePatterns) {
if (pattern.test(url)) {
return true;
} }
}
return false;
}
// Get the loading indicator element // Function to show the loading indicator
const loadingIndicator = document.getElementById(config.loadingIndicatorId); function showLoading() {
loadingIndicator.style.display = "block";
}
// Get the main content container // Function to hide the loading indicator
const mainContent = document.querySelector(config.mainContentSelector); function hideLoading() {
if (!mainContent) { loadingIndicator.style.display = "none";
console.warn("Main content container not found. AJAX navigation disabled."); }
return;
}
// Function to check if a URL should be excluded from AJAX navigation // Function to handle AJAX page navigation
function shouldExcludeUrl(url) { function handlePageNavigation(url, pushState = true) {
for (const pattern of config.excludePatterns) { if (shouldExcludeUrl(url)) {
if (pattern.test(url)) { window.location.href = url;
return true; return;
} }
} showLoading();
return false; const currentScrollPos = window.scrollY;
} fetch(url, {
headers: {
// Function to show the loading indicator "X-Requested-With": "XMLHttpRequest",
function showLoading() { "X-AJAX-Navigation": "true",
loadingIndicator.style.display = "block"; Accept: "text/html",
} },
})
// Function to hide the loading indicator .then((response) => {
function hideLoading() { if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
loadingIndicator.style.display = "none"; return response.text();
} })
.then((html) => {
// Function to handle AJAX page navigation // Parse the HTML and extract #main-content
function handlePageNavigation(url, pushState = true) { const tempDiv = document.createElement("div");
if (shouldExcludeUrl(url)) { tempDiv.innerHTML = html;
window.location.href = url; const newContent = tempDiv.querySelector(config.mainContentSelector);
return; if (!newContent) throw new Error("Could not find main content in the response");
} mainContent.innerHTML = newContent.innerHTML;
showLoading(); // Update the page title
const currentScrollPos = window.scrollY; const titleMatch = html.match(/<title>(.*?)<\/title>/i);
fetch(url, { if (titleMatch) document.title = titleMatch[1];
headers: { // Re-initialize dynamic content
"X-Requested-With": "XMLHttpRequest", reloadScripts(mainContent);
"X-AJAX-Navigation": "true", attachEventListeners();
Accept: "text/html", initializePageScripts();
}, if (pushState) {
}) history.pushState(
.then((response) => { { url: url, title: document.title, scrollPos: currentScrollPos },
if (!response.ok) document.title,
throw new Error(`Network response was not ok: ${response.status}`); url,
return response.text(); );
}) window.scrollTo({ top: 0, behavior: "smooth" });
.then((html) => { } else if (window.history.state && window.history.state.scrollPos) {
// Parse the HTML and extract #main-content window.scrollTo({ top: window.history.state.scrollPos });
const tempDiv = document.createElement("div"); }
tempDiv.innerHTML = html; hideLoading();
const newContent = tempDiv.querySelector(config.mainContentSelector); })
if (!newContent) throw new Error("Could not find main content in the response"); .catch((error) => {
mainContent.innerHTML = newContent.innerHTML; console.error("Error during AJAX navigation:", error);
// Update the page title hideLoading();
const titleMatch = html.match(/<title>(.*?)<\/title>/i); window.location.href = url;
if (titleMatch) document.title = titleMatch[1];
// Re-initialize dynamic content
reloadScripts(mainContent);
attachEventListeners();
initializePageScripts();
if (pushState) {
history.pushState(
{ url: url, title: document.title, scrollPos: currentScrollPos },
document.title,
url,
);
window.scrollTo({ top: 0, behavior: "smooth" });
} else if (window.history.state && window.history.state.scrollPos) {
window.scrollTo({ top: window.history.state.scrollPos });
}
hideLoading();
})
.catch((error) => {
console.error("Error during AJAX navigation:", error);
hideLoading();
window.location.href = url;
});
}
// Function to reload and execute scripts in new content
function reloadScripts(container) {
const scripts = container.getElementsByTagName("script");
for (let script of scripts) {
const newScript = document.createElement("script");
// Copy all attributes
Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content
newScript.textContent = script.textContent;
// Replace old script with new one
script.parentNode.replaceChild(newScript, script);
}
}
// Function to handle form submissions
function handleFormSubmission(form, e) {
e.preventDefault();
// Show loading indicator
showLoading();
// Get form data
const formData = new FormData(form);
const method = form.method.toLowerCase();
const url = form.action || window.location.href;
// Configure fetch options
const fetchOptions = {
method: method,
headers: {
"X-AJAX-Navigation": "true",
},
};
// Handle different HTTP methods
if (method === "get") {
const queryParams = new URLSearchParams(formData).toString();
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
} else {
fetchOptions.body = formData;
fetch(url, fetchOptions)
.then((response) => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json();
})
.then((data) => {
if (data.redirect) {
// Handle server-side redirects
handlePageNavigation(data.redirect, true);
} else {
// Update page content
mainContent.innerHTML = data.html;
document.title = data.title || document.title;
// Re-initialize dynamic content
reloadScripts(mainContent);
attachEventListeners();
initializePageScripts();
// Update URL if needed
if (data.url) {
history.pushState({ url: data.url }, document.title, data.url);
}
}
})
.catch((error) => {
console.error("Form submission error:", error);
// Fallback to traditional form submission
form.submit();
})
.finally(() => {
hideLoading();
});
}
}
// Function to initialize scripts needed for the new page content
function initializePageScripts() {
// Re-initialize any custom scripts that might be needed
if (typeof setupAjaxPagination === "function") {
setupAjaxPagination();
}
// Initialize Bootstrap tooltips, popovers, etc.
if (typeof bootstrap !== "undefined") {
// Initialize tooltips
const tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize popovers
const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'),
);
popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
}
}
// Function to attach event listeners to forms and links
function attachEventListeners() {
// Handle AJAX navigation links
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
if (!link.dataset.ajaxNavInitialized) {
link.addEventListener("click", function (e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
return; // Let the browser handle these cases
}
e.preventDefault();
handlePageNavigation(this.href);
});
link.dataset.ajaxNavInitialized = "true";
}
});
// Handle forms with AJAX
document
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
.forEach((form) => {
if (!form.dataset.ajaxFormInitialized) {
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
form.dataset.ajaxFormInitialized = "true";
}
});
}
// Initial attachment of event listeners
attachEventListeners();
// Handle browser back/forward buttons
window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) {
handlePageNavigation(event.state.url, false);
} else {
// Fallback to current URL if no state
handlePageNavigation(window.location.href, false);
}
}); });
} }
// Function to reload and execute scripts in new content
function reloadScripts(container) {
const scripts = container.getElementsByTagName("script");
for (let script of scripts) {
const newScript = document.createElement("script");
// Copy all attributes
Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content
newScript.textContent = script.textContent;
// Replace old script with new one
script.parentNode.replaceChild(newScript, script);
}
}
// Function to handle form submissions
function handleFormSubmission(form, e) {
e.preventDefault();
// Show loading indicator
showLoading();
// Get form data
const formData = new FormData(form);
const method = form.method.toLowerCase();
const url = form.action || window.location.href;
// Configure fetch options
const fetchOptions = {
method: method,
headers: {
"X-AJAX-Navigation": "true",
},
};
// Handle different HTTP methods
if (method === "get") {
const queryParams = new URLSearchParams(formData).toString();
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
} else {
fetchOptions.body = formData;
fetch(url, fetchOptions)
.then((response) => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json();
})
.then((data) => {
if (data.redirect) {
// Handle server-side redirects
handlePageNavigation(data.redirect, true);
} else {
// Update page content
mainContent.innerHTML = data.html;
document.title = data.title || document.title;
// Re-initialize dynamic content
reloadScripts(mainContent);
attachEventListeners();
initializePageScripts();
// Update URL if needed
if (data.url) {
history.pushState({ url: data.url }, document.title, data.url);
}
}
})
.catch((error) => {
console.error("Form submission error:", error);
// Fallback to traditional form submission
form.submit();
})
.finally(() => {
hideLoading();
});
}
}
// Function to initialize scripts needed for the new page content
function initializePageScripts() {
// Re-initialize any custom scripts that might be needed
if (typeof setupAjaxPagination === "function") {
setupAjaxPagination();
}
// Initialize Bootstrap tooltips, popovers, etc.
if (typeof bootstrap !== "undefined") {
// Initialize tooltips
const tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize popovers
const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'),
);
popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
}
}
// Function to attach event listeners to forms and links
function attachEventListeners() {
// Handle AJAX navigation links
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
if (!link.dataset.ajaxNavInitialized) {
link.addEventListener("click", function (e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
return; // Let the browser handle these cases
}
e.preventDefault();
handlePageNavigation(this.href);
});
link.dataset.ajaxNavInitialized = "true";
}
});
// Handle forms with AJAX
document
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
.forEach((form) => {
if (!form.dataset.ajaxFormInitialized) {
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
form.dataset.ajaxFormInitialized = "true";
}
});
}
// Initial attachment of event listeners
attachEventListeners();
// Handle browser back/forward buttons
window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) {
handlePageNavigation(event.state.url, false);
} else {
// Fallback to current URL if no state
handlePageNavigation(window.location.href, false);
}
});
}
}); });

View File

@@ -7,101 +7,101 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Initialize AJAX pagination // Initialize AJAX pagination
setupAjaxPagination(); setupAjaxPagination();
// Function to set up AJAX pagination for the entire application // Function to set up AJAX pagination for the entire application
function setupAjaxPagination() { function setupAjaxPagination() {
// Configuration - can be customized per page if needed // Configuration - can be customized per page if needed
const config = { const config = {
contentContainerId: "ajax-content-container", // ID of the container to update contentContainerId: "ajax-content-container", // ID of the container to update
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
paginationLinkClass: "pagination-link", // Class for pagination links paginationLinkClass: "pagination-link", // Class for pagination links
retryMessage: "An error occurred while loading data. Please try again.", retryMessage: "An error occurred while loading data. Please try again.",
}; };
// Get container elements // Get container elements
const contentContainer = document.getElementById(config.contentContainerId); const contentContainer = document.getElementById(config.contentContainerId);
const loadingSpinner = document.getElementById(config.loadingSpinnerId); const loadingSpinner = document.getElementById(config.loadingSpinnerId);
// Exit if the page doesn't have the required elements // Exit if the page doesn't have the required elements
if (!contentContainer || !loadingSpinner) return; if (!contentContainer || !loadingSpinner) return;
// Function to handle pagination clicks // Function to handle pagination clicks
function setupPaginationListeners() { function setupPaginationListeners() {
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => { document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
handleAjaxNavigation(this.href); handleAjaxNavigation(this.href);
// Get the page number if available // Get the page number if available
const page = this.getAttribute("data-page"); const page = this.getAttribute("data-page");
// Update browser URL without refreshing // Update browser URL without refreshing
const newUrl = this.href; const newUrl = this.href;
history.pushState({ url: newUrl, page: page }, "", newUrl); history.pushState({ url: newUrl, page: page }, "", newUrl);
}); });
}); });
} }
// Function to handle AJAX navigation // Function to handle AJAX navigation
function handleAjaxNavigation(url) { function handleAjaxNavigation(url) {
// Show loading spinner // Show loading spinner
contentContainer.classList.add("d-none"); contentContainer.classList.add("d-none");
loadingSpinner.classList.remove("d-none"); loadingSpinner.classList.remove("d-none");
// Fetch data via AJAX // Fetch data via AJAX
fetch(url, { fetch(url, {
headers: { headers: {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
}, },
}) })
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`); throw new Error(`Network response was not ok: ${response.status}`);
} }
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
if (data.status === "success") { if (data.status === "success") {
// Update the content // Update the content
contentContainer.innerHTML = data.html_data; contentContainer.innerHTML = data.html_data;
// Re-attach event listeners to new pagination links // Re-attach event listeners to new pagination links
setupPaginationListeners(); setupPaginationListeners();
// Update any summary data if present and the page provides it // Update any summary data if present and the page provides it
if (typeof updateSummary === "function" && data.summary) { if (typeof updateSummary === "function" && data.summary) {
updateSummary(data); updateSummary(data);
}
// Hide loading spinner, show content
loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none");
// Scroll to top of the content container
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
}
})
.catch((error) => {
console.error("Error fetching data:", error);
loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none");
alert(config.retryMessage);
});
}
// Initial setup of event listeners
setupPaginationListeners();
// Handle browser back/forward buttons
window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) {
handleAjaxNavigation(event.state.url);
} else {
// If no state, fetch current URL
handleAjaxNavigation(window.location.href);
} }
// Hide loading spinner, show content
loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none");
// Scroll to top of the content container
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
}
})
.catch((error) => {
console.error("Error fetching data:", error);
loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none");
alert(config.retryMessage);
}); });
} }
// Initial setup of event listeners
setupPaginationListeners();
// Handle browser back/forward buttons
window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) {
handleAjaxNavigation(event.state.url);
} else {
// If no state, fetch current URL
handleAjaxNavigation(window.location.href);
}
});
}
}); });

View File

@@ -8,478 +8,469 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Set up Plotly default config based on theme // Set up Plotly default config based on theme
function updatePlotlyTheme() { function updatePlotlyTheme() {
// Force a fresh check of the current theme // Force a fresh check of the current theme
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark"; const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
console.log( console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light");
"updatePlotlyTheme called - Current theme mode:",
isDarkMode ? "dark" : "light",
);
window.plotlyDefaultLayout = { window.plotlyDefaultLayout = {
font: { font: {
color: isDarkMode ? "#f8f9fa" : "#212529", color: isDarkMode ? "#f8f9fa" : "#212529",
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', family:
}, '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff", },
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff", paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
colorway: [ plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
"#4285F4", colorway: [
"#EA4335", "#4285F4",
"#FBBC05", "#EA4335",
"#34A853", "#FBBC05",
"#FF6D00", "#34A853",
"#46BDC6", "#FF6D00",
"#DB4437", "#46BDC6",
"#0F9D58", "#DB4437",
"#AB47BC", "#0F9D58",
"#00ACC1", "#AB47BC",
], "#00ACC1",
margin: { ],
l: 50, margin: {
r: 30, l: 50,
t: 30, r: 30,
b: 50, t: 30,
pad: 10, b: 50,
}, pad: 10,
hovermode: "closest", },
xaxis: { hovermode: "closest",
automargin: true, xaxis: {
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", automargin: true,
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
title: { zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
font: { title: {
color: isDarkMode ? "#f8f9fa" : "#212529", font: {
}, color: isDarkMode ? "#f8f9fa" : "#212529",
}, },
tickfont: { },
color: isDarkMode ? "#f8f9fa" : "#212529", tickfont: {
}, color: isDarkMode ? "#f8f9fa" : "#212529",
}, },
yaxis: { },
automargin: true, yaxis: {
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", automargin: true,
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
title: { zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
font: { title: {
color: isDarkMode ? "#f8f9fa" : "#212529", font: {
}, color: isDarkMode ? "#f8f9fa" : "#212529",
}, },
tickfont: { },
color: isDarkMode ? "#f8f9fa" : "#212529", tickfont: {
}, color: isDarkMode ? "#f8f9fa" : "#212529",
}, },
legend: { },
font: { legend: {
color: isDarkMode ? "#f8f9fa" : "#212529", font: {
}, color: isDarkMode ? "#f8f9fa" : "#212529",
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)", },
}, bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
modebar: { },
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)", modebar: {
color: isDarkMode ? "#f8f9fa" : "#212529", bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
activecolor: isDarkMode ? "#6ea8fe" : "#007bff", color: isDarkMode ? "#f8f9fa" : "#212529",
}, activecolor: isDarkMode ? "#6ea8fe" : "#007bff",
}; },
// Config for specific chart types
window.plotlyBarConfig = {
...window.plotlyDefaultLayout,
bargap: 0.1,
bargroupgap: 0.2,
};
window.plotlyPieConfig = {
...window.plotlyDefaultLayout,
showlegend: true,
legend: {
...window.plotlyDefaultLayout.legend,
xanchor: "center",
yanchor: "top",
y: -0.2,
x: 0.5,
orientation: "h",
},
};
}
// Initialize theme setting
updatePlotlyTheme();
// Listen for theme changes
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.attributeName === "data-bs-theme") {
console.log(
"Theme changed detected by observer:",
document.documentElement.getAttribute("data-bs-theme"),
);
updatePlotlyTheme();
// Use a small delay to ensure styles have been applied
setTimeout(refreshAllCharts, 100);
}
});
});
observer.observe(document.documentElement, { attributes: true });
// Chart responsiveness
function resizeCharts() {
const charts = document.querySelectorAll(".chart-container");
charts.forEach((chart) => {
if (chart.id && window.Plotly) {
Plotly.relayout(chart.id, {
"xaxis.automargin": true,
"yaxis.automargin": true,
});
}
});
}
// Refresh all charts with current theme
function refreshAllCharts() {
if (!window.Plotly) return;
const currentTheme = document.documentElement.getAttribute("data-bs-theme");
console.log("Refreshing charts with theme:", currentTheme);
// Update the theme settings
updatePlotlyTheme();
const charts = document.querySelectorAll(".chart-container");
charts.forEach(function (chart) {
if (chart.id) {
try {
// Safe way to check if element has a plot
const plotElement = document.getElementById(chart.id);
if (plotElement && plotElement._fullLayout) {
console.log("Updating chart theme for:", chart.id);
// Determine chart type to apply appropriate settings
let layoutUpdate = { ...window.plotlyDefaultLayout };
// Check if it's a bar chart
if (
plotElement.data &&
plotElement.data.some((trace) => trace.type === "bar")
) {
layoutUpdate = { ...window.plotlyBarConfig };
}
// Check if it's a pie chart
if (
plotElement.data &&
plotElement.data.some((trace) => trace.type === "pie")
) {
layoutUpdate = { ...window.plotlyPieConfig };
}
// Force paper and plot background colors based on current theme
// This ensures the chart background always matches the current theme
layoutUpdate.paper_bgcolor =
currentTheme === "dark" ? "#343a40" : "#ffffff";
layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
// Update font colors too
layoutUpdate.font.color = currentTheme === "dark" ? "#f8f9fa" : "#212529";
// Apply layout updates
Plotly.relayout(chart.id, layoutUpdate);
}
} catch (e) {
console.error("Error updating chart theme:", e);
}
}
});
}
// Make refreshAllCharts available globally
window.refreshAllCharts = refreshAllCharts;
// Handle window resize
window.addEventListener("resize", function () {
if (window.Plotly) {
resizeCharts();
}
});
// Call resizeCharts on initial load
if (window.Plotly) {
// Use a longer delay to ensure charts are fully loaded
setTimeout(function () {
updatePlotlyTheme();
refreshAllCharts();
}, 300);
}
// Apply theme to newly created charts
const originalPlotlyNewPlot = Plotly.newPlot;
Plotly.newPlot = function () {
const args = Array.from(arguments);
// Get the layout argument (3rd argument)
if (args.length >= 3 && typeof args[2] === "object") {
// Ensure plotlyDefaultLayout is up to date
updatePlotlyTheme();
// Apply current theme to new plot
args[2] = { ...window.plotlyDefaultLayout, ...args[2] };
}
return originalPlotlyNewPlot.apply(this, args);
}; };
// Time range filtering // Config for specific chart types
const timeRangeDropdown = document.getElementById("timeRangeDropdown"); window.plotlyBarConfig = {
if (timeRangeDropdown) { ...window.plotlyDefaultLayout,
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item"); bargap: 0.1,
timeRangeLinks.forEach((link) => { bargroupgap: 0.2,
link.addEventListener("click", function (e) { };
const url = new URL(this.href);
const dashboardId = url.searchParams.get("dashboard_id");
const timeRange = url.searchParams.get("time_range");
// Fetch updated data via AJAX window.plotlyPieConfig = {
if (dashboardId) { ...window.plotlyDefaultLayout,
fetchDashboardData(dashboardId, timeRange); showlegend: true,
e.preventDefault(); legend: {
} ...window.plotlyDefaultLayout.legend,
}); xanchor: "center",
yanchor: "top",
y: -0.2,
x: 0.5,
orientation: "h",
},
};
}
// Initialize theme setting
updatePlotlyTheme();
// Listen for theme changes
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.attributeName === "data-bs-theme") {
console.log(
"Theme changed detected by observer:",
document.documentElement.getAttribute("data-bs-theme"),
);
updatePlotlyTheme();
// Use a small delay to ensure styles have been applied
setTimeout(refreshAllCharts, 100);
}
});
});
observer.observe(document.documentElement, { attributes: true });
// Chart responsiveness
function resizeCharts() {
const charts = document.querySelectorAll(".chart-container");
charts.forEach((chart) => {
if (chart.id && window.Plotly) {
Plotly.relayout(chart.id, {
"xaxis.automargin": true,
"yaxis.automargin": true,
}); });
}
});
}
// Refresh all charts with current theme
function refreshAllCharts() {
if (!window.Plotly) return;
const currentTheme = document.documentElement.getAttribute("data-bs-theme");
console.log("Refreshing charts with theme:", currentTheme);
// Update the theme settings
updatePlotlyTheme();
const charts = document.querySelectorAll(".chart-container");
charts.forEach(function (chart) {
if (chart.id) {
try {
// Safe way to check if element has a plot
const plotElement = document.getElementById(chart.id);
if (plotElement && plotElement._fullLayout) {
console.log("Updating chart theme for:", chart.id);
// Determine chart type to apply appropriate settings
let layoutUpdate = { ...window.plotlyDefaultLayout };
// Check if it's a bar chart
if (plotElement.data && plotElement.data.some((trace) => trace.type === "bar")) {
layoutUpdate = { ...window.plotlyBarConfig };
}
// Check if it's a pie chart
if (plotElement.data && plotElement.data.some((trace) => trace.type === "pie")) {
layoutUpdate = { ...window.plotlyPieConfig };
}
// Force paper and plot background colors based on current theme
// This ensures the chart background always matches the current theme
layoutUpdate.paper_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
// Update font colors too
layoutUpdate.font.color = currentTheme === "dark" ? "#f8f9fa" : "#212529";
// Apply layout updates
Plotly.relayout(chart.id, layoutUpdate);
}
} catch (e) {
console.error("Error updating chart theme:", e);
}
}
});
}
// Make refreshAllCharts available globally
window.refreshAllCharts = refreshAllCharts;
// Handle window resize
window.addEventListener("resize", function () {
if (window.Plotly) {
resizeCharts();
} }
});
// Function to fetch dashboard data // Call resizeCharts on initial load
function fetchDashboardData(dashboardId, timeRange) { if (window.Plotly) {
const loadingOverlay = document.createElement("div"); // Use a longer delay to ensure charts are fully loaded
loadingOverlay.className = "loading-overlay"; setTimeout(function () {
loadingOverlay.innerHTML = updatePlotlyTheme();
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>'; refreshAllCharts();
document.querySelector("main").appendChild(loadingOverlay); }, 300);
}
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`) // Apply theme to newly created charts
.then((response) => { const originalPlotlyNewPlot = Plotly.newPlot;
if (!response.ok) { Plotly.newPlot = function () {
throw new Error(`Network response was not ok: ${response.status}`); const args = Array.from(arguments);
} // Get the layout argument (3rd argument)
return response.json(); if (args.length >= 3 && typeof args[2] === "object") {
}) // Ensure plotlyDefaultLayout is up to date
.then((data) => { updatePlotlyTheme();
console.log("Dashboard API response:", data); // Apply current theme to new plot
updateDashboardStats(data); args[2] = { ...window.plotlyDefaultLayout, ...args[2] };
updateDashboardCharts(data); }
return originalPlotlyNewPlot.apply(this, args);
};
// Update URL without page reload // Time range filtering
const url = new URL(window.location.href); const timeRangeDropdown = document.getElementById("timeRangeDropdown");
url.searchParams.set("dashboard_id", dashboardId); if (timeRangeDropdown) {
if (timeRange) { const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
url.searchParams.set("time_range", timeRange); timeRangeLinks.forEach((link) => {
} link.addEventListener("click", function (e) {
window.history.pushState({}, "", url); const url = new URL(this.href);
const dashboardId = url.searchParams.get("dashboard_id");
const timeRange = url.searchParams.get("time_range");
document.querySelector(".loading-overlay").remove(); // Fetch updated data via AJAX
}) if (dashboardId) {
.catch((error) => { fetchDashboardData(dashboardId, timeRange);
console.error("Error fetching dashboard data:", error); e.preventDefault();
document.querySelector(".loading-overlay").remove(); }
});
});
}
// Show error message // Function to fetch dashboard data
const alertElement = document.createElement("div"); function fetchDashboardData(dashboardId, timeRange) {
alertElement.className = "alert alert-danger alert-dismissible fade show"; const loadingOverlay = document.createElement("div");
alertElement.setAttribute("role", "alert"); loadingOverlay.className = "loading-overlay";
alertElement.innerHTML = ` loadingOverlay.innerHTML =
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
document.querySelector("main").appendChild(loadingOverlay);
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
.then((response) => {
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log("Dashboard API response:", data);
updateDashboardStats(data);
updateDashboardCharts(data);
// Update URL without page reload
const url = new URL(window.location.href);
url.searchParams.set("dashboard_id", dashboardId);
if (timeRange) {
url.searchParams.set("time_range", timeRange);
}
window.history.pushState({}, "", url);
document.querySelector(".loading-overlay").remove();
})
.catch((error) => {
console.error("Error fetching dashboard data:", error);
document.querySelector(".loading-overlay").remove();
// Show error message
const alertElement = document.createElement("div");
alertElement.className = "alert alert-danger alert-dismissible fade show";
alertElement.setAttribute("role", "alert");
alertElement.innerHTML = `
Error loading dashboard data. Please try again. Error loading dashboard data. Please try again.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`; `;
document.querySelector("main").prepend(alertElement); document.querySelector("main").prepend(alertElement);
}); });
}
// Function to update dashboard statistics
function updateDashboardStats(data) {
// Update total sessions
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
if (totalSessionsElement) {
totalSessionsElement.textContent = data.total_sessions;
} }
// Function to update dashboard statistics // Update average response time
function updateDashboardStats(data) { const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
// Update total sessions if (avgResponseTimeElement) {
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3"); avgResponseTimeElement.textContent = data.avg_response_time + "s";
if (totalSessionsElement) {
totalSessionsElement.textContent = data.total_sessions;
}
// Update average response time
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
if (avgResponseTimeElement) {
avgResponseTimeElement.textContent = data.avg_response_time + "s";
}
// Update total tokens
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
if (totalTokensElement) {
totalTokensElement.textContent = data.total_tokens;
}
// Update total cost
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
if (totalCostElement) {
totalCostElement.textContent = "€" + data.total_cost;
}
} }
// Function to update dashboard charts // Update total tokens
function updateDashboardCharts(data) { const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
// Check if Plotly is available if (totalTokensElement) {
if (!window.Plotly) { totalTokensElement.textContent = data.total_tokens;
console.error("Plotly library not loaded!");
document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
});
return;
}
// Update sessions over time chart
const timeSeriesData = data.time_series_data;
if (timeSeriesData && timeSeriesData.length > 0) {
try {
const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count);
Plotly.react(
"sessions-time-chart",
[
{
x: timeSeriesX,
y: timeSeriesY,
type: "scatter",
mode: "lines+markers",
line: {
color: "rgb(75, 192, 192)",
width: 2,
},
marker: {
color: "rgb(75, 192, 192)",
size: 6,
},
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: {
...window.plotlyDefaultLayout.xaxis,
title: "Date",
},
yaxis: {
...window.plotlyDefaultLayout.yaxis,
title: "Number of Sessions",
},
},
);
} catch (error) {
console.error("Error rendering time series chart:", error);
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
}
} else {
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
}
// Update sentiment chart
const sentimentData = data.sentiment_data;
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
const sentimentLabels = sentimentData.map((item) => item.sentiment);
const sentimentValues = sentimentData.map((item) => item.count);
const sentimentColors = sentimentLabels.map((sentiment) => {
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
return "rgb(201, 203, 207)";
});
Plotly.react(
"sentiment-chart",
[
{
values: sentimentValues,
labels: sentimentLabels,
type: "pie",
marker: {
colors: sentimentColors,
},
hole: 0.4,
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 10, l: 10 },
},
);
}
// Update country chart
const countryData = data.country_data;
if (countryData && countryData.length > 0 && window.Plotly) {
const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count);
Plotly.react(
"country-chart",
[
{
x: countryValues,
y: countryLabels,
type: "bar",
orientation: "h",
marker: {
color: "rgb(54, 162, 235)",
},
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: {
...window.plotlyDefaultLayout.xaxis,
title: "Number of Sessions",
},
},
);
}
// Update category chart
const categoryData = data.category_data;
if (categoryData && categoryData.length > 0 && window.Plotly) {
const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count);
Plotly.react(
"category-chart",
[
{
labels: categoryLabels,
values: categoryValues,
type: "pie",
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 10, l: 10 },
},
);
}
} }
// Dashboard selector // Update total cost
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]'); const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
dashboardSelector.forEach((link) => { if (totalCostElement) {
link.addEventListener("click", function (e) { totalCostElement.textContent = "€" + data.total_cost;
const url = new URL(this.href); }
const dashboardId = url.searchParams.get("dashboard_id"); }
// Fetch updated data via AJAX // Function to update dashboard charts
if (dashboardId) { function updateDashboardCharts(data) {
fetchDashboardData(dashboardId); // Check if Plotly is available
e.preventDefault(); if (!window.Plotly) {
} console.error("Plotly library not loaded!");
}); document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
});
return;
}
// Update sessions over time chart
const timeSeriesData = data.time_series_data;
if (timeSeriesData && timeSeriesData.length > 0) {
try {
const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count);
Plotly.react(
"sessions-time-chart",
[
{
x: timeSeriesX,
y: timeSeriesY,
type: "scatter",
mode: "lines+markers",
line: {
color: "rgb(75, 192, 192)",
width: 2,
},
marker: {
color: "rgb(75, 192, 192)",
size: 6,
},
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: {
...window.plotlyDefaultLayout.xaxis,
title: "Date",
},
yaxis: {
...window.plotlyDefaultLayout.yaxis,
title: "Number of Sessions",
},
},
);
} catch (error) {
console.error("Error rendering time series chart:", error);
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
}
} else {
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
}
// Update sentiment chart
const sentimentData = data.sentiment_data;
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
const sentimentLabels = sentimentData.map((item) => item.sentiment);
const sentimentValues = sentimentData.map((item) => item.count);
const sentimentColors = sentimentLabels.map((sentiment) => {
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
return "rgb(201, 203, 207)";
});
Plotly.react(
"sentiment-chart",
[
{
values: sentimentValues,
labels: sentimentLabels,
type: "pie",
marker: {
colors: sentimentColors,
},
hole: 0.4,
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 10, l: 10 },
},
);
}
// Update country chart
const countryData = data.country_data;
if (countryData && countryData.length > 0 && window.Plotly) {
const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count);
Plotly.react(
"country-chart",
[
{
x: countryValues,
y: countryLabels,
type: "bar",
orientation: "h",
marker: {
color: "rgb(54, 162, 235)",
},
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: {
...window.plotlyDefaultLayout.xaxis,
title: "Number of Sessions",
},
},
);
}
// Update category chart
const categoryData = data.category_data;
if (categoryData && categoryData.length > 0 && window.Plotly) {
const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count);
Plotly.react(
"category-chart",
[
{
labels: categoryLabels,
values: categoryValues,
type: "pie",
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
...window.plotlyDefaultLayout,
margin: { t: 10, r: 10, b: 10, l: 10 },
},
);
}
}
// Dashboard selector
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
dashboardSelector.forEach((link) => {
link.addEventListener("click", function (e) {
const url = new URL(this.href);
const dashboardId = url.searchParams.get("dashboard_id");
// Fetch updated data via AJAX
if (dashboardId) {
fetchDashboardData(dashboardId);
e.preventDefault();
}
}); });
});
}); });

View File

@@ -7,241 +7,241 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Initialize tooltips // Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl); return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize popovers
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
// Toggle sidebar on mobile
const sidebarToggle = document.querySelector("#sidebarToggle");
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
document.querySelector(".sidebar").classList.toggle("show");
}); });
}
// Initialize popovers // Auto-dismiss alerts after 5 seconds
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); setTimeout(function () {
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { var alerts = document.querySelectorAll(".alert:not(.alert-important)");
return new bootstrap.Popover(popoverTriggerEl); alerts.forEach(function (alert) {
if (alert && bootstrap.Alert.getInstance(alert)) {
bootstrap.Alert.getInstance(alert).close();
}
}); });
}, 5000);
// Toggle sidebar on mobile // Form validation
const sidebarToggle = document.querySelector("#sidebarToggle"); const forms = document.querySelectorAll(".needs-validation");
if (sidebarToggle) { forms.forEach(function (form) {
sidebarToggle.addEventListener("click", function () { form.addEventListener(
document.querySelector(".sidebar").classList.toggle("show"); "submit",
}); function (event) {
} if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add("was-validated");
},
false,
);
});
// Auto-dismiss alerts after 5 seconds // Confirm dialogs
setTimeout(function () { const confirmButtons = document.querySelectorAll("[data-confirm]");
var alerts = document.querySelectorAll(".alert:not(.alert-important)"); confirmButtons.forEach(function (button) {
alerts.forEach(function (alert) { button.addEventListener("click", function (event) {
if (alert && bootstrap.Alert.getInstance(alert)) { if (!confirm(this.dataset.confirm || "Are you sure?")) {
bootstrap.Alert.getInstance(alert).close(); event.preventDefault();
} }
});
}, 5000);
// Form validation
const forms = document.querySelectorAll(".needs-validation");
forms.forEach(function (form) {
form.addEventListener(
"submit",
function (event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add("was-validated");
},
false,
);
}); });
});
// Confirm dialogs // Back button
const confirmButtons = document.querySelectorAll("[data-confirm]"); const backButtons = document.querySelectorAll(".btn-back");
confirmButtons.forEach(function (button) { backButtons.forEach(function (button) {
button.addEventListener("click", function (event) { button.addEventListener("click", function (event) {
if (!confirm(this.dataset.confirm || "Are you sure?")) { event.preventDefault();
event.preventDefault(); window.history.back();
}
});
}); });
});
// Back button // File input customization
const backButtons = document.querySelectorAll(".btn-back"); const fileInputs = document.querySelectorAll(".custom-file-input");
backButtons.forEach(function (button) { fileInputs.forEach(function (input) {
button.addEventListener("click", function (event) { input.addEventListener("change", function (e) {
event.preventDefault(); const fileName = this.files[0]?.name || "Choose file";
window.history.back(); const nextSibling = this.nextElementSibling;
}); if (nextSibling) {
nextSibling.innerText = fileName;
}
}); });
});
// File input customization // Search form submit on enter
const fileInputs = document.querySelectorAll(".custom-file-input"); const searchInputs = document.querySelectorAll(".search-input");
fileInputs.forEach(function (input) { searchInputs.forEach(function (input) {
input.addEventListener("change", function (e) { input.addEventListener("keypress", function (e) {
const fileName = this.files[0]?.name || "Choose file"; if (e.key === "Enter") {
const nextSibling = this.nextElementSibling; e.preventDefault();
if (nextSibling) { this.closest("form").submit();
nextSibling.innerText = fileName; }
}
});
}); });
});
// Search form submit on enter // Toggle password visibility
const searchInputs = document.querySelectorAll(".search-input"); const togglePasswordButtons = document.querySelectorAll(".toggle-password");
searchInputs.forEach(function (input) { togglePasswordButtons.forEach(function (button) {
input.addEventListener("keypress", function (e) { button.addEventListener("click", function () {
if (e.key === "Enter") { const target = document.querySelector(this.dataset.target);
e.preventDefault(); if (target) {
this.closest("form").submit(); const type = target.getAttribute("type") === "password" ? "text" : "password";
} target.setAttribute("type", type);
}); this.querySelector("i").classList.toggle("fa-eye");
this.querySelector("i").classList.toggle("fa-eye-slash");
}
}); });
});
// Toggle password visibility // Dropdown menu positioning
const togglePasswordButtons = document.querySelectorAll(".toggle-password"); const dropdowns = document.querySelectorAll(".dropdown-menu");
togglePasswordButtons.forEach(function (button) { dropdowns.forEach(function (dropdown) {
button.addEventListener("click", function () { dropdown.addEventListener("click", function (e) {
const target = document.querySelector(this.dataset.target); e.stopPropagation();
if (target) {
const type = target.getAttribute("type") === "password" ? "text" : "password";
target.setAttribute("type", type);
this.querySelector("i").classList.toggle("fa-eye");
this.querySelector("i").classList.toggle("fa-eye-slash");
}
});
}); });
});
// Dropdown menu positioning // Responsive table handling
const dropdowns = document.querySelectorAll(".dropdown-menu"); const tables = document.querySelectorAll(".table-responsive");
dropdowns.forEach(function (dropdown) { if (window.innerWidth < 768) {
dropdown.addEventListener("click", function (e) { tables.forEach(function (table) {
e.stopPropagation(); table.classList.add("table-responsive-force");
});
}); });
}
// Responsive table handling // Handle special links (printable views, exports)
const tables = document.querySelectorAll(".table-responsive"); const printLinks = document.querySelectorAll(".print-link");
printLinks.forEach(function (link) {
link.addEventListener("click", function (e) {
e.preventDefault();
window.print();
});
});
const exportLinks = document.querySelectorAll("[data-export]");
exportLinks.forEach(function (link) {
link.addEventListener("click", function (e) {
// Handle export functionality if needed
console.log("Export requested:", this.dataset.export);
});
});
// Handle sidebar collapse on small screens
function handleSidebarOnResize() {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
tables.forEach(function (table) { document.querySelector(".sidebar")?.classList.remove("show");
table.classList.add("table-responsive-force"); }
}); }
window.addEventListener("resize", handleSidebarOnResize); // Theme toggling functionality
function setTheme(theme, isUserPreference = false) {
console.log("Setting theme to:", theme, "User preference:", isUserPreference);
// Update the HTML attribute that controls theme
document.documentElement.setAttribute("data-bs-theme", theme);
// Save the theme preference to localStorage
localStorage.setItem("theme", theme);
// If this was a user choice (from the toggle button), record that fact
if (isUserPreference) {
localStorage.setItem("userPreferredTheme", "true");
} }
// Handle special links (printable views, exports) // Update toggle button icon
const printLinks = document.querySelectorAll(".print-link");
printLinks.forEach(function (link) {
link.addEventListener("click", function (e) {
e.preventDefault();
window.print();
});
});
const exportLinks = document.querySelectorAll("[data-export]");
exportLinks.forEach(function (link) {
link.addEventListener("click", function (e) {
// Handle export functionality if needed
console.log("Export requested:", this.dataset.export);
});
});
// Handle sidebar collapse on small screens
function handleSidebarOnResize() {
if (window.innerWidth < 768) {
document.querySelector(".sidebar")?.classList.remove("show");
}
}
window.addEventListener("resize", handleSidebarOnResize); // Theme toggling functionality
function setTheme(theme, isUserPreference = false) {
console.log("Setting theme to:", theme, "User preference:", isUserPreference);
// Update the HTML attribute that controls theme
document.documentElement.setAttribute("data-bs-theme", theme);
// Save the theme preference to localStorage
localStorage.setItem("theme", theme);
// If this was a user choice (from the toggle button), record that fact
if (isUserPreference) {
localStorage.setItem("userPreferredTheme", "true");
}
// Update toggle button icon
const themeToggle = document.getElementById("theme-toggle");
if (themeToggle) {
const icon = themeToggle.querySelector("i");
if (theme === "dark") {
icon.classList.remove("fa-moon");
icon.classList.add("fa-sun");
themeToggle.setAttribute("title", "Switch to light mode");
themeToggle.setAttribute("aria-label", "Switch to light mode");
} else {
icon.classList.remove("fa-sun");
icon.classList.add("fa-moon");
themeToggle.setAttribute("title", "Switch to dark mode");
themeToggle.setAttribute("aria-label", "Switch to dark mode");
}
}
// If we're on a page with charts, refresh them to match the theme
if (typeof window.refreshAllCharts === "function") {
console.log("Calling refresh charts from theme toggle");
// Add a small delay to ensure DOM updates have completed
setTimeout(window.refreshAllCharts, 100);
}
}
// Check if the user has a system preference for dark mode
function getSystemPreference() {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
// Initialize theme based on saved preference or system setting
function initializeTheme() {
// Check if the user has explicitly set a preference
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
const savedTheme = localStorage.getItem("theme");
const systemTheme = getSystemPreference();
console.log("Theme initialization:", {
hasUserPreference,
savedTheme,
systemTheme,
});
// Use saved theme if it exists and was set by user
// Otherwise, use system preference
if (hasUserPreference && savedTheme) {
setTheme(savedTheme);
} else {
// No user preference, use system preference
setTheme(systemTheme);
// Clear any saved theme to ensure it uses system preference
localStorage.removeItem("userPreferredTheme");
}
}
// Initialize theme on page load
initializeTheme();
// Listen for system preference changes
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
colorSchemeMediaQuery.addEventListener("change", (e) => {
// Only update theme based on system if user hasn't set a preference
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
console.log("System preference changed. Following system?", !hasUserPreference);
if (!hasUserPreference) {
setTheme(e.matches ? "dark" : "light");
}
});
// Theme toggle button functionality
const themeToggle = document.getElementById("theme-toggle"); const themeToggle = document.getElementById("theme-toggle");
if (themeToggle) { if (themeToggle) {
themeToggle.addEventListener("click", function () { const icon = themeToggle.querySelector("i");
const currentTheme = document.documentElement.getAttribute("data-bs-theme") || "light"; if (theme === "dark") {
const newTheme = currentTheme === "dark" ? "light" : "dark"; icon.classList.remove("fa-moon");
console.log("Manual theme toggle from", currentTheme, "to", newTheme); icon.classList.add("fa-sun");
setTheme(newTheme, true); // true indicates this is a user preference themeToggle.setAttribute("title", "Switch to light mode");
}); themeToggle.setAttribute("aria-label", "Switch to light mode");
} else {
icon.classList.remove("fa-sun");
icon.classList.add("fa-moon");
themeToggle.setAttribute("title", "Switch to dark mode");
themeToggle.setAttribute("aria-label", "Switch to dark mode");
}
} }
// If we're on a page with charts, refresh them to match the theme
if (typeof window.refreshAllCharts === "function") {
console.log("Calling refresh charts from theme toggle");
// Add a small delay to ensure DOM updates have completed
setTimeout(window.refreshAllCharts, 100);
}
}
// Check if the user has a system preference for dark mode
function getSystemPreference() {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
// Initialize theme based on saved preference or system setting
function initializeTheme() {
// Check if the user has explicitly set a preference
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
const savedTheme = localStorage.getItem("theme");
const systemTheme = getSystemPreference();
console.log("Theme initialization:", {
hasUserPreference,
savedTheme,
systemTheme,
});
// Use saved theme if it exists and was set by user
// Otherwise, use system preference
if (hasUserPreference && savedTheme) {
setTheme(savedTheme);
} else {
// No user preference, use system preference
setTheme(systemTheme);
// Clear any saved theme to ensure it uses system preference
localStorage.removeItem("userPreferredTheme");
}
}
// Initialize theme on page load
initializeTheme();
// Listen for system preference changes
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
colorSchemeMediaQuery.addEventListener("change", (e) => {
// Only update theme based on system if user hasn't set a preference
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
console.log("System preference changed. Following system?", !hasUserPreference);
if (!hasUserPreference) {
setTheme(e.matches ? "dark" : "light");
}
});
// Theme toggle button functionality
const themeToggle = document.getElementById("theme-toggle");
if (themeToggle) {
themeToggle.addEventListener("click", function () {
const currentTheme = document.documentElement.getAttribute("data-bs-theme") || "light";
const newTheme = currentTheme === "dark" ? "light" : "dark";
console.log("Manual theme toggle from", currentTheme, "to", newTheme);
setTheme(newTheme, true); // true indicates this is a user preference
});
}
}); });

View File

@@ -1,26 +1,26 @@
{ {
"scripts": { "scripts": {
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"lint:md": "markdownlint-cli2 \"**/*.md\"", "lint:md": "markdownlint-cli2 \"**/*.md\"",
"lint:md:fix": "bun lint:md -- --fix" "lint:md:fix": "bun lint:md -- --fix"
},
"devDependencies": {
"markdownlint-cli2": "^0.18.1",
"prettier": "^3.6.2",
"prettier-plugin-jinja-template": "^2.1.0",
"prettier-plugin-packagejson": "^2.5.19"
},
"markdownlint-cli2": {
"config": {
"MD013": false,
"MD033": false
}, },
"devDependencies": { "ignores": [
"markdownlint-cli2": "^0.18.1", ".git",
"prettier": "^3.6.2", ".trunk",
"prettier-plugin-jinja-template": "^2.1.0", ".venv",
"prettier-plugin-packagejson": "^2.5.19" "node_modules"
}, ]
"markdownlint-cli2": { }
"config": {
"MD013": false,
"MD033": false
},
"ignores": [
".git",
".trunk",
".venv",
"node_modules"
]
}
} }

View File

@@ -40,6 +40,13 @@ dependencies = [
"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme" "Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
"Source" = "https://github.com/kjanat/livegraphsdjango" "Source" = "https://github.com/kjanat/livegraphsdjango"
[project.scripts]
# Django management commands
livegraphs-manage = "dashboard_project.manage:main"
livegraphs-migrate = "dashboard_project.__main__:migrate"
livegraphs-server = "dashboard_project.__main__:runserver"
livegraphs-shell = "dashboard_project.__main__:shell"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"bandit>=1.8.3", "bandit>=1.8.3",
@@ -52,6 +59,7 @@ dev = [
"pytest>=8.3.5", "pytest>=8.3.5",
"pytest-django>=4.11.1", "pytest-django>=4.11.1",
"ruff>=0.11.10", "ruff>=0.11.10",
"ty>=0.0.1a25",
] ]
[build-system] [build-system]
@@ -165,4 +173,9 @@ line-ending = "lf"
packages = ["dashboard_project"] packages = ["dashboard_project"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
"dashboard_project" = ["static/__/*", "templates/__/*", "media/**/*"] "dashboard_project" = [
"static/**/*",
"templates/**/*",
"media/**/*",
"py.typed"
]

View File

@@ -547,6 +547,25 @@ tinycss2==1.4.0 \
# via # via
# bleach # bleach
# livegraphsdjango # livegraphsdjango
ty==0.0.1a25 \
--hash=sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc \
--hash=sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445 \
--hash=sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979 \
--hash=sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703 \
--hash=sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9 \
--hash=sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511 \
--hash=sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35 \
--hash=sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068 \
--hash=sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf \
--hash=sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7 \
--hash=sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb \
--hash=sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01 \
--hash=sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d \
--hash=sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447 \
--hash=sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d \
--hash=sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b \
--hash=sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a \
--hash=sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62
types-pyyaml==6.0.12.20250915 \ types-pyyaml==6.0.12.20250915 \
--hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \
--hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6 --hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6

26
ty.toml Normal file
View File

@@ -0,0 +1,26 @@
# ty Type Checker Configuration
[environment]
# Django project root for first-party module resolution
root = ["dashboard_project"]
# Python version (matches pyproject.toml requires-python)
python-version = "3.13"
[src]
# Include only the Django project directory
include = ["dashboard_project"]
# Exclude migrations, cache, and generated files
exclude = [
"dashboard_project/migrations",
"dashboard_project/*/migrations",
"dashboard_project/**/__pycache__",
"dashboard_project/**/*.pyc"
]
# Respect .gitignore files
respect-ignore-files = true
[terminal]
# Use concise output for cleaner CI/CD logs
output-format = "concise"
# Treat warnings as errors in CI
error-on-warning = false

27
uv.lock generated
View File

@@ -563,6 +563,7 @@ dev = [
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-django" }, { name = "pytest-django" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" },
] ]
[package.metadata] [package.metadata]
@@ -599,6 +600,7 @@ dev = [
{ name = "pytest", specifier = ">=8.3.5" }, { name = "pytest", specifier = ">=8.3.5" },
{ name = "pytest-django", specifier = ">=4.11.1" }, { name = "pytest-django", specifier = ">=4.11.1" },
{ name = "ruff", specifier = ">=0.11.10" }, { name = "ruff", specifier = ">=0.11.10" },
{ name = "ty", specifier = ">=0.0.1a25" },
] ]
[[package]] [[package]]
@@ -1088,6 +1090,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
] ]
[[package]]
name = "ty"
version = "0.0.1a25"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/6b/e73bc3c1039ea72936158a08313155a49e5aa5e7db5205a149fe516a4660/ty-0.0.1a25.tar.gz", hash = "sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511", size = 4403670, upload-time = "2025-10-29T19:40:23.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/3b/4457231238a2eeb04cba4ba7cc33d735be68ee46ca40a98ae30e187de864/ty-0.0.1a25-py3-none-linux_armv6l.whl", hash = "sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d", size = 8878803, upload-time = "2025-10-29T19:39:42.243Z" },
{ url = "https://files.pythonhosted.org/packages/8a/fa/a328713dd310018fc7a381693d8588185baa2fdae913e01a6839187215df/ty-0.0.1a25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703", size = 8695667, upload-time = "2025-10-29T19:39:45.179Z" },
{ url = "https://files.pythonhosted.org/packages/22/e8/5707939118992ced2bf5385adc3ede7723c1b717b07ad14c495eea1e47b4/ty-0.0.1a25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf", size = 8159012, upload-time = "2025-10-29T19:39:47.011Z" },
{ url = "https://files.pythonhosted.org/packages/eb/fb/ff313aa71602225cd78f1bce3017713d6d1b1c1e0fa8101ead4594a60d95/ty-0.0.1a25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7", size = 8433675, upload-time = "2025-10-29T19:39:48.443Z" },
{ url = "https://files.pythonhosted.org/packages/c0/8d/cc7e7fb57215a15b575a43ed042bdd92971871e0decec1b26d2e7d969465/ty-0.0.1a25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447", size = 8668456, upload-time = "2025-10-29T19:39:50.412Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6d/d7bf5909ed2dcdcbc1e2ca7eea80929893e2d188d9c36b3fcb2b36532ff6/ty-0.0.1a25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01", size = 9023543, upload-time = "2025-10-29T19:39:52.292Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b8/72bcefb4be32e5a84f0b21de2552f16cdb4cae3eb271ac891c8199c26b1a/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a", size = 9700013, upload-time = "2025-10-29T19:39:57.283Z" },
{ url = "https://files.pythonhosted.org/packages/90/0d/cf7e794b840cf6b0bbecb022e593c543f85abad27a582241cf2095048cb1/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b", size = 9372574, upload-time = "2025-10-29T19:40:04.532Z" },
{ url = "https://files.pythonhosted.org/packages/1e/71/2d35e7d51b48eabd330e2f7b7e0bce541cbd95950c4d2f780e85f3366af1/ty-0.0.1a25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62", size = 9535726, upload-time = "2025-10-29T19:40:06.548Z" },
{ url = "https://files.pythonhosted.org/packages/57/d3/01ecc23bbd8f3e0dfbcf9172d06d84e88155c5f416f1491137e8066fd859/ty-0.0.1a25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc", size = 9003380, upload-time = "2025-10-29T19:40:08.683Z" },
{ url = "https://files.pythonhosted.org/packages/de/f9/cde9380d8a1a6ca61baeb9aecb12cbec90d489aa929be55cd78ad5c2ccd9/ty-0.0.1a25-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068", size = 8401833, upload-time = "2025-10-29T19:40:10.627Z" },
{ url = "https://files.pythonhosted.org/packages/0b/39/0acf3625b0c495011795a391016b572f97a812aca1d67f7a76621fdb9ebf/ty-0.0.1a25-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9", size = 8706761, upload-time = "2025-10-29T19:40:12.575Z" },
{ url = "https://files.pythonhosted.org/packages/25/73/7de1648f3563dd9d416d36ab5f1649bfd7b47a179135027f31d44b89a246/ty-0.0.1a25-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979", size = 8792426, upload-time = "2025-10-29T19:40:14.553Z" },
{ url = "https://files.pythonhosted.org/packages/7d/8a/b6e761a65eac7acd10b2e452f49b2d8ae0ea163ca36bb6b18b2dadae251b/ty-0.0.1a25-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35", size = 9103991, upload-time = "2025-10-29T19:40:16.332Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/9324ae947fcc4322470326cf8276a3fc2f08dc82adec1de79d963fdf7af5/ty-0.0.1a25-py3-none-win32.whl", hash = "sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445", size = 8387095, upload-time = "2025-10-29T19:40:18.368Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2b/cb12cbc7db1ba310aa7b1de9b4e018576f653105993736c086ee67d2ec02/ty-0.0.1a25-py3-none-win_amd64.whl", hash = "sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb", size = 9059225, upload-time = "2025-10-29T19:40:20.278Z" },
{ url = "https://files.pythonhosted.org/packages/2f/c1/f6be8cdd0bf387c1d8ee9d14bb299b7b5d2c0532f550a6693216a32ec0c5/ty-0.0.1a25-py3-none-win_arm64.whl", hash = "sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d", size = 8536832, upload-time = "2025-10-29T19:40:22.014Z" },
]
[[package]] [[package]]
name = "types-pyyaml" name = "types-pyyaml"
version = "6.0.12.20250915" version = "6.0.12.20250915"