From fdcec7eb8470db71d08ca63878eafb4dc20e84ac Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Wed, 5 Nov 2025 14:54:56 +0100 Subject: [PATCH] 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) --- .devcontainer/devcontainer.json | 384 ++++---- .editorconfig | 4 +- .markdownlint.json | 17 - .uv | 10 +- .zed/settings.json | 14 + dashboard_project/__main__.py | 29 +- dashboard_project/accounts/forms.py | 3 +- dashboard_project/dashboard/admin.py | 4 +- dashboard_project/dashboard/forms.py | 11 +- .../management/commands/create_test_data.py | 2 +- dashboard_project/dashboard/utils.py | 5 +- dashboard_project/dashboard/views.py | 2 +- .../dashboard_project/settings.py | 4 +- dashboard_project/data_integration/admin.py | 6 +- .../commands/fix_datasource_schema.py | 3 +- .../management/commands/test_redis.py | 2 +- dashboard_project/data_integration/utils.py | 8 +- dashboard_project/py.typed | 0 dashboard_project/static/css/dashboard.css | 538 +++++------ dashboard_project/static/css/style.css | 374 ++++---- .../static/js/ajax-navigation.js | 511 +++++----- .../static/js/ajax-pagination.js | 168 ++-- dashboard_project/static/js/dashboard.js | 895 +++++++++--------- dashboard_project/static/js/main.js | 424 ++++----- package.json | 46 +- pyproject.toml | 15 +- requirements.txt | 19 + ty.toml | 26 + uv.lock | 27 + 29 files changed, 1831 insertions(+), 1720 deletions(-) delete mode 100644 .markdownlint.json create mode 100644 .zed/settings.json create mode 100644 dashboard_project/py.typed create mode 100644 ty.toml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d69f99e..1582b6e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,198 +1,198 @@ // 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 { - "name": "Ubuntu", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/base:jammy", - // Features to add to the dev container. More info: https://containers.dev/features. - "features": { - "ghcr.io/devcontainers-community/npm-features/prettier:1": { - "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": {} + "name": "Ubuntu", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:jammy", + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers-community/npm-features/prettier:1": { + "version": "latest" }, - "customizations": { - "vscode": { - "extensions": [ - "bierner.github-markdown-preview", - "bierner.markdown-mermaid", - "bierner.markdown-preview-github-styles", - "charliermarsh.ruff", - "CS50.ddb50", - "DavidAnson.vscode-markdownlint", - "esbenp.prettier-vscode", - "GitHub.copilot-chat", - "GitHub.copilot-workspace", - "GitHub.remotehub", - "github.vscode-github-actions", - "ms-vscode.copilot-mermaid-diagram", - "ms-vscode.vscode-copilot-data-analysis", - "ms-vscode.vscode-copilot-vision", - "ms-vscode.vscode-github-issue-notebooks", - "ms-vscode.vscode-websearchforcopilot", - "PyCQA.bandit-pycqa", - "samuelcolvin.jinjahtml", - "shd101wyy.markdown-preview-enhanced", - "tamasfe.even-better-toml", - "timonwong.shellcheck", - "trunk.io", - "VisualStudioExptTeam.intellicode-api-usage-examples", - "yzhang.markdown-all-in-one" - ], - "settings": { - "github.copilot.chat.codeGeneration.instructions": [ - { - "text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`." - }, - { - "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." - }, - { - "text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`." - }, - { - "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." - }, - { - "text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development." - }, - { - "text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development." - }, - { - "text": "This dev container includes `python3` and `pip3` pre-installed and available on the `PATH`, along with the Python language extensions for Python development." - }, - { - "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." - }, - { - "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." - }, - { - "text": "This workspace is in a dev container running on \"Ubuntu 22.04.5 LTS\".\n\nUse `\"$BROWSER\" ` 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`" - } - ], - "[css]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[html]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[markdown]": { - "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 - } - } - } + "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": { + "vscode": { + "extensions": [ + "bierner.github-markdown-preview", + "bierner.markdown-mermaid", + "bierner.markdown-preview-github-styles", + "charliermarsh.ruff", + "CS50.ddb50", + "DavidAnson.vscode-markdownlint", + "esbenp.prettier-vscode", + "GitHub.copilot-chat", + "GitHub.copilot-workspace", + "GitHub.remotehub", + "github.vscode-github-actions", + "ms-vscode.copilot-mermaid-diagram", + "ms-vscode.vscode-copilot-data-analysis", + "ms-vscode.vscode-copilot-vision", + "ms-vscode.vscode-github-issue-notebooks", + "ms-vscode.vscode-websearchforcopilot", + "PyCQA.bandit-pycqa", + "samuelcolvin.jinjahtml", + "shd101wyy.markdown-preview-enhanced", + "tamasfe.even-better-toml", + "timonwong.shellcheck", + "trunk.io", + "VisualStudioExptTeam.intellicode-api-usage-examples", + "yzhang.markdown-all-in-one" + ], + "settings": { + "github.copilot.chat.codeGeneration.instructions": [ + { + "text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`." + }, + { + "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." + }, + { + "text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`." + }, + { + "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." + }, + { + "text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development." + }, + { + "text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development." + }, + { + "text": "This dev container includes `python3` and `pip3` pre-installed and available on the `PATH`, along with the Python language extensions for Python development." + }, + { + "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." + }, + { + "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." + }, + { + "text": "This workspace is in a dev container running on \"Ubuntu 22.04.5 LTS\".\n\nUse `\"$BROWSER\" ` 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`" + } + ], + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[markdown]": { + "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. - "postCreateCommand": "bash .devcontainer/postCreateCommand.sh" - // Configure tool-specific properties. - // "customizations": {}, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + } + } + }, + // 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. + "postCreateCommand": "bash .devcontainer/postCreateCommand.sh" + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } diff --git a/.editorconfig b/.editorconfig index 506c4a2..ac7bb2c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,8 +27,8 @@ indent_size = 2 # CSS, JavaScript, and JSON files [*.{css,scss,js,json}] -indent_style = tab -indent_size = 4 +indent_style = space +indent_size = 2 # Markdown files [*.md] diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index 8b742bc..0000000 --- a/.markdownlint.json +++ /dev/null @@ -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 -} diff --git a/.uv b/.uv index 04e0726..4afb25d 100644 --- a/.uv +++ b/.uv @@ -5,17 +5,11 @@ keep-lockfile = true # Cache compiled bytecode for dependencies compile-bytecode = true -# Use a local cache directory -local-cache = true - # Verbosity of output verbosity = "minimal" -# Define which part of the environment to check -environment-checks = ["python", "dependencies"] +; # Define which part of the environment to check +; environment-checks = ["python", "dependencies"] # How to resolve dependencies not specified with exact versions dependency-resolution = "strict" - -# If the cache and target directories are on different filesystems, hardlinking may not be supported. -link-mode = "copy" diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..d6d5a3c --- /dev/null +++ b/.zed/settings.json @@ -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", + "..." + ] + } + } +} diff --git a/dashboard_project/__main__.py b/dashboard_project/__main__.py index 926d054..f8f8ab0 100644 --- a/dashboard_project/__main__.py +++ b/dashboard_project/__main__.py @@ -17,7 +17,7 @@ def main(): # Default to 'manage.py' if no specific command if cmd_name == "__main__": # 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() return @@ -48,5 +48,32 @@ def main(): 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__": main() diff --git a/dashboard_project/accounts/forms.py b/dashboard_project/accounts/forms.py index 4665474..79af2b8 100644 --- a/dashboard_project/accounts/forms.py +++ b/dashboard_project/accounts/forms.py @@ -30,7 +30,8 @@ class CustomUserChangeForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 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: self.fields["company"].disabled = True if "is_company_admin" in self.fields: diff --git a/dashboard_project/dashboard/admin.py b/dashboard_project/dashboard/admin.py index a015bc6..c161c79 100644 --- a/dashboard_project/dashboard/admin.py +++ b/dashboard_project/dashboard/admin.py @@ -49,7 +49,9 @@ class DataSourceAdmin(admin.ModelAdmin): @admin.display(description="External Data Status") def get_external_data_status(self, obj): 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" diff --git a/dashboard_project/dashboard/forms.py b/dashboard_project/dashboard/forms.py index abb8de2..e8a809c 100644 --- a/dashboard_project/dashboard/forms.py +++ b/dashboard_project/dashboard/forms.py @@ -1,7 +1,14 @@ # dashboard/forms.py +from __future__ import annotations + +from typing import TYPE_CHECKING + from django import forms +if TYPE_CHECKING: + pass + from .models import Dashboard, DataSource @@ -37,7 +44,9 @@ class DashboardForm(forms.ModelForm): super().__init__(*args, **kwargs) 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): instance = super().save(commit=False) diff --git a/dashboard_project/dashboard/management/commands/create_test_data.py b/dashboard_project/dashboard/management/commands/create_test_data.py index 7cf4321..540fd35 100644 --- a/dashboard_project/dashboard/management/commands/create_test_data.py +++ b/dashboard_project/dashboard/management/commands/create_test_data.py @@ -83,7 +83,7 @@ class Command(BaseCommand): ChatSession.objects.all().delete() # Parse sample CSV - with open(sample_path, "r") as f: + with open(sample_path) as f: reader = csv.reader(f) header = next(reader) # Skip header diff --git a/dashboard_project/dashboard/utils.py b/dashboard_project/dashboard/utils.py index 135dddd..1e39535 100644 --- a/dashboard_project/dashboard/utils.py +++ b/dashboard_project/dashboard/utils.py @@ -1,10 +1,13 @@ # dashboard/utils.py +from __future__ import annotations + import contextlib import numpy as np import pandas as pd from django.db import models +from django.db.models import functions from django.utils.timezone import make_aware from .models import ChatSession @@ -137,7 +140,7 @@ def generate_dashboard_data(data_sources): # Time series data (sessions per day) time_series_query = ( 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") .annotate(count=models.Count("id")) .order_by("date") diff --git a/dashboard_project/dashboard/views.py b/dashboard_project/dashboard/views.py index 5497be7..ea103cf 100644 --- a/dashboard_project/dashboard/views.py +++ b/dashboard_project/dashboard/views.py @@ -58,7 +58,7 @@ def dashboard_view(request): if selected_dashboard_id: selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company) else: - selected_dashboard = dashboards.first() + selected_dashboard = dashboards.first() # type: ignore[assignment] # Generate dashboard data dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all()) diff --git a/dashboard_project/dashboard_project/settings.py b/dashboard_project/dashboard_project/settings.py index f589345..0956818 100644 --- a/dashboard_project/dashboard_project/settings.py +++ b/dashboard_project/dashboard_project/settings.py @@ -184,8 +184,8 @@ try: logger.info("Using Redis for Celery broker and result backend") except ( ImportError, - redis.exceptions.ConnectionError, - redis.exceptions.TimeoutError, + redis.exceptions.ConnectionError, # type: ignore[attr-defined] + redis.exceptions.TimeoutError, # type: ignore[attr-defined] ) as e: # Redis is not available, use SQLite as fallback (works for development) CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite") diff --git a/dashboard_project/data_integration/admin.py b/dashboard_project/data_integration/admin.py index 0d9ab32..82d3717 100644 --- a/dashboard_project/data_integration/admin.py +++ b/dashboard_project/data_integration/admin.py @@ -52,10 +52,8 @@ class ExternalDataSourceAdmin(admin.ModelAdmin): status, ) else: - return format_html( - '{}', - status, - ) + style = "color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;" + return format_html(f'{{}}', status) @admin.display(description="Actions") def refresh_action(self, obj): diff --git a/dashboard_project/data_integration/management/commands/fix_datasource_schema.py b/dashboard_project/data_integration/management/commands/fix_datasource_schema.py index d288433..8ac0984 100644 --- a/dashboard_project/data_integration/management/commands/fix_datasource_schema.py +++ b/dashboard_project/data_integration/management/commands/fix_datasource_schema.py @@ -56,7 +56,8 @@ class Command(BaseCommand): ) elif col == "sync_interval": 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": cursor.execute( diff --git a/dashboard_project/data_integration/management/commands/test_redis.py b/dashboard_project/data_integration/management/commands/test_redis.py index 58d8d36..c9c2161 100644 --- a/dashboard_project/data_integration/management/commands/test_redis.py +++ b/dashboard_project/data_integration/management/commands/test_redis.py @@ -59,7 +59,7 @@ class Command(BaseCommand): redis_client.delete(test_key) else: 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("Celery will use SQLite fallback if configured.") except ImportError: diff --git a/dashboard_project/data_integration/utils.py b/dashboard_project/data_integration/utils.py index 3f1a871..b98924d 100644 --- a/dashboard_project/data_integration/utils.py +++ b/dashboard_project/data_integration/utils.py @@ -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 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) stats["errors"] += 1 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 not has_recognized_patterns and len(lines) > 0: 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 diff --git a/dashboard_project/py.typed b/dashboard_project/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/dashboard_project/static/css/dashboard.css b/dashboard_project/static/css/dashboard.css index fdb6570..685dc35 100644 --- a/dashboard_project/static/css/dashboard.css +++ b/dashboard_project/static/css/dashboard.css @@ -5,527 +5,527 @@ /*Theme variables */ :root { - /* Light theme (default)*/ - --bg-color: #f8f9fa; - --text-color: #212529; - --card-bg: #ffffff; - --card-border: #dee2e6; - --card-header-bg: #f1f3f5; - --sidebar-bg: #f8f9fa; - --navbar-bg: #343a40; - --navbar-color: #ffffff; - --link-color: #007bff; - --secondary-text: #6c757d; - --border-color: #e9ecef; - --input-bg: #ffffff; - --input-border: #ced4da; - --table-stripe: rgba(0, 0, 0, 0.05); - --stats-card-bg: #f1f3f5; - --icon-bg: #e9f2ff; - --icon-color: #007bff; - --theme-transition: - color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; + /* Light theme (default)*/ + --bg-color: #f8f9fa; + --text-color: #212529; + --card-bg: #ffffff; + --card-border: #dee2e6; + --card-header-bg: #f1f3f5; + --sidebar-bg: #f8f9fa; + --navbar-bg: #343a40; + --navbar-color: #ffffff; + --link-color: #007bff; + --secondary-text: #6c757d; + --border-color: #e9ecef; + --input-bg: #ffffff; + --input-border: #ced4da; + --table-stripe: rgba(0, 0, 0, 0.05); + --stats-card-bg: #f1f3f5; + --icon-bg: #e9f2ff; + --icon-color: #007bff; + --theme-transition: + color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; } /*Dark theme*/ [data-bs-theme="dark"] { - --bg-color: #212529; - --text-color: #f8f9fa; - --card-bg: #343a40; - --card-border: #495057; - --card-header-bg: #495057; - --sidebar-bg: #2c3034; - --navbar-bg: #1c1f23; - --navbar-color: #f8f9fa; - --link-color: #6ea8fe; - --secondary-text: #adb5bd; - --border-color: #495057; - --input-bg: #2b3035; - --input-border: #495057; - --table-stripe: rgba(255, 255, 255, 0.05); - --stats-card-bg: #2c3034; - --icon-bg: #1e3a8a; - --icon-color: #6ea8fe; + --bg-color: #212529; + --text-color: #f8f9fa; + --card-bg: #343a40; + --card-border: #495057; + --card-header-bg: #495057; + --sidebar-bg: #2c3034; + --navbar-bg: #1c1f23; + --navbar-color: #f8f9fa; + --link-color: #6ea8fe; + --secondary-text: #adb5bd; + --border-color: #495057; + --input-bg: #2b3035; + --input-border: #495057; + --table-stripe: rgba(255, 255, 255, 0.05); + --stats-card-bg: #2c3034; + --icon-bg: #1e3a8a; + --icon-color: #6ea8fe; } /*Apply theme variables*/ body { - background-color: var(--bg-color); - color: var(--text-color); - transition: var(--theme-transition); + background-color: var(--bg-color); + color: var(--text-color); + transition: var(--theme-transition); } .card { - background-color: var(--card-bg); - border-color: var(--card-border); - transition: var(--theme-transition); + background-color: var(--card-bg); + border-color: var(--card-border); + transition: var(--theme-transition); } .card-header { - background-color: var(--card-header-bg); - border-bottom-color: var(--card-border); - transition: var(--theme-transition); + background-color: var(--card-header-bg); + border-bottom-color: var(--card-border); + transition: var(--theme-transition); } .navbar-dark { - background-color: var(--navbar-bg) !important; - border-bottom: 1px solid var(--border-color); + background-color: var(--navbar-bg) !important; + border-bottom: 1px solid var(--border-color); } .navbar-dark .navbar-brand, .navbar-dark .nav-link, .navbar-dark .navbar-text { - color: var(--navbar-color) !important; + color: var(--navbar-color) !important; } .navbar-dark .btn-outline-light { - border-color: var(--border-color); - color: var(--navbar-color); + border-color: var(--border-color); + color: var(--navbar-color); } .navbar-dark .btn-outline-light:hover { - background-color: rgba(255, 255, 255, 0.1); - border-color: var(--border-color); + background-color: rgba(255, 255, 255, 0.1); + border-color: var(--border-color); } .sidebar { - background-color: var(--sidebar-bg) !important; + background-color: var(--sidebar-bg) !important; } /*Sidebar navigation styling with dark mode support*/ .sidebar .nav-link { - color: var(--text-color); - transition: all 0.2s ease; - border-radius: 0.375rem; - margin: 0.1rem 0.5rem; - padding: 0.5rem 1rem; + color: var(--text-color); + transition: all 0.2s ease; + border-radius: 0.375rem; + margin: 0.1rem 0.5rem; + padding: 0.5rem 1rem; } .sidebar .nav-link:hover { - color: var(--link-color); - background-color: rgba(0, 0, 0, 0.05); + color: var(--link-color); + background-color: rgba(0, 0, 0, 0.05); } [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 { - color: var(--link-color); - background-color: rgba(13, 110, 253, 0.1); - font-weight: 600; + color: var(--link-color); + background-color: rgba(13, 110, 253, 0.1); + font-weight: 600; } [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 { - color: var(--secondary-text); - width: 20px; - text-align: center; - margin-right: 0.5rem; + color: var(--secondary-text); + width: 20px; + text-align: center; + margin-right: 0.5rem; } .sidebar .nav-link:hover i, .sidebar .nav-link.active i { - color: var(--link-color); + color: var(--link-color); } .sidebar .nav-header { - color: var(--secondary-text); - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.08em; - padding: 0.5rem 1.25rem; - margin-top: 1rem; + color: var(--secondary-text); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 0.5rem 1.25rem; + margin-top: 1rem; } .table { - color: var(--text-color); + color: var(--text-color); } .table-striped tbody tr:nth-of-type(odd) { - background-color: var(--table-stripe); + background-color: var(--table-stripe); } .nav-link { - color: var(--link-color); + color: var(--link-color); } .stats-card { - background-color: var(--stats-card-bg) !important; + background-color: var(--stats-card-bg) !important; } .stat-card .stat-icon { - background-color: var(--icon-bg); - color: var(--icon-color); + background-color: var(--icon-bg); + color: var(--icon-color); } .form-control, .form-select { - background-color: var(--input-bg); - border-color: var(--input-border); - color: var(--text-color); + background-color: var(--input-bg); + border-color: var(--input-border); + color: var(--text-color); } /*Footer*/ footer { - background-color: var(--card-bg); - border-top: 1px solid var(--border-color); - color: var(--secondary-text); - margin-top: 2rem; - padding: 1.5rem 0; - transition: var(--theme-transition); + background-color: var(--card-bg); + border-top: 1px solid var(--border-color); + color: var(--secondary-text); + margin-top: 2rem; + padding: 1.5rem 0; + transition: var(--theme-transition); } [data-bs-theme="dark"] footer { - background-color: var(--navbar-bg); + background-color: var(--navbar-bg); } /*Dashboard grid layout*/ .dashboard-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - /* Slightly larger minmax for widgets */ - gap: 1.5rem; + /* Slightly larger minmax for widgets */ + gap: 1.5rem; - /* Increased gap */ + /* Increased gap */ } /*Dashboard widget cards*/ .dashboard-widget { - display: flex; + display: flex; - /* Allow flex for content alignment */ - flex-direction: column; + /* Allow flex for content alignment */ + flex-direction: column; - /* Stack header, body, footer vertically */ - height: 100%; + /* Stack header, body, footer vertically */ + height: 100%; - /* Ensure widgets fill grid cell height */ + /* Ensure widgets fill grid cell height */ } .dashboard-widget .card-header { - display: flex; - justify-content: space-between; - align-items: center; + display: flex; + justify-content: space-between; + align-items: center; } .dashboard-widget .card-header .widget-title { - font-size: 1.1rem; + font-size: 1.1rem; - /* Slightly larger widget titles */ - font-weight: 600; + /* Slightly larger widget titles */ + font-weight: 600; } .dashboard-widget .card-header .widget-actions { - display: flex; - gap: 0.5rem; + display: flex; + gap: 0.5rem; } .dashboard-widget .card-header .widget-actions .btn { - width: 32px; + width: 32px; - /* Slightly larger action buttons */ - height: 32px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.85rem; - background-color: transparent; - border: 1px solid transparent; - color: #6c757d; + /* Slightly larger action buttons */ + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; + background-color: transparent; + border: 1px solid transparent; + color: #6c757d; } .dashboard-widget .card-header .widget-actions .btn:hover { - background-color: #f0f0f0; - border-color: #e0e0e0; - color: #333; + background-color: #f0f0f0; + border-color: #e0e0e0; + color: #333; } .dashboard-widget .card-body { - flex-grow: 1; + flex-grow: 1; - /* Allow card body to take available space */ - padding: 1.25rem; + /* Allow card body to take available space */ + padding: 1.25rem; - /* Consistent padding */ + /* Consistent padding */ } /*Chart widgets*/ .chart-widget .card-body { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .chart-widget .chart-container { - flex: 1; - min-height: 250px; + flex: 1; + min-height: 250px; - /* Adjusted min-height */ - width: 100%; + /* Adjusted min-height */ + width: 100%; - /* Ensure it takes full width of card body */ + /* Ensure it takes full width of card body */ } /*Stat widgets / Stat Cards*/ .stat-card { - text-align: center; - padding: 1.5rem; + text-align: center; + padding: 1.5rem; - /* Generous padding */ + /* Generous padding */ } .stat-card .stat-icon { - font-size: 2.25rem; + font-size: 2.25rem; - /* Larger icon */ - margin-bottom: 1rem; - display: inline-block; - width: 4.5rem; - height: 4.5rem; - line-height: 4.5rem; - text-align: center; - border-radius: 50%; - background-color: #e9f2ff; + /* Larger icon */ + margin-bottom: 1rem; + display: inline-block; + width: 4.5rem; + height: 4.5rem; + line-height: 4.5rem; + text-align: center; + border-radius: 50%; + background-color: #e9f2ff; - /* Light blue background for icon */ - color: #007bff; + /* Light blue background for icon */ + color: #007bff; - /* Primary color for icon */ + /* Primary color for icon */ } .stat-card .stat-value { - font-size: 2.25rem; + font-size: 2.25rem; - /* Larger stat value */ - font-weight: 700; - margin-bottom: 0.25rem; + /* Larger stat value */ + font-weight: 700; + margin-bottom: 0.25rem; - /* Reduced margin */ - line-height: 1.1; - color: #212529; + /* Reduced margin */ + line-height: 1.1; + color: #212529; - /* Darker color for value */ + /* Darker color for value */ } .stat-card .stat-label { - font-size: 0.9rem; + font-size: 0.9rem; - /* Slightly larger label */ - color: #6c757d; - margin-bottom: 0; + /* Slightly larger label */ + color: #6c757d; + margin-bottom: 0; } /*Dashboard theme variations*/ .dashboard-theme-light .card { - background-color: #fff; + background-color: #fff; } .dashboard-theme-dark { - background-color: #212529; - color: #f8f9fa; + background-color: #212529; + color: #f8f9fa; } .dashboard-theme-dark .card { - background-color: #343a40; - color: #f8f9fa; - border-color: #495057; + background-color: #343a40; + color: #f8f9fa; + border-color: #495057; } .dashboard-theme-dark .card-header { - background-color: #495057; - border-bottom-color: #6c757d; + background-color: #495057; + border-bottom-color: #6c757d; } .dashboard-theme-dark .stat-card .stat-label { - color: #adb5bd; + color: #adb5bd; } /*Time period selector*/ .time-period-selector { - display: flex; - align-items: center; - gap: 0.75rem; + display: flex; + align-items: center; + gap: 0.75rem; - /* Increased gap */ - margin-bottom: 1.5rem; + /* Increased gap */ + margin-bottom: 1.5rem; - /* Increased margin */ + /* Increased margin */ } .time-period-selector .btn-group { - flex-wrap: wrap; + flex-wrap: wrap; } .time-period-selector .btn { - padding: 0.375rem 0.75rem; + padding: 0.375rem 0.75rem; - /* Bootstrap-like padding */ - font-size: 0.875rem; + /* Bootstrap-like padding */ + font-size: 0.875rem; } /*Custom metric selector*/ .metric-selector { - max-width: 100%; - overflow-x: auto; - white-space: nowrap; - padding-bottom: 0.5rem; - margin-bottom: 1rem; + max-width: 100%; + overflow-x: auto; + white-space: nowrap; + padding-bottom: 0.5rem; + margin-bottom: 1rem; } .metric-selector .nav-link { - white-space: nowrap; - padding: 0.5rem 1rem; - font-weight: 500; + white-space: nowrap; + padding: 0.5rem 1rem; + font-weight: 500; } .metric-selector .nav-link.active { - background-color: #007bff; - color: white; - border-radius: 0.25rem; + background-color: #007bff; + color: white; + border-radius: 0.25rem; } /*Dashboard loading states*/ .widget-placeholder { - min-height: 300px; - background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%); + min-height: 300px; + background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%); - /* Lighter gradient */ - background-size: 200% 100%; - animation: loading 1.8s infinite ease-in-out; + /* Lighter gradient */ + background-size: 200% 100%; + animation: loading 1.8s infinite ease-in-out; - /* Smoother animation */ - border-radius: 0.5rem; + /* Smoother animation */ + border-radius: 0.5rem; - /* Consistent with cards */ + /* Consistent with cards */ } @keyframes loading { - 0% { - background-position: 200% 0; - } + 0% { + background-position: 200% 0; + } - 100% { - background-position: -200% 0; - } + 100% { + background-position: -200% 0; + } } /*Dashboard empty states*/ .empty-state { - padding: 2.5rem; + padding: 2.5rem; - /* Increased padding */ - text-align: center; - color: #6c757d; - background-color: #f8f9fa; + /* Increased padding */ + text-align: center; + color: #6c757d; + background-color: #f8f9fa; - /* Light background for empty state */ - border-radius: 0.5rem; - border: 1px dashed #ced4da; + /* Light background for empty state */ + border-radius: 0.5rem; + border: 1px dashed #ced4da; - /* Dashed border */ + /* Dashed border */ } .empty-state .empty-state-icon { - font-size: 3.5rem; + font-size: 3.5rem; - /* Larger icon */ - margin-bottom: 1.5rem; - opacity: 0.4; + /* Larger icon */ + margin-bottom: 1.5rem; + opacity: 0.4; } .empty-state .empty-state-message { - font-size: 1.2rem; + font-size: 1.2rem; - /* Slightly larger message */ - margin-bottom: 1.5rem; - font-weight: 500; + /* Slightly larger message */ + margin-bottom: 1.5rem; + font-weight: 500; } .empty-state .btn { - margin-top: 1rem; + margin-top: 1rem; } /*Responsive adjustments*/ @media (width <=767.98px) { - .dashboard-grid { - grid-template-columns: 1fr; - } + .dashboard-grid { + grid-template-columns: 1fr; + } - .stat-card { - padding: 1rem; - } + .stat-card { + padding: 1rem; + } - .stat-card .stat-icon { - font-size: 1.5rem; - width: 3rem; - height: 3rem; - line-height: 3rem; - } + .stat-card .stat-icon { + font-size: 1.5rem; + width: 3rem; + height: 3rem; + line-height: 3rem; + } - .stat-card .stat-value { - font-size: 1.5rem; - } + .stat-card .stat-value { + font-size: 1.5rem; + } } /*Preserve colored background for stat cards in both themes*/ .col-md-3 .card.stats-card.bg-primary { - background-color: var(--bs-primary) !important; - color: white !important; + background-color: var(--bs-primary) !important; + color: white !important; } .col-md-3 .card.stats-card.bg-success { - background-color: var(--bs-success) !important; - color: white !important; + background-color: var(--bs-success) !important; + color: white !important; } .col-md-3 .card.stats-card.bg-info { - background-color: var(--bs-info) !important; - color: white !important; + background-color: var(--bs-info) !important; + color: white !important; } .col-md-3 .card.stats-card.bg-warning { - background-color: var(--bs-warning) !important; - color: white !important; + background-color: var(--bs-warning) !important; + color: white !important; } .col-md-3 .card.stats-card.bg-danger { - background-color: var(--bs-danger) !important; - color: white !important; + background-color: var(--bs-danger) !important; + color: white !important; } .col-md-3 .card.stats-card.bg-secondary { - background-color: var(--bs-secondary) !important; - color: white !important; + background-color: var(--bs-secondary) !important; + color: white !important; } .col-md-3 .card.stats-card.bg-light { - background-color: var(--bs-light) !important; - color: var(--bs-dark) !important; + background-color: var(--bs-light) !important; + color: var(--bs-dark) !important; } /*Stats Cards Alignment Fix (Bottom Align, No Overlap)*/ .stats-row { - display: flex; - flex-wrap: wrap; - gap: 1.5rem; - align-items: stretch; + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + align-items: stretch; } .stats-card { - flex: 1 1 0; - min-width: 200px; - display: flex; - flex-direction: column; - justify-content: flex-end; + flex: 1 1 0; + min-width: 200px; + display: flex; + flex-direction: column; + justify-content: flex-end; - /* Push content to bottom */ - align-items: flex-start; - box-sizing: border-box; + /* Push content to bottom */ + align-items: flex-start; + box-sizing: border-box; - /* Remove min-height/height for natural stretch */ + /* Remove min-height/height for natural stretch */ } diff --git a/dashboard_project/static/css/style.css b/dashboard_project/static/css/style.css index 20c49b1..caf1fb2 100644 --- a/dashboard_project/static/css/style.css +++ b/dashboard_project/static/css/style.css @@ -5,361 +5,361 @@ /*General Styles*/ body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - background-color: #f4f7f9; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + background-color: #f4f7f9; - /* Lighter, cleaner background */ - color: #333; + /* Lighter, cleaner background */ + color: #333; - /* Darker text for better contrast */ - line-height: 1.6; - display: flex; + /* Darker text for better contrast */ + line-height: 1.6; + display: flex; - /* Added for sticky footer */ - flex-direction: column; + /* Added for sticky footer */ + flex-direction: column; - /* Added for sticky footer */ - min-height: 100vh; + /* Added for sticky footer */ + 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 { - 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*/ .text-truncate-2 { - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } .cursor-pointer { - cursor: pointer; + cursor: pointer; } .min-w-150 { - min-width: 150px; + min-width: 150px; } /*Card styles*/ .card { - border: 1px solid #e0e5e9; + border: 1px solid #e0e5e9; - /* Lighter border */ - border-radius: 0.5rem; + /* Lighter border */ + border-radius: 0.5rem; - /* Slightly more rounded corners */ - box-shadow: 0 4px 12px rgb(0 0 0 / 8%); + /* Slightly more rounded corners */ + box-shadow: 0 4px 12px rgb(0 0 0 / 8%); - /* Softer, more modern shadow */ - transition: - transform 0.2s ease-in-out, - box-shadow 0.2s ease-in-out; - margin-bottom: 1.5rem; + /* Softer, more modern shadow */ + transition: + transform 0.2s ease-in-out, + box-shadow 0.2s ease-in-out; + margin-bottom: 1.5rem; - /* Consistent margin */ + /* Consistent margin */ } .card-hover:hover { - transform: translateY(-3px); - box-shadow: 0 6px 16px rgb(0 0 0 / 10%); + transform: translateY(-3px); + box-shadow: 0 6px 16px rgb(0 0 0 / 10%); } .card-header { - background-color: #fff; + background-color: #fff; - /* Clean white header */ - border-bottom: 1px solid #e0e5e9; - font-weight: 500; + /* Clean white header */ + border-bottom: 1px solid #e0e5e9; + font-weight: 500; - /* Slightly bolder header text */ - padding: 0.75rem 1.25rem; + /* Slightly bolder header text */ + padding: 0.75rem 1.25rem; } .card-title { - font-size: 1.15rem; + font-size: 1.15rem; - /* Adjusted card title size */ - font-weight: 600; + /* Adjusted card title size */ + font-weight: 600; } /*Sidebar enhancements*/ .sidebar { - background-color: #fff; + background-color: #fff; - /* White sidebar for a cleaner look */ - border-right: 1px solid #e0e5e9; - box-shadow: 2px 0 5px rgb(0 0 0 / 3%); - transition: all 0.3s; + /* White sidebar for a cleaner look */ + border-right: 1px solid #e0e5e9; + box-shadow: 2px 0 5px rgb(0 0 0 / 3%); + transition: all 0.3s; } .sidebar-sticky { - padding-top: 1rem; + padding-top: 1rem; } .sidebar .nav-link { - color: #4a5568; + color: #4a5568; - /* Softer link color */ - padding: 0.65rem 1.25rem; + /* Softer link color */ + padding: 0.65rem 1.25rem; - /* Adjusted padding */ - border-radius: 0.375rem; + /* Adjusted padding */ + border-radius: 0.375rem; - /* Bootstrap-like rounded corners for links */ - margin: 0.1rem 0.5rem; + /* Bootstrap-like rounded corners for links */ + margin: 0.1rem 0.5rem; - /* Margin around links */ - font-weight: 500; + /* Margin around links */ + font-weight: 500; } .sidebar .nav-link:hover { - color: #007bff; + color: #007bff; - /* Primary color on hover */ - background-color: #e9f2ff; + /* Primary color on hover */ + background-color: #e9f2ff; - /* Light blue background on hover */ + /* Light blue background on hover */ } .sidebar .nav-link.active { - color: #007bff; - background-color: #d6e4ff; + color: #007bff; + background-color: #d6e4ff; - /* Slightly darker blue for active */ - font-weight: 600; + /* Slightly darker blue for active */ + font-weight: 600; } .sidebar .nav-link i.me-2 { - width: 20px; + width: 20px; - /* Ensure icons align well */ - text-align: center; - margin-right: 0.75rem !important; + /* Ensure icons align well */ + text-align: center; + margin-right: 0.75rem !important; - /* Consistent icon spacing */ + /* Consistent icon spacing */ } .sidebar .nav-header { - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: #718096; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #718096; - /* Softer header color */ - padding: 0.5rem 1.25rem; - margin-top: 1rem; + /* Softer header color */ + padding: 0.5rem 1.25rem; + margin-top: 1rem; } /*Dashboard stats cards*/ .stats-card { - border-radius: 0.5rem; - overflow: hidden; + border-radius: 0.5rem; + overflow: hidden; } .stats-card h3 { - font-size: 1.75rem; - font-weight: 600; + font-size: 1.75rem; + font-weight: 600; } .stats-card p { - font-size: 0.875rem; - margin-bottom: 0; - opacity: 0.8; + font-size: 0.875rem; + margin-bottom: 0; + opacity: 0.8; } /*Chart containers*/ .chart-container { - width: 100%; - height: 300px; - position: relative; + width: 100%; + height: 300px; + position: relative; } /*Loading overlay*/ .loading-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgb(255 255 255 / 70%); - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgb(255 255 255 / 70%); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; } /*Table enhancements*/ .table { - border-color: #e0e5e9; + border-color: #e0e5e9; } .table th { - font-weight: 600; + font-weight: 600; - /* Bolder table headers */ - color: #4a5568; - background-color: #f8f9fc; + /* Bolder table headers */ + color: #4a5568; + background-color: #f8f9fc; - /* Light background for headers */ + /* Light background for headers */ } .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 { - background-color: #e9f2ff; + background-color: #e9f2ff; - /* Consistent hover with sidebar */ + /* Consistent hover with sidebar */ } /*Form improvements*/ .form-control, .form-select { - border-color: #ced4da; - border-radius: 0.375rem; + border-color: #ced4da; + border-radius: 0.375rem; - /* Consistent border radius */ - padding: 0.5rem 0.75rem; + /* Consistent border radius */ + padding: 0.5rem 0.75rem; - /* Adjusted padding */ + /* Adjusted padding */ } .form-control:focus, .form-select:focus { - border-color: #86b7fe; + border-color: #86b7fe; - /* Bootstrap focus color */ - box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%); + /* Bootstrap focus color */ + box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%); - /* Bootstrap focus shadow */ + /* Bootstrap focus shadow */ } /*Button styling*/ .btn { - border-radius: 0.375rem; + border-radius: 0.375rem; - /* Consistent border radius */ - padding: 0.5rem 1rem; + /* Consistent border radius */ + padding: 0.5rem 1rem; - /* Standard button padding */ - font-weight: 500; - transition: - background-color 0.15s ease-in-out, - border-color 0.15s ease-in-out, - box-shadow 0.15s ease-in-out; + /* Standard button padding */ + font-weight: 500; + transition: + background-color 0.15s ease-in-out, + border-color 0.15s ease-in-out, + box-shadow 0.15s ease-in-out; } .btn-primary { - background-color: #007bff; - border-color: #007bff; + background-color: #007bff; + border-color: #007bff; } .btn-primary:hover { - background-color: #0069d9; - border-color: #0062cc; + background-color: #0069d9; + border-color: #0062cc; } .btn-secondary { - background-color: #6c757d; - border-color: #6c757d; + background-color: #6c757d; + border-color: #6c757d; } .btn-secondary:hover { - background-color: #5a6268; - border-color: #545b62; + background-color: #5a6268; + border-color: #545b62; } /*Alert styling*/ .alert { - border-radius: 0.375rem; - padding: 0.9rem 1.25rem; + border-radius: 0.375rem; + padding: 0.9rem 1.25rem; } /*Chat transcript styling*/ .chat-transcript { - background-color: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 0.25rem; - padding: 1rem; - max-height: 500px; - overflow-y: auto; - font-size: 0.875rem; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 0.25rem; + padding: 1rem; + max-height: 500px; + overflow-y: auto; + font-size: 0.875rem; } .chat-transcript pre { - white-space: pre-wrap; - font-family: inherit; - margin-bottom: 0; + white-space: pre-wrap; + font-family: inherit; + margin-bottom: 0; } /*Footer styling*/ footer { - background-color: #fff; + background-color: #fff; - /* White footer */ - border-top: 1px solid #e0e5e9; - padding: 1.5rem 0; - color: #6c757d; - font-size: 0.9rem; - margin-top: auto; + /* White footer */ + border-top: 1px solid #e0e5e9; + padding: 1.5rem 0; + color: #6c757d; + font-size: 0.9rem; + margin-top: auto; - /* Added for sticky footer */ + /* Added for sticky footer */ } /*Responsive adjustments*/ @media (width <=767.98px) { - .main-content { - margin-left: 0; - } + .main-content { + margin-left: 0; + } - .stats-card h3 { - font-size: 1.5rem; - } + .stats-card h3 { + font-size: 1.5rem; + } - .chart-container { - height: 250px; - } + .chart-container { + height: 250px; + } - .card-title { - font-size: 1.25rem; - } + .card-title { + font-size: 1.25rem; + } } /*Print styles*/ @media print { - .sidebar, - .navbar, - .btn, - footer { - display: none !important; - } + .sidebar, + .navbar, + .btn, + footer { + display: none !important; + } - .main-content { - margin-left: 0 !important; - padding: 0 !important; - } + .main-content { + margin-left: 0 !important; + padding: 0 !important; + } - .card { - break-inside: avoid; - border: none !important; - box-shadow: none !important; - } + .card { + break-inside: avoid; + border: none !important; + box-shadow: none !important; + } - .chart-container { - break-inside: avoid; - height: auto !important; - } + .chart-container { + break-inside: avoid; + height: auto !important; + } } diff --git a/dashboard_project/static/js/ajax-navigation.js b/dashboard_project/static/js/ajax-navigation.js index f167429..12381f4 100644 --- a/dashboard_project/static/js/ajax-navigation.js +++ b/dashboard_project/static/js/ajax-navigation.js @@ -7,269 +7,268 @@ */ document.addEventListener("DOMContentLoaded", function () { - // Only initialize if AJAX navigation is enabled - if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) { - setupAjaxNavigation(); + // Only initialize if AJAX navigation is enabled + if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) { + 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 = + '
'; + loadingIndicator.style.display = "none"; + loadingIndicator.style.zIndex = "9999"; + document.body.appendChild(loadingIndicator); } - // 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 - ], - }; + // Get the loading indicator element + const loadingIndicator = document.getElementById(config.loadingIndicatorId); - // 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 = - '
'; - loadingIndicator.style.display = "none"; - loadingIndicator.style.zIndex = "9999"; - document.body.appendChild(loadingIndicator); + // Get the main content container + const mainContent = document.querySelector(config.mainContentSelector); + if (!mainContent) { + console.warn("Main content container not found. AJAX navigation disabled."); + return; + } + + // Function to check if a URL should be excluded from AJAX navigation + function shouldExcludeUrl(url) { + for (const pattern of config.excludePatterns) { + if (pattern.test(url)) { + return true; } + } + return false; + } - // Get the loading indicator element - const loadingIndicator = document.getElementById(config.loadingIndicatorId); + // Function to show the loading indicator + function showLoading() { + loadingIndicator.style.display = "block"; + } - // Get the main content container - const mainContent = document.querySelector(config.mainContentSelector); - if (!mainContent) { - console.warn("Main content container not found. AJAX navigation disabled."); - return; - } + // Function to hide the loading indicator + function hideLoading() { + loadingIndicator.style.display = "none"; + } - // Function to check if a URL should be excluded from AJAX navigation - function shouldExcludeUrl(url) { - for (const pattern of config.excludePatterns) { - if (pattern.test(url)) { - return true; - } - } - return false; - } - - // Function to show the loading indicator - function showLoading() { - loadingIndicator.style.display = "block"; - } - - // Function to hide the loading indicator - function hideLoading() { - loadingIndicator.style.display = "none"; - } - - // Function to handle AJAX page navigation - function handlePageNavigation(url, pushState = true) { - if (shouldExcludeUrl(url)) { - window.location.href = url; - return; - } - showLoading(); - const currentScrollPos = window.scrollY; - fetch(url, { - headers: { - "X-Requested-With": "XMLHttpRequest", - "X-AJAX-Navigation": "true", - Accept: "text/html", - }, - }) - .then((response) => { - if (!response.ok) - throw new Error(`Network response was not ok: ${response.status}`); - return response.text(); - }) - .then((html) => { - // Parse the HTML and extract #main-content - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = html; - const newContent = tempDiv.querySelector(config.mainContentSelector); - if (!newContent) throw new Error("Could not find main content in the response"); - mainContent.innerHTML = newContent.innerHTML; - // Update the page title - const titleMatch = html.match(/(.*?)<\/title>/i); - 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 handle AJAX page navigation + function handlePageNavigation(url, pushState = true) { + if (shouldExcludeUrl(url)) { + window.location.href = url; + return; + } + showLoading(); + const currentScrollPos = window.scrollY; + fetch(url, { + headers: { + "X-Requested-With": "XMLHttpRequest", + "X-AJAX-Navigation": "true", + Accept: "text/html", + }, + }) + .then((response) => { + if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`); + return response.text(); + }) + .then((html) => { + // Parse the HTML and extract #main-content + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = html; + const newContent = tempDiv.querySelector(config.mainContentSelector); + if (!newContent) throw new Error("Could not find main content in the response"); + mainContent.innerHTML = newContent.innerHTML; + // Update the page title + const titleMatch = html.match(/<title>(.*?)<\/title>/i); + 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); + } + }); + } }); diff --git a/dashboard_project/static/js/ajax-pagination.js b/dashboard_project/static/js/ajax-pagination.js index 13176a4..c29d2ac 100644 --- a/dashboard_project/static/js/ajax-pagination.js +++ b/dashboard_project/static/js/ajax-pagination.js @@ -7,101 +7,101 @@ */ document.addEventListener("DOMContentLoaded", function () { - // Initialize AJAX pagination - setupAjaxPagination(); + // Initialize AJAX pagination + setupAjaxPagination(); - // Function to set up AJAX pagination for the entire application - function setupAjaxPagination() { - // Configuration - can be customized per page if needed - const config = { - contentContainerId: "ajax-content-container", // ID of the container to update - loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner - paginationLinkClass: "pagination-link", // Class for pagination links - retryMessage: "An error occurred while loading data. Please try again.", - }; + // Function to set up AJAX pagination for the entire application + function setupAjaxPagination() { + // Configuration - can be customized per page if needed + const config = { + contentContainerId: "ajax-content-container", // ID of the container to update + loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner + paginationLinkClass: "pagination-link", // Class for pagination links + retryMessage: "An error occurred while loading data. Please try again.", + }; - // Get container elements - const contentContainer = document.getElementById(config.contentContainerId); - const loadingSpinner = document.getElementById(config.loadingSpinnerId); + // Get container elements + const contentContainer = document.getElementById(config.contentContainerId); + const loadingSpinner = document.getElementById(config.loadingSpinnerId); - // Exit if the page doesn't have the required elements - if (!contentContainer || !loadingSpinner) return; + // Exit if the page doesn't have the required elements + if (!contentContainer || !loadingSpinner) return; - // Function to handle pagination clicks - function setupPaginationListeners() { - document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => { - link.addEventListener("click", function (e) { - e.preventDefault(); - handleAjaxNavigation(this.href); + // Function to handle pagination clicks + function setupPaginationListeners() { + document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => { + link.addEventListener("click", function (e) { + e.preventDefault(); + handleAjaxNavigation(this.href); - // Get the page number if available - const page = this.getAttribute("data-page"); + // Get the page number if available + const page = this.getAttribute("data-page"); - // Update browser URL without refreshing - const newUrl = this.href; - history.pushState({ url: newUrl, page: page }, "", newUrl); - }); - }); - } + // Update browser URL without refreshing + const newUrl = this.href; + history.pushState({ url: newUrl, page: page }, "", newUrl); + }); + }); + } - // Function to handle AJAX navigation - function handleAjaxNavigation(url) { - // Show loading spinner - contentContainer.classList.add("d-none"); - loadingSpinner.classList.remove("d-none"); + // Function to handle AJAX navigation + function handleAjaxNavigation(url) { + // Show loading spinner + contentContainer.classList.add("d-none"); + loadingSpinner.classList.remove("d-none"); - // Fetch data via AJAX - fetch(url, { - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok: ${response.status}`); - } - return response.json(); - }) - .then((data) => { - if (data.status === "success") { - // Update the content - contentContainer.innerHTML = data.html_data; + // Fetch data via AJAX + fetch(url, { + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + if (data.status === "success") { + // Update the content + contentContainer.innerHTML = data.html_data; - // Re-attach event listeners to new pagination links - setupPaginationListeners(); + // Re-attach event listeners to new pagination links + setupPaginationListeners(); - // Update any summary data if present and the page provides it - if (typeof updateSummary === "function" && data.summary) { - 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); + // Update any summary data if present and the page provides it + if (typeof updateSummary === "function" && data.summary) { + 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); + } + }); + } }); diff --git a/dashboard_project/static/js/dashboard.js b/dashboard_project/static/js/dashboard.js index 8ba13fa..5ce31c2 100644 --- a/dashboard_project/static/js/dashboard.js +++ b/dashboard_project/static/js/dashboard.js @@ -8,478 +8,469 @@ */ document.addEventListener("DOMContentLoaded", function () { - // Set up Plotly default config based on theme - function updatePlotlyTheme() { - // Force a fresh check of the current theme - const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark"; - console.log( - "updatePlotlyTheme called - Current theme mode:", - isDarkMode ? "dark" : "light", - ); + // Set up Plotly default config based on theme + function updatePlotlyTheme() { + // Force a fresh check of the current theme + const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark"; + console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light"); - window.plotlyDefaultLayout = { - font: { - color: isDarkMode ? "#f8f9fa" : "#212529", - family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', - }, - paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff", - plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff", - colorway: [ - "#4285F4", - "#EA4335", - "#FBBC05", - "#34A853", - "#FF6D00", - "#46BDC6", - "#DB4437", - "#0F9D58", - "#AB47BC", - "#00ACC1", - ], - margin: { - l: 50, - r: 30, - t: 30, - b: 50, - pad: 10, - }, - hovermode: "closest", - xaxis: { - automargin: true, - gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", - zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", - title: { - font: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - tickfont: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - yaxis: { - automargin: true, - gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", - zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", - title: { - font: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - tickfont: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - legend: { - font: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - 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)", - 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); + window.plotlyDefaultLayout = { + font: { + color: isDarkMode ? "#f8f9fa" : "#212529", + family: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + }, + paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff", + plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff", + colorway: [ + "#4285F4", + "#EA4335", + "#FBBC05", + "#34A853", + "#FF6D00", + "#46BDC6", + "#DB4437", + "#0F9D58", + "#AB47BC", + "#00ACC1", + ], + margin: { + l: 50, + r: 30, + t: 30, + b: 50, + pad: 10, + }, + hovermode: "closest", + xaxis: { + automargin: true, + gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", + zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", + title: { + font: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, + }, + tickfont: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, + }, + yaxis: { + automargin: true, + gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", + zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", + title: { + font: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, + }, + tickfont: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, + }, + legend: { + font: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, + 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)", + color: isDarkMode ? "#f8f9fa" : "#212529", + activecolor: isDarkMode ? "#6ea8fe" : "#007bff", + }, }; - // Time range filtering - const timeRangeDropdown = document.getElementById("timeRangeDropdown"); - if (timeRangeDropdown) { - const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item"); - timeRangeLinks.forEach((link) => { - 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"); + // Config for specific chart types + window.plotlyBarConfig = { + ...window.plotlyDefaultLayout, + bargap: 0.1, + bargroupgap: 0.2, + }; - // Fetch updated data via AJAX - if (dashboardId) { - fetchDashboardData(dashboardId, timeRange); - e.preventDefault(); - } - }); + 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(); } + }); - // Function to fetch dashboard data - function fetchDashboardData(dashboardId, timeRange) { - const loadingOverlay = document.createElement("div"); - loadingOverlay.className = "loading-overlay"; - loadingOverlay.innerHTML = - '<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>'; - document.querySelector("main").appendChild(loadingOverlay); + // Call resizeCharts on initial load + if (window.Plotly) { + // Use a longer delay to ensure charts are fully loaded + setTimeout(function () { + updatePlotlyTheme(); + refreshAllCharts(); + }, 300); + } - 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); + // 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); + }; - // 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); + // Time range filtering + const timeRangeDropdown = document.getElementById("timeRangeDropdown"); + if (timeRangeDropdown) { + const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item"); + timeRangeLinks.forEach((link) => { + 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"); - document.querySelector(".loading-overlay").remove(); - }) - .catch((error) => { - console.error("Error fetching dashboard data:", error); - document.querySelector(".loading-overlay").remove(); + // Fetch updated data via AJAX + if (dashboardId) { + fetchDashboardData(dashboardId, timeRange); + e.preventDefault(); + } + }); + }); + } - // Show error message - const alertElement = document.createElement("div"); - alertElement.className = "alert alert-danger alert-dismissible fade show"; - alertElement.setAttribute("role", "alert"); - alertElement.innerHTML = ` + // Function to fetch dashboard data + function fetchDashboardData(dashboardId, timeRange) { + const loadingOverlay = document.createElement("div"); + loadingOverlay.className = "loading-overlay"; + 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. <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 - function updateDashboardStats(data) { - // Update total sessions - const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3"); - 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; - } + // Update average response time + const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3"); + if (avgResponseTimeElement) { + avgResponseTimeElement.textContent = data.avg_response_time + "s"; } - // Function to update dashboard charts - function updateDashboardCharts(data) { - // Check if Plotly is available - 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 }, - }, - ); - } + // Update total tokens + const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3"); + if (totalTokensElement) { + totalTokensElement.textContent = data.total_tokens; } - // 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"); + // Update total cost + const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3"); + if (totalCostElement) { + totalCostElement.textContent = "€" + data.total_cost; + } + } - // Fetch updated data via AJAX - if (dashboardId) { - fetchDashboardData(dashboardId); - e.preventDefault(); - } - }); + // Function to update dashboard charts + function updateDashboardCharts(data) { + // Check if Plotly is available + 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(); + } }); + }); }); diff --git a/dashboard_project/static/js/main.js b/dashboard_project/static/js/main.js index 78625fe..4f10e8f 100644 --- a/dashboard_project/static/js/main.js +++ b/dashboard_project/static/js/main.js @@ -7,241 +7,241 @@ */ document.addEventListener("DOMContentLoaded", function () { - // Initialize tooltips - var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); - var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl); + // Initialize tooltips + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + var tooltipList = tooltipTriggerList.map(function (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 - var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); - var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { - return new bootstrap.Popover(popoverTriggerEl); + // Auto-dismiss alerts after 5 seconds + setTimeout(function () { + var alerts = document.querySelectorAll(".alert:not(.alert-important)"); + alerts.forEach(function (alert) { + if (alert && bootstrap.Alert.getInstance(alert)) { + bootstrap.Alert.getInstance(alert).close(); + } }); + }, 5000); - // Toggle sidebar on mobile - const sidebarToggle = document.querySelector("#sidebarToggle"); - if (sidebarToggle) { - sidebarToggle.addEventListener("click", function () { - document.querySelector(".sidebar").classList.toggle("show"); - }); - } + // 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, + ); + }); - // Auto-dismiss alerts after 5 seconds - setTimeout(function () { - var alerts = document.querySelectorAll(".alert:not(.alert-important)"); - alerts.forEach(function (alert) { - if (alert && bootstrap.Alert.getInstance(alert)) { - bootstrap.Alert.getInstance(alert).close(); - } - }); - }, 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 + const confirmButtons = document.querySelectorAll("[data-confirm]"); + confirmButtons.forEach(function (button) { + button.addEventListener("click", function (event) { + if (!confirm(this.dataset.confirm || "Are you sure?")) { + event.preventDefault(); + } }); + }); - // Confirm dialogs - const confirmButtons = document.querySelectorAll("[data-confirm]"); - confirmButtons.forEach(function (button) { - button.addEventListener("click", function (event) { - if (!confirm(this.dataset.confirm || "Are you sure?")) { - event.preventDefault(); - } - }); + // Back button + const backButtons = document.querySelectorAll(".btn-back"); + backButtons.forEach(function (button) { + button.addEventListener("click", function (event) { + event.preventDefault(); + window.history.back(); }); + }); - // Back button - const backButtons = document.querySelectorAll(".btn-back"); - backButtons.forEach(function (button) { - button.addEventListener("click", function (event) { - event.preventDefault(); - window.history.back(); - }); + // File input customization + const fileInputs = document.querySelectorAll(".custom-file-input"); + fileInputs.forEach(function (input) { + input.addEventListener("change", function (e) { + const fileName = this.files[0]?.name || "Choose file"; + const nextSibling = this.nextElementSibling; + if (nextSibling) { + nextSibling.innerText = fileName; + } }); + }); - // File input customization - const fileInputs = document.querySelectorAll(".custom-file-input"); - fileInputs.forEach(function (input) { - input.addEventListener("change", function (e) { - const fileName = this.files[0]?.name || "Choose file"; - const nextSibling = this.nextElementSibling; - if (nextSibling) { - nextSibling.innerText = fileName; - } - }); + // Search form submit on enter + const searchInputs = document.querySelectorAll(".search-input"); + searchInputs.forEach(function (input) { + input.addEventListener("keypress", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + this.closest("form").submit(); + } }); + }); - // Search form submit on enter - const searchInputs = document.querySelectorAll(".search-input"); - searchInputs.forEach(function (input) { - input.addEventListener("keypress", function (e) { - if (e.key === "Enter") { - e.preventDefault(); - this.closest("form").submit(); - } - }); + // Toggle password visibility + const togglePasswordButtons = document.querySelectorAll(".toggle-password"); + togglePasswordButtons.forEach(function (button) { + button.addEventListener("click", function () { + const target = document.querySelector(this.dataset.target); + 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"); + } }); + }); - // Toggle password visibility - const togglePasswordButtons = document.querySelectorAll(".toggle-password"); - togglePasswordButtons.forEach(function (button) { - button.addEventListener("click", function () { - const target = document.querySelector(this.dataset.target); - 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 + const dropdowns = document.querySelectorAll(".dropdown-menu"); + dropdowns.forEach(function (dropdown) { + dropdown.addEventListener("click", function (e) { + e.stopPropagation(); }); + }); - // Dropdown menu positioning - const dropdowns = document.querySelectorAll(".dropdown-menu"); - dropdowns.forEach(function (dropdown) { - dropdown.addEventListener("click", function (e) { - e.stopPropagation(); - }); + // Responsive table handling + const tables = document.querySelectorAll(".table-responsive"); + if (window.innerWidth < 768) { + tables.forEach(function (table) { + table.classList.add("table-responsive-force"); }); + } - // Responsive table handling - const tables = document.querySelectorAll(".table-responsive"); + // Handle special links (printable views, exports) + 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) { - tables.forEach(function (table) { - table.classList.add("table-responsive-force"); - }); + 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"); } - // Handle special links (printable views, exports) - 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 + // Update toggle button icon 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 - }); + 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"); + 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 + }); + } }); diff --git a/package.json b/package.json index a0a1fc4..3f82792 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,26 @@ { - "scripts": { - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint:md": "markdownlint-cli2 \"**/*.md\"", - "lint:md:fix": "bun lint:md -- --fix" + "scripts": { + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint:md": "markdownlint-cli2 \"**/*.md\"", + "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": { - "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 - }, - "ignores": [ - ".git", - ".trunk", - ".venv", - "node_modules" - ] - } + "ignores": [ + ".git", + ".trunk", + ".venv", + "node_modules" + ] + } } diff --git a/pyproject.toml b/pyproject.toml index 7680220..04b3584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,13 @@ dependencies = [ "Documentation" = "https://github.com/kjanat/livegraphsdjango#readme" "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] dev = [ "bandit>=1.8.3", @@ -52,6 +59,7 @@ dev = [ "pytest>=8.3.5", "pytest-django>=4.11.1", "ruff>=0.11.10", + "ty>=0.0.1a25", ] [build-system] @@ -165,4 +173,9 @@ line-ending = "lf" packages = ["dashboard_project"] [tool.setuptools.package-data] -"dashboard_project" = ["static/__/*", "templates/__/*", "media/**/*"] +"dashboard_project" = [ + "static/**/*", + "templates/**/*", + "media/**/*", + "py.typed" +] diff --git a/requirements.txt b/requirements.txt index 9b7a360..0dd33a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -547,6 +547,25 @@ tinycss2==1.4.0 \ # via # bleach # 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 \ --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ --hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6 diff --git a/ty.toml b/ty.toml new file mode 100644 index 0000000..4607353 --- /dev/null +++ b/ty.toml @@ -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 diff --git a/uv.lock b/uv.lock index 4c295d1..0570b23 100644 --- a/uv.lock +++ b/uv.lock @@ -563,6 +563,7 @@ dev = [ { name = "pytest" }, { name = "pytest-django" }, { name = "ruff" }, + { name = "ty" }, ] [package.metadata] @@ -599,6 +600,7 @@ dev = [ { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-django", specifier = ">=4.11.1" }, { name = "ruff", specifier = ">=0.11.10" }, + { name = "ty", specifier = ">=0.0.1a25" }, ] [[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" }, ] +[[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]] name = "types-pyyaml" version = "6.0.12.20250915"