diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dd8446f..23b4540 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -13,21 +13,25 @@ default_language_version:
repos:
- repo: https://github.com/adamchainz/django-upgrade
- rev: 1.25.0
+ rev: 1.29.1
hooks:
- id: django-upgrade
# uv hooks for dependency management
- repo: https://github.com/astral-sh/uv-pre-commit
- rev: 0.7.12
+ rev: 0.9.7
hooks:
+ # Update the uv lockfile
+ - id: uv-lock
+ # Update the requirements.txt
- id: uv-export
# Standard pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
+ args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-yaml
# - id: check-json
@@ -50,12 +54,12 @@ repos:
- prettier
- prettier-plugin-jinja-template
types_or: [html, jinja]
- entry: npx prettier --plugin=prettier-plugin-jinja-template --parser=jinja-template --write
+ entry: bunx prettier --plugin=prettier-plugin-jinja-template --parser=jinja-template --write
- id: prettier-all
name: Prettier All
language: node
types_or: [javascript, jsx, ts, tsx, css, scss, json, yaml, markdown]
- entry: npx prettier --write
+ entry: bunx prettier --write
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.18.1
@@ -65,40 +69,49 @@ repos:
# Ruff for linting and formatting
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.11.13
+ rev: v0.14.3
hooks:
- - id: ruff
+ # Run the linter.
+ - id: ruff-check
args: [--fix]
+ # Run the formatter.
- id: ruff-format
- # Django-specific hooks
- - repo: local
- hooks:
- - id: django-check
- name: Django Check
- entry: uv run python dashboard_project/manage.py check
- language: python
- pass_filenames: false
- types: [python]
- always_run: true
- additional_dependencies: [uv]
+ # # Django-specific hooks
+ # - repo: local
+ # hooks:
+ # - id: django-check
+ # name: Django Check
+ # entry: uv run python dashboard_project/manage.py check
+ # language: python
+ # pass_filenames: false
+ # types: [python]
+ # always_run: true
+ # additional_dependencies: [uv]
- - id: django-check-migrations
- name: Django Check Migrations
- entry: uv run python dashboard_project/manage.py makemigrations --check --dry-run
- language: python
- pass_filenames: false
- types: [python]
- additional_dependencies: [uv]
+ # - id: django-check-migrations
+ # name: Django Check Migrations
+ # entry: uv run python dashboard_project/manage.py makemigrations --check --dry-run
+ # language: python
+ # pass_filenames: false
+ # types: [python]
+ # additional_dependencies: [uv]
# Security checks
- repo: https://github.com/pycqa/bandit
- rev: 1.8.3
+ rev: 1.8.6
hooks:
- id: bandit
args: [-c, pyproject.toml, -r, dashboard_project]
# additional_dependencies: ["bandit[toml]"]
+ # TOML formatting and linting
+ - repo: https://github.com/tombi-toml/tombi-pre-commit
+ rev: v0.6.40
+ hooks:
+ - id: tombi-format
+ - id: tombi-lint
+
# # Type checking
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.15.0
diff --git a/.prettierrc b/.prettierrc
index 98b51e3..f7a1bd8 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,4 +1,5 @@
{
+ "$schema": "https://json.schemastore.org/prettierrc.json",
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
@@ -29,5 +30,5 @@
}
}
],
- "plugins": ["prettier-plugin-jinja-template"]
+ "plugins": ["prettier-plugin-jinja-template", "prettier-plugin-packagejson"]
}
diff --git a/Dockerfile b/Dockerfile
index 262ad61..bce041a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,32 +3,41 @@
FROM python:3.13-slim
# Set environment variables
+
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV DJANGO_SETTINGS_MODULE=dashboard_project.settings
# Set work directory
+
WORKDIR /app
# Install UV for Python package management
+
RUN pip install uv
# Copy project files
+
COPY pyproject.toml .
COPY uv.lock .
COPY . .
# Install dependencies
+
RUN uv pip install -e .
# Change to the Django project directory
+
WORKDIR /app/dashboard_project
# Collect static files
+
RUN python manage.py collectstatic --noinput
# Change back to the app directory
+
WORKDIR /app
# Run gunicorn
+
CMD ["gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000"]
diff --git a/Makefile b/Makefile
index d5e4256..cfb6067 100644
--- a/Makefile
+++ b/Makefile
@@ -1,124 +1,148 @@
.PHONY: venv install install-dev lint test format clean run migrate makemigrations superuser setup-node celery celery-beat docker-build docker-up docker-down reset-db setup-dev procfile
# Create a virtual environment
+
venv:
- uv venv -p 3.13
+ uv venv -p 3.13
# Install production dependencies
+
install:
- uv pip install -e .
+ uv pip install -e .
# Install development dependencies
+
install-dev:
- uv pip install -e ".[dev]"
+ uv pip install -e ".[dev]"
# Run linting
+
lint:
- uv run -m ruff check dashboard_project
+ uv run -m ruff check dashboard_project
# Run tests
+
test:
- uv run -m pytest
+ uv run -m pytest
# Format Python code
+
format:
- uv run -m ruff format dashboard_project
- uv run -m black dashboard_project
+ uv run -m ruff format dashboard_project
+ uv run -m black dashboard_project
# Setup Node.js dependencies
+
setup-node:
- npm install --include=dev
+ npm install --include=dev
# Clean Python cache files
+
clean:
- find . -type d -name "__pycache__" -exec rm -rf {} +
- find . -type f -name "*.pyc" -delete
- find . -type f -name "*.pyo" -delete
- find . -type f -name "*.pyd" -delete
- find . -type d -name "*.egg-info" -exec rm -rf {} +
- find . -type d -name "*.egg" -exec rm -rf {} +
- find . -type d -name ".pytest_cache" -exec rm -rf {} +
- find . -type d -name ".coverage" -exec rm -rf {} +
- find . -type d -name "htmlcov" -exec rm -rf {} +
- find . -type d -name ".ruff_cache" -exec rm -rf {} +
- find . -type d -name ".mypy_cache" -exec rm -rf {} +
- find . -type d -name ".tox" -exec rm -rf {} +
- find . -type d -name "node_modules" -exec rm -rf {} +
- rm -rf build/
- rm -rf dist/
+ find . -type d -name "__pycache__" -exec rm -rf {} +
+ find . -type f -name "*.pyc" -delete
+ find . -type f -name "*.pyo" -delete
+ find . -type f -name "*.pyd" -delete
+ find . -type d -name "*.egg-info" -exec rm -rf {} +
+ find . -type d -name "*.egg" -exec rm -rf {} +
+ find . -type d -name ".pytest_cache" -exec rm -rf {} +
+ find . -type d -name ".coverage" -exec rm -rf {} +
+ find . -type d -name "htmlcov" -exec rm -rf {} +
+ find . -type d -name ".ruff_cache" -exec rm -rf {} +
+ find . -type d -name ".mypy_cache" -exec rm -rf {} +
+ find . -type d -name ".tox" -exec rm -rf {} +
+ find . -type d -name "node_modules" -exec rm -rf {} +
+ rm -rf build/
+ rm -rf dist/
# Run the development server
+
run:
- cd dashboard_project && uv run python manage.py runserver 8001
+ cd dashboard_project && uv run python manage.py runserver 8001
# Run Celery worker for background tasks
+
celery:
- cd dashboard_project && uv run celery -A dashboard_project worker --loglevel=info
+ cd dashboard_project && uv run celery -A dashboard_project worker --loglevel=info
# Run Celery Beat for scheduled tasks
+
celery-beat:
- cd dashboard_project && uv run celery -A dashboard_project beat --scheduler django_celery_beat.schedulers:DatabaseScheduler
+ cd dashboard_project && uv run celery -A dashboard_project beat --scheduler django_celery_beat.schedulers:DatabaseScheduler
# Apply migrations
+
migrate:
- cd dashboard_project && uv run python manage.py migrate
+ cd dashboard_project && uv run python manage.py migrate
# Create migrations
+
makemigrations:
- cd dashboard_project && uv run python manage.py makemigrations
+ cd dashboard_project && uv run python manage.py makemigrations
# Create a superuser
+
superuser:
- cd dashboard_project && uv run python manage.py createsuperuser
+ cd dashboard_project && uv run python manage.py createsuperuser
# Update uv lock file
+
lock:
- uv pip freeze > requirements.lock
+ uv pip freeze > requirements.lock
# Setup pre-commit hooks
+
setup-pre-commit:
- pre-commit install
+ pre-commit install
# Run pre-commit on all files
+
lint-all:
- pre-commit run --all-files
+ pre-commit run --all-files
# Docker commands
+
docker-build:
- docker-compose build
+ docker-compose build
docker-up:
- docker-compose up -d
+ docker-compose up -d
docker-down:
- docker-compose down
+ docker-compose down
# Initialize or reset the database in development
+
reset-db:
- cd dashboard_project && uv run python manage.py flush --no-input
- cd dashboard_project && uv run python manage.py migrate
+ cd dashboard_project && uv run python manage.py flush --no-input
+ cd dashboard_project && uv run python manage.py migrate
# Start a Redis server in development (if not installed, fallback to SQLite)
+
run-redis:
- redis-server || echo "Redis not installed, using SQLite fallback"
+ redis-server || echo "Redis not installed, using SQLite fallback"
# Start all development services (web, redis, celery, celery-beat)
+
run-all:
- foreman start
+ foreman start
procfile:
- foreman start
+ foreman start
# Test Celery task
+
test-celery:
- cd dashboard_project && uv run python manage.py test_celery
+ cd dashboard_project && uv run python manage.py test_celery
# Initialize data integration
+
init-data-integration:
- cd dashboard_project && uv run python manage.py create_default_datasource
- cd dashboard_project && uv run python manage.py create_default_datasource
- cd dashboard_project && uv run python manage.py test_celery
+ cd dashboard_project && uv run python manage.py create_default_datasource
+ cd dashboard_project && uv run python manage.py create_default_datasource
+ cd dashboard_project && uv run python manage.py test_celery
# Setup development environment
+
setup-dev: venv install-dev migrate create_default_datasource
- @echo "Development environment setup complete"
+ @echo "Development environment setup complete"
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..bb52be8
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,204 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "devDependencies": {
+ "markdownlint-cli2": "^0.18.1",
+ "prettier": "^3.6.2",
+ "prettier-plugin-jinja-template": "^2.1.0",
+ "prettier-plugin-packagejson": "^2.5.19",
+ },
+ },
+ },
+ "packages": {
+ "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
+
+ "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
+
+ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
+
+ "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
+
+ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
+
+ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
+
+ "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
+ "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
+ "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="],
+
+ "detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
+ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
+
+ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="],
+
+ "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="],
+
+ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
+
+ "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
+
+ "katex": ["katex@0.16.25", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q=="],
+
+ "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
+
+ "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
+
+ "markdownlint": ["markdownlint@0.38.0", "", { "dependencies": { "micromark": "4.0.2", "micromark-core-commonmark": "2.0.3", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", "micromark-util-types": "2.0.2" } }, "sha512-xaSxkaU7wY/0852zGApM8LdlIfGCW8ETZ0Rr62IQtAnUMlMuifsg09vWJcNYeL4f0anvr8Vo4ZQar8jGpV0btQ=="],
+
+ "markdownlint-cli2": ["markdownlint-cli2@0.18.1", "", { "dependencies": { "globby": "14.1.0", "js-yaml": "4.1.0", "jsonc-parser": "3.3.1", "markdown-it": "14.1.0", "markdownlint": "0.38.0", "markdownlint-cli2-formatter-default": "0.0.5", "micromatch": "4.0.8" }, "bin": { "markdownlint-cli2": "markdownlint-cli2-bin.mjs" } }, "sha512-/4Osri9QFGCZOCTkfA8qJF+XGjKYERSHkXzxSyS1hd3ZERJGjvsUao2h4wdnvpHp6Tu2Jh/bPHM0FE9JJza6ng=="],
+
+ "markdownlint-cli2-formatter-default": ["markdownlint-cli2-formatter-default@0.0.5", "", { "peerDependencies": { "markdownlint-cli2": ">=0.0.4" } }, "sha512-4XKTwQ5m1+Txo2kuQ3Jgpo/KmnG+X90dWt4acufg6HVGadTUG5hzHF/wssp9b5MBYOMCnZ9RMPaU//uHsszF8Q=="],
+
+ "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
+
+ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
+
+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+
+ "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="],
+
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
+
+ "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
+
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
+ "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="],
+
+ "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
+
+ "prettier-plugin-jinja-template": ["prettier-plugin-jinja-template@2.1.0", "", { "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-mzoCp2Oy9BDSug80fw3B3J4n4KQj1hRvoQOL1akqcDKBb5nvYxrik9zUEDs4AEJ6nK7QDTGoH0y9rx7AlnQ78Q=="],
+
+ "prettier-plugin-packagejson": ["prettier-plugin-packagejson@2.5.19", "", { "dependencies": { "sort-package-json": "3.4.0", "synckit": "0.11.11" }, "peerDependencies": { "prettier": ">= 1.16.0" }, "optionalPeers": ["prettier"] }, "sha512-Qsqp4+jsZbKMpEGZB1UP1pxeAT8sCzne2IwnKkr+QhUe665EXUo3BAvTf1kAPCqyMv9kg3ZmO0+7eOni/C6Uag=="],
+
+ "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
+
+ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
+
+ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
+
+ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
+
+ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+ "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
+
+ "sort-object-keys": ["sort-object-keys@1.1.3", "", {}, "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg=="],
+
+ "sort-package-json": ["sort-package-json@3.4.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "git-hooks-list": "^4.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA=="],
+
+ "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
+
+ "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
+
+ "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+ }
+}
diff --git a/dashboard_project/__main__.py b/dashboard_project/__main__.py
index 1327369..926d054 100644
--- a/dashboard_project/__main__.py
+++ b/dashboard_project/__main__.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+# !/usr/bin/env python
"""
Entry point for Django commands executed as Python modules.
This enables commands like `python -m runserver`.
diff --git a/dashboard_project/asgi.py b/dashboard_project/asgi.py
index 78c9588..b1497b1 100644
--- a/dashboard_project/asgi.py
+++ b/dashboard_project/asgi.py
@@ -4,7 +4,7 @@ ASGI config for dashboard_project project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
-https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
+
"""
import os
diff --git a/dashboard_project/dashboard/management/__init__.py b/dashboard_project/dashboard/management/__init__.py
index 5a95e76..319959a 100644
--- a/dashboard_project/dashboard/management/__init__.py
+++ b/dashboard_project/dashboard/management/__init__.py
@@ -1,2 +1,3 @@
# dashboard/management/__init__.py
+
# This file is intentionally left empty to mark the directory as a Python package
diff --git a/dashboard_project/dashboard/management/commands/__init__.py b/dashboard_project/dashboard/management/commands/__init__.py
index c095440..cec16b7 100644
--- a/dashboard_project/dashboard/management/commands/__init__.py
+++ b/dashboard_project/dashboard/management/commands/__init__.py
@@ -1,2 +1,3 @@
# dashboard/management/commands/__init__.py
+
# This file is intentionally left empty to mark the directory as a Python package
diff --git a/dashboard_project/dashboard/templatetags/__init__.py b/dashboard_project/dashboard/templatetags/__init__.py
index db9731b..d797df1 100644
--- a/dashboard_project/dashboard/templatetags/__init__.py
+++ b/dashboard_project/dashboard/templatetags/__init__.py
@@ -1,2 +1,3 @@
# dashboard/templatetags/__init__.py
+
# This file is intentionally left empty to mark the directory as a Python package
diff --git a/dashboard_project/dashboard/views.py b/dashboard_project/dashboard/views.py
index 401811b..5497be7 100644
--- a/dashboard_project/dashboard/views.py
+++ b/dashboard_project/dashboard/views.py
@@ -200,12 +200,10 @@ def chat_session_detail_view(request, session_id):
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/chat_session_detail.html", context, request=request)
- return JsonResponse(
- {
- "html": html_content,
- "title": f"Chat Session {session_id} | Chat Analytics",
- }
- )
+ return JsonResponse({
+ "html": html_content,
+ "title": f"Chat Session {session_id} | Chat Analytics",
+ })
return render(request, "dashboard/chat_session_detail.html", context)
@@ -282,12 +280,10 @@ def edit_dashboard_view(request, dashboard_id):
# Check if this is an AJAX navigation request
if is_ajax_navigation(request):
html_content = render_to_string("dashboard/dashboard_form.html", context, request=request)
- return JsonResponse(
- {
- "html": html_content,
- "title": f"Edit Dashboard: {dashboard.name} | Chat Analytics",
- }
- )
+ return JsonResponse({
+ "html": html_content,
+ "title": f"Edit Dashboard: {dashboard.name} | Chat Analytics",
+ })
return render(request, "dashboard/dashboard_form.html", context)
@@ -349,6 +345,8 @@ def delete_data_source_view(request, data_source_id):
# API views for dashboard data
+
+
@login_required
def dashboard_data_api(request, dashboard_id):
"""API endpoint for dashboard data"""
@@ -450,26 +448,24 @@ def search_chat_sessions(request):
# Check if this is an AJAX pagination request
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
- return JsonResponse(
- {
- "status": "success",
- "html_data": render(request, "dashboard/partials/search_results_table.html", context).content.decode(
- "utf-8"
- ),
- "page_obj": {
- "number": page_obj.number,
- "has_previous": page_obj.has_previous(),
- "has_next": page_obj.has_next(),
- "previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
- "next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
- "paginator": {
- "num_pages": page_obj.paginator.num_pages,
- "count": page_obj.paginator.count,
- },
+ return JsonResponse({
+ "status": "success",
+ "html_data": render(request, "dashboard/partials/search_results_table.html", context).content.decode(
+ "utf-8"
+ ),
+ "page_obj": {
+ "number": page_obj.number,
+ "has_previous": page_obj.has_previous(),
+ "has_next": page_obj.has_next(),
+ "previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
+ "next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
+ "paginator": {
+ "num_pages": page_obj.paginator.num_pages,
+ "count": page_obj.paginator.count,
},
- "query": query,
- }
- )
+ },
+ "query": query,
+ })
return render(request, "dashboard/search_results.html", context)
@@ -554,26 +550,24 @@ def data_view(request):
# Check if this is an AJAX pagination request
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
- return JsonResponse(
- {
- "status": "success",
- "html_data": render(request, "dashboard/partials/data_table.html", context).content.decode("utf-8"),
- "page_obj": {
- "number": page_obj.number,
- "has_previous": page_obj.has_previous(),
- "has_next": page_obj.has_next(),
- "previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
- "next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
- "paginator": {
- "num_pages": page_obj.paginator.num_pages,
- "count": page_obj.paginator.count,
- },
+ return JsonResponse({
+ "status": "success",
+ "html_data": render(request, "dashboard/partials/data_table.html", context).content.decode("utf-8"),
+ "page_obj": {
+ "number": page_obj.number,
+ "has_previous": page_obj.has_previous(),
+ "has_next": page_obj.has_next(),
+ "previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
+ "next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
+ "paginator": {
+ "num_pages": page_obj.paginator.num_pages,
+ "count": page_obj.paginator.count,
},
- "view": view,
- "avg_response_time": avg_response_time,
- "avg_messages": avg_messages,
- "escalation_rate": escalation_rate,
- }
- )
+ },
+ "view": view,
+ "avg_response_time": avg_response_time,
+ "avg_messages": avg_messages,
+ "escalation_rate": escalation_rate,
+ })
return render(request, "dashboard/data_view.html", context)
diff --git a/dashboard_project/dashboard/views_export.py b/dashboard_project/dashboard/views_export.py
index f43510c..8259cb5 100644
--- a/dashboard_project/dashboard/views_export.py
+++ b/dashboard_project/dashboard/views_export.py
@@ -91,51 +91,47 @@ def export_chats_csv(request):
writer = csv.writer(response)
# Write CSV header
- writer.writerow(
- [
- "Session ID",
- "Start Time",
- "End Time",
- "IP Address",
- "Country",
- "Language",
- "Messages Sent",
- "Sentiment",
- "Escalated",
- "Forwarded HR",
- "Full Transcript",
- "Avg Response Time (s)",
- "Tokens",
- "Tokens EUR",
- "Category",
- "Initial Message",
- "User Rating",
- ]
- )
+ writer.writerow([
+ "Session ID",
+ "Start Time",
+ "End Time",
+ "IP Address",
+ "Country",
+ "Language",
+ "Messages Sent",
+ "Sentiment",
+ "Escalated",
+ "Forwarded HR",
+ "Full Transcript",
+ "Avg Response Time (s)",
+ "Tokens",
+ "Tokens EUR",
+ "Category",
+ "Initial Message",
+ "User Rating",
+ ])
# Write data rows
for session in sessions:
- writer.writerow(
- [
- session.session_id,
- session.start_time,
- session.end_time,
- session.ip_address,
- session.country,
- session.language,
- session.messages_sent,
- session.sentiment,
- "Yes" if session.escalated else "No",
- "Yes" if session.forwarded_hr else "No",
- session.full_transcript,
- session.avg_response_time,
- session.tokens,
- session.tokens_eur,
- session.category,
- session.initial_msg,
- session.user_rating,
- ]
- )
+ writer.writerow([
+ session.session_id,
+ session.start_time,
+ session.end_time,
+ session.ip_address,
+ session.country,
+ session.language,
+ session.messages_sent,
+ session.sentiment,
+ "Yes" if session.escalated else "No",
+ "Yes" if session.forwarded_hr else "No",
+ session.full_transcript,
+ session.avg_response_time,
+ session.tokens,
+ session.tokens_eur,
+ session.category,
+ session.initial_msg,
+ session.user_rating,
+ ])
return response
diff --git a/dashboard_project/dashboard_project/asgi.py b/dashboard_project/dashboard_project/asgi.py
index 78c9588..b1497b1 100644
--- a/dashboard_project/dashboard_project/asgi.py
+++ b/dashboard_project/dashboard_project/asgi.py
@@ -4,7 +4,7 @@ ASGI config for dashboard_project project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
-https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
+
"""
import os
diff --git a/dashboard_project/dashboard_project/celery.py b/dashboard_project/dashboard_project/celery.py
index 360787d..b7bbee6 100644
--- a/dashboard_project/dashboard_project/celery.py
+++ b/dashboard_project/dashboard_project/celery.py
@@ -2,18 +2,24 @@ import os
from celery import Celery
-# Set the default Django settings module for the 'celery' program.
+# Set the default Django settings module for the 'celery' program
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
app = Celery("dashboard_project")
# Using a string here means the worker doesn't have to serialize
-# the configuration object to child processes.
+
+# the configuration object to child processes
+
# - namespace='CELERY' means all celery-related configuration keys
-# should have a `CELERY_` prefix.
+
+# should have a `CELERY_` prefix
+
app.config_from_object("django.conf:settings", namespace="CELERY")
-# Load task modules from all registered Django app configs.
+# Load task modules from all registered Django app configs
+
app.autodiscover_tasks()
diff --git a/dashboard_project/dashboard_project/settings.py b/dashboard_project/dashboard_project/settings.py
index 4ab2620..f589345 100644
--- a/dashboard_project/dashboard_project/settings.py
+++ b/dashboard_project/dashboard_project/settings.py
@@ -7,6 +7,7 @@ from pathlib import Path
from django.core.management.utils import get_random_secret_key
# Load environment variables from .env file if present
+
try:
from dotenv import load_dotenv
@@ -14,18 +15,22 @@ try:
except ImportError:
pass
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
+# Build paths inside the project like this: BASE_DIR / 'subdir'
+
BASE_DIR = Path(__file__).resolve().parent.parent
-# SECURITY WARNING: keep the secret key used in production secret!
+# SECURITY WARNING: keep the secret key used in production secret
+
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", get_random_secret_key())
-# SECURITY WARNING: don't run with debug turned on in production!
+# SECURITY WARNING: don't run with debug turned on in production
+
DEBUG = os.environ.get("DJANGO_DEBUG", "True") == "True"
ALLOWED_HOSTS = []
# Application definition
+
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
@@ -80,6 +85,7 @@ TEMPLATES = [
WSGI_APPLICATION = "dashboard_project.wsgi.application"
# Database
+
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
@@ -88,6 +94,7 @@ DATABASES = {
}
# Password validation
+
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@@ -104,12 +111,14 @@ AUTH_PASSWORD_VALIDATORS = [
]
# Internationalization
+
LANGUAGE_CODE = "en-US"
TIME_ZONE = "Europe/Amsterdam"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
+
STATIC_URL = "static/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
@@ -125,23 +134,28 @@ STORAGES = {
}
# Media files
+
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# Default primary key field type
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Crispy Forms
+
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
# Authentication
+
AUTH_USER_MODEL = "accounts.CustomUser"
LOGIN_REDIRECT_URL = "dashboard"
LOGOUT_REDIRECT_URL = "login"
ACCOUNT_LOGOUT_ON_GET = True
# django-allauth
+
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
@@ -150,7 +164,9 @@ SITE_ID = 1
ACCOUNT_EMAIL_VERIFICATION = "none"
# Celery Configuration
+
# Check if Redis is available
+
try:
import redis
@@ -184,6 +200,7 @@ CELERY_TIMEZONE = TIME_ZONE
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
# Get schedule from environment variables or use defaults
+
CHAT_DATA_FETCH_INTERVAL = int(os.environ.get("CHAT_DATA_FETCH_INTERVAL", 3600)) # Default: 1 hour
CELERY_BEAT_SCHEDULE = {
diff --git a/dashboard_project/dashboard_project/wsgi.py b/dashboard_project/dashboard_project/wsgi.py
index ec69107..3136932 100644
--- a/dashboard_project/dashboard_project/wsgi.py
+++ b/dashboard_project/dashboard_project/wsgi.py
@@ -4,7 +4,7 @@ WSGI config for dashboard_project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
-https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
+
"""
import os
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 04c7cb3..d288433 100644
--- a/dashboard_project/data_integration/management/commands/fix_datasource_schema.py
+++ b/dashboard_project/data_integration/management/commands/fix_datasource_schema.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+# !/usr/bin/env python
"""
Migration Fix Script for ExternalDataSource
diff --git a/dashboard_project/data_integration/management/commands/test_datasource_schema.py b/dashboard_project/data_integration/management/commands/test_datasource_schema.py
index 1118287..9b75603 100644
--- a/dashboard_project/data_integration/management/commands/test_datasource_schema.py
+++ b/dashboard_project/data_integration/management/commands/test_datasource_schema.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+# !/usr/bin/env python
"""
Test the ExternalDataSource Model Schema
diff --git a/dashboard_project/data_integration/models.py b/dashboard_project/data_integration/models.py
index b5175c6..06ca964 100644
--- a/dashboard_project/data_integration/models.py
+++ b/dashboard_project/data_integration/models.py
@@ -39,7 +39,7 @@ class ChatMessage(models.Model):
class ExternalDataSource(models.Model):
name = models.CharField(max_length=255, default="External API")
- api_url = models.URLField(default="https://proto.notso.ai/jumbo/chats")
+ api_url = models.URLField(default="")
auth_username = models.CharField(max_length=255, blank=True, null=True)
auth_password = models.CharField(
max_length=255, blank=True, null=True
diff --git a/dashboard_project/data_integration/tests.py b/dashboard_project/data_integration/tests.py
index a39b155..d85108a 100644
--- a/dashboard_project/data_integration/tests.py
+++ b/dashboard_project/data_integration/tests.py
@@ -1 +1 @@
-# Create your tests here.
+# Create your tests here
diff --git a/dashboard_project/data_integration/views.py b/dashboard_project/data_integration/views.py
index d159334..f669421 100644
--- a/dashboard_project/data_integration/views.py
+++ b/dashboard_project/data_integration/views.py
@@ -7,7 +7,7 @@ from .models import ExternalDataSource
from .tasks import periodic_fetch_chat_data, refresh_specific_source
from .utils import fetch_and_store_chat_data
-# Create your views here.
+# Create your views here
def is_superuser(user):
diff --git a/dashboard_project/manage.py b/dashboard_project/manage.py
index a2baa97..fc183f7 100644
--- a/dashboard_project/manage.py
+++ b/dashboard_project/manage.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+# !/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
diff --git a/dashboard_project/scripts/cleanup_duplicates.py b/dashboard_project/scripts/cleanup_duplicates.py
index 5abaa0f..570c70b 100644
--- a/dashboard_project/scripts/cleanup_duplicates.py
+++ b/dashboard_project/scripts/cleanup_duplicates.py
@@ -4,6 +4,7 @@ import os
import sys
# Add the project root to sys.path
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
diff --git a/dashboard_project/scripts/fix_dashboard_data.py b/dashboard_project/scripts/fix_dashboard_data.py
index 9f7f236..cc73479 100755
--- a/dashboard_project/scripts/fix_dashboard_data.py
+++ b/dashboard_project/scripts/fix_dashboard_data.py
@@ -1,4 +1,5 @@
-#!/usr/bin/env python
+# !/usr/bin/env python
+
# scripts/fix_dashboard_data.py
import os
@@ -15,11 +16,13 @@ from django.db import transaction
from django.utils.timezone import make_aware
# Set up Django environment
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
django.setup()
# SCRIPT CONFIG
+
CREATE_TEST_DATA = False # Set to True to create sample data if none exists
COMPANY_NAME = "Notso AI" # The company name to use
diff --git a/dashboard_project/static/css/dashboard.css b/dashboard_project/static/css/dashboard.css
index abcca83..fdb6570 100644
--- a/dashboard_project/static/css/dashboard.css
+++ b/dashboard_project/static/css/dashboard.css
@@ -1,10 +1,11 @@
/**
- * dashboard.css - Styles specific to dashboard functionality
+
+* dashboard.css - Styles specific to dashboard functionality
*/
-/* Theme variables */
+/*Theme variables */
:root {
- /* Light theme (default) */
+ /* Light theme (default)*/
--bg-color: #f8f9fa;
--text-color: #212529;
--card-bg: #ffffff;
@@ -26,7 +27,7 @@
color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
-/* Dark theme */
+/*Dark theme*/
[data-bs-theme="dark"] {
--bg-color: #212529;
--text-color: #f8f9fa;
@@ -47,7 +48,7 @@
--icon-color: #6ea8fe;
}
-/* Apply theme variables */
+/*Apply theme variables*/
body {
background-color: var(--bg-color);
color: var(--text-color);
@@ -91,7 +92,7 @@ body {
background-color: var(--sidebar-bg) !important;
}
-/* Sidebar navigation styling with dark mode support */
+/*Sidebar navigation styling with dark mode support*/
.sidebar .nav-link {
color: var(--text-color);
transition: all 0.2s ease;
@@ -168,7 +169,7 @@ body {
color: var(--text-color);
}
-/* Footer */
+/*Footer*/
footer {
background-color: var(--card-bg);
border-top: 1px solid var(--border-color);
@@ -182,7 +183,7 @@ footer {
background-color: var(--navbar-bg);
}
-/* Dashboard grid layout */
+/*Dashboard grid layout*/
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
@@ -193,7 +194,7 @@ footer {
/* Increased gap */
}
-/* Dashboard widget cards */
+/*Dashboard widget cards*/
.dashboard-widget {
display: flex;
@@ -254,7 +255,7 @@ footer {
/* Consistent padding */
}
-/* Chart widgets */
+/*Chart widgets*/
.chart-widget .card-body {
display: flex;
flex-direction: column;
@@ -270,7 +271,7 @@ footer {
/* Ensure it takes full width of card body */
}
-/* Stat widgets / Stat Cards */
+/*Stat widgets / Stat Cards*/
.stat-card {
text-align: center;
padding: 1.5rem;
@@ -319,7 +320,7 @@ footer {
margin-bottom: 0;
}
-/* Dashboard theme variations */
+/*Dashboard theme variations*/
.dashboard-theme-light .card {
background-color: #fff;
}
@@ -344,7 +345,7 @@ footer {
color: #adb5bd;
}
-/* Time period selector */
+/*Time period selector*/
.time-period-selector {
display: flex;
align-items: center;
@@ -367,7 +368,7 @@ footer {
font-size: 0.875rem;
}
-/* Custom metric selector */
+/*Custom metric selector*/
.metric-selector {
max-width: 100%;
overflow-x: auto;
@@ -388,7 +389,7 @@ footer {
border-radius: 0.25rem;
}
-/* Dashboard loading states */
+/*Dashboard loading states*/
.widget-placeholder {
min-height: 300px;
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
@@ -413,7 +414,7 @@ footer {
}
}
-/* Dashboard empty states */
+/*Dashboard empty states*/
.empty-state {
padding: 2.5rem;
@@ -449,7 +450,7 @@ footer {
margin-top: 1rem;
}
-/* Responsive adjustments */
+/*Responsive adjustments*/
@media (width <=767.98px) {
.dashboard-grid {
grid-template-columns: 1fr;
@@ -471,7 +472,7 @@ footer {
}
}
-/* Preserve colored background for stat cards in both themes */
+/*Preserve colored background for stat cards in both themes*/
.col-md-3 .card.stats-card.bg-primary {
background-color: var(--bs-primary) !important;
color: white !important;
@@ -507,7 +508,7 @@ footer {
color: var(--bs-dark) !important;
}
-/* Stats Cards Alignment Fix (Bottom Align, No Overlap) */
+/*Stats Cards Alignment Fix (Bottom Align, No Overlap)*/
.stats-row {
display: flex;
flex-wrap: wrap;
diff --git a/dashboard_project/static/css/style.css b/dashboard_project/static/css/style.css
index 6b0763a..20c49b1 100644
--- a/dashboard_project/static/css/style.css
+++ b/dashboard_project/static/css/style.css
@@ -1,8 +1,9 @@
/**
- * style.css - Global styles for the application
+
+* style.css - Global styles for the application
*/
-/* General Styles */
+/*General Styles*/
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
@@ -25,14 +26,14 @@ body {
/* Ensures body takes at least full viewport height */
}
-/* Navbar adjustments (if needed, Bootstrap usually handles this well) */
+/*Navbar adjustments (if needed, Bootstrap usually handles this well)*/
.navbar {
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
/* Subtle shadow for depth */
}
-/* Helper Classes */
+/*Helper Classes*/
.text-truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
@@ -49,7 +50,7 @@ body {
min-width: 150px;
}
-/* Card styles */
+/*Card styles*/
.card {
border: 1px solid #e0e5e9;
@@ -91,7 +92,7 @@ body {
font-weight: 600;
}
-/* Sidebar enhancements */
+/*Sidebar enhancements*/
.sidebar {
background-color: #fff;
@@ -159,7 +160,7 @@ body {
margin-top: 1rem;
}
-/* Dashboard stats cards */
+/*Dashboard stats cards*/
.stats-card {
border-radius: 0.5rem;
overflow: hidden;
@@ -176,14 +177,14 @@ body {
opacity: 0.8;
}
-/* Chart containers */
+/*Chart containers*/
.chart-container {
width: 100%;
height: 300px;
position: relative;
}
-/* Loading overlay */
+/*Loading overlay*/
.loading-overlay {
position: fixed;
top: 0;
@@ -197,7 +198,7 @@ body {
z-index: 9999;
}
-/* Table enhancements */
+/*Table enhancements*/
.table {
border-color: #e0e5e9;
}
@@ -224,7 +225,7 @@ body {
/* Consistent hover with sidebar */
}
-/* Form improvements */
+/*Form improvements*/
.form-control,
.form-select {
border-color: #ced4da;
@@ -246,7 +247,7 @@ body {
/* Bootstrap focus shadow */
}
-/* Button styling */
+/*Button styling*/
.btn {
border-radius: 0.375rem;
@@ -281,13 +282,13 @@ body {
border-color: #545b62;
}
-/* Alert styling */
+/*Alert styling*/
.alert {
border-radius: 0.375rem;
padding: 0.9rem 1.25rem;
}
-/* Chat transcript styling */
+/*Chat transcript styling*/
.chat-transcript {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
@@ -304,7 +305,7 @@ body {
margin-bottom: 0;
}
-/* Footer styling */
+/*Footer styling*/
footer {
background-color: #fff;
@@ -318,7 +319,7 @@ footer {
/* Added for sticky footer */
}
-/* Responsive adjustments */
+/*Responsive adjustments*/
@media (width <=767.98px) {
.main-content {
margin-left: 0;
@@ -337,7 +338,7 @@ footer {
}
}
-/* Print styles */
+/*Print styles*/
@media print {
.sidebar,
.navbar,
diff --git a/dashboard_project/static/js/ajax-navigation.js b/dashboard_project/static/js/ajax-navigation.js
index 7e069bc..f167429 100644
--- a/dashboard_project/static/js/ajax-navigation.js
+++ b/dashboard_project/static/js/ajax-navigation.js
@@ -1,8 +1,9 @@
/**
- * ajax-navigation.js - JavaScript for AJAX-based navigation across the entire application
- *
- * This script handles AJAX navigation between pages in the Chat Analytics Dashboard.
- * It intercepts link clicks, loads content via AJAX, and updates the browser history.
+
+* ajax-navigation.js - JavaScript for AJAX-based navigation across the entire application
+*
+* This script handles AJAX navigation between pages in the Chat Analytics Dashboard.
+* It intercepts link clicks, loads content via AJAX, and updates the browser history.
*/
document.addEventListener("DOMContentLoaded", function () {
diff --git a/dashboard_project/static/js/ajax-pagination.js b/dashboard_project/static/js/ajax-pagination.js
index 3dcff3a..13176a4 100644
--- a/dashboard_project/static/js/ajax-pagination.js
+++ b/dashboard_project/static/js/ajax-pagination.js
@@ -1,8 +1,9 @@
/**
- * ajax-pagination.js - Common JavaScript for AJAX pagination across the application
- *
- * This script handles AJAX-based pagination for all pages in the Chat Analytics Dashboard.
- * It intercepts pagination link clicks, loads content via AJAX, and updates the browser history.
+
+* ajax-pagination.js - Common JavaScript for AJAX pagination across the application
+*
+* This script handles AJAX-based pagination for all pages in the Chat Analytics Dashboard.
+* It intercepts pagination link clicks, loads content via AJAX, and updates the browser history.
*/
document.addEventListener("DOMContentLoaded", function () {
diff --git a/dashboard_project/static/js/dashboard.js b/dashboard_project/static/js/dashboard.js
index e634d06..8ba13fa 100644
--- a/dashboard_project/static/js/dashboard.js
+++ b/dashboard_project/static/js/dashboard.js
@@ -1,9 +1,10 @@
/**
- * dashboard.js - JavaScript for the dashboard functionality
- *
- * This file handles the interactive features of the dashboard,
- * including chart refreshing, dashboard filtering, and dashboard
- * customization.
+
+* dashboard.js - JavaScript for the dashboard functionality
+*
+* This file handles the interactive features of the dashboard,
+* including chart refreshing, dashboard filtering, and dashboard
+* customization.
*/
document.addEventListener("DOMContentLoaded", function () {
diff --git a/dashboard_project/static/js/main.js b/dashboard_project/static/js/main.js
index 475bed0..78625fe 100644
--- a/dashboard_project/static/js/main.js
+++ b/dashboard_project/static/js/main.js
@@ -1,8 +1,9 @@
/**
- * main.js - Global JavaScript functionality
- *
- * This file contains general JavaScript functionality used across
- * the entire application, including navigation, forms, and UI interactions.
+
+* main.js - Global JavaScript functionality
+*
+* This file contains general JavaScript functionality used across
+* the entire application, including navigation, forms, and UI interactions.
*/
document.addEventListener("DOMContentLoaded", function () {
diff --git a/dashboard_project/wsgi.py b/dashboard_project/wsgi.py
index ec69107..3136932 100644
--- a/dashboard_project/wsgi.py
+++ b/dashboard_project/wsgi.py
@@ -4,7 +4,7 @@ WSGI config for dashboard_project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
-https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
+
"""
import os
diff --git a/dev.sh b/dev.sh
index 7bafe9a..3c48b7b 100755
--- a/dev.sh
+++ b/dev.sh
@@ -1,10 +1,13 @@
#!/bin/bash
+
# LiveGraphsDjango Development Helper Script
# Set UV_LINK_MODE to copy to avoid hardlink warnings
+
export UV_LINK_MODE=copy
# Function to print section header
+
print_header() {
echo "======================================"
echo "🚀 $1"
@@ -12,6 +15,7 @@ print_header() {
}
# Display help menu
+
if [[ $1 == "help" ]] || [[ $1 == "-h" ]] || [[ $1 == "--help" ]] || [[ -z $1 ]]; then
print_header "LiveGraphsDjango Development Commands"
echo "Usage: ./dev.sh COMMAND"
@@ -33,6 +37,7 @@ if [[ $1 == "help" ]] || [[ $1 == "-h" ]] || [[ $1 == "--help" ]] || [[ -z $1 ]]
fi
# Start Redis server
+
if [[ $1 == "redis-start" ]]; then
print_header "Starting Redis Server"
redis-server --daemonize yes
@@ -46,6 +51,7 @@ if [[ $1 == "redis-start" ]]; then
fi
# Test Redis connection
+
if [[ $1 == "redis-test" ]]; then
print_header "Testing Redis Connection"
cd dashboard_project && python manage.py test_redis
@@ -53,6 +59,7 @@ if [[ $1 == "redis-test" ]]; then
fi
# Stop Redis server
+
if [[ $1 == "redis-stop" ]]; then
print_header "Stopping Redis Server"
redis-cli shutdown
@@ -61,6 +68,7 @@ if [[ $1 == "redis-stop" ]]; then
fi
# Run migrations
+
if [[ $1 == "migrate" ]]; then
print_header "Running Migrations"
cd dashboard_project && UV_LINK_MODE=copy uv run python manage.py migrate
@@ -68,6 +76,7 @@ if [[ $1 == "migrate" ]]; then
fi
# Make migrations
+
if [[ $1 == "makemigrations" ]]; then
print_header "Creating Migrations"
cd dashboard_project && UV_LINK_MODE=copy uv run python manage.py makemigrations
@@ -75,6 +84,7 @@ if [[ $1 == "makemigrations" ]]; then
fi
# Create superuser
+
if [[ $1 == "superuser" ]]; then
print_header "Creating Superuser"
cd dashboard_project && UV_LINK_MODE=copy uv run python manage.py createsuperuser
@@ -82,6 +92,7 @@ if [[ $1 == "superuser" ]]; then
fi
# Test Celery
+
if [[ $1 == "test-celery" ]]; then
print_header "Testing Celery"
cd dashboard_project && UV_LINK_MODE=copy uv run python manage.py test_celery
@@ -89,6 +100,7 @@ if [[ $1 == "test-celery" ]]; then
fi
# View Celery logs
+
if [[ $1 == "logs-celery" ]]; then
print_header "Celery Worker Logs"
echo "Press Ctrl+C to exit"
@@ -97,6 +109,7 @@ if [[ $1 == "logs-celery" ]]; then
fi
# View Celery Beat logs
+
if [[ $1 == "logs-beat" ]]; then
print_header "Celery Beat Logs"
echo "Press Ctrl+C to exit"
@@ -105,6 +118,7 @@ if [[ $1 == "logs-beat" ]]; then
fi
# Django shell
+
if [[ $1 == "shell" ]]; then
print_header "Django Shell"
cd dashboard_project && UV_LINK_MODE=copy uv run python manage.py shell
@@ -112,6 +126,7 @@ if [[ $1 == "shell" ]]; then
fi
# Start the application
+
if [[ $1 == "start" ]]; then
print_header "Starting LiveGraphsDjango Application"
./start.sh
@@ -119,6 +134,7 @@ if [[ $1 == "start" ]]; then
fi
# Invalid command
+
echo "❌ Unknown command: $1"
echo "Run './dev.sh help' to see available commands"
exit 1
diff --git a/package.json b/package.json
index 48d481e..a0a1fc4 100644
--- a/package.json
+++ b/package.json
@@ -1,35 +1,26 @@
{
- "devDependencies": {
- "markdownlint-cli2": "^0.18.1",
- "prettier": "^3.5.3",
- "prettier-plugin-jinja-template": "^2.1.0"
- },
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
- "lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
- "lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\""
+ "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": {
- "MD007": {
- "indent": 4,
- "start_indented": false,
- "start_indent": 4
- },
"MD013": false,
- "MD030": {
- "ul_single": 3,
- "ol_single": 2,
- "ul_multi": 3,
- "ol_multi": 2
- },
"MD033": false
},
"ignores": [
- "node_modules",
".git",
- "*.json"
+ ".trunk",
+ ".venv",
+ "node_modules"
]
}
}
diff --git a/pyproject.toml b/pyproject.toml
index 383a16a..7680220 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,18 +4,16 @@ version = "0.1.0"
description = "Live Graphs Django Dashboard"
readme = "README.md"
requires-python = ">=3.13"
-authors = [{ name = "LiveGraphs Team" }]
license = { text = "MIT" }
-
+authors = [{ name = "LiveGraphs Team" }]
classifiers = [
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.13",
"Framework :: Django",
"Framework :: Django :: 5.2",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.13",
]
-
dependencies = [
"bleach[css]>=6.2.0",
"celery[sqlalchemy]>=5.5.2",
@@ -37,6 +35,11 @@ dependencies = [
"xlsxwriter>=3.2.3",
]
+[project.urls]
+"Bug Tracker" = "https://github.com/kjanat/livegraphsdjango/issues"
+"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
+"Source" = "https://github.com/kjanat/livegraphsdjango"
+
[dependency-groups]
dev = [
"bandit>=1.8.3",
@@ -55,14 +58,64 @@ dev = [
requires = ["setuptools>=69.0.0", "wheel>=0.42.0"]
build-backend = "setuptools.build_meta"
-[tool.setuptools]
-packages = ["dashboard_project"]
+[tool.bandit]
+exclude_dirs = [
+ "tests",
+ "venv",
+ ".venv",
+ ".git",
+ "__pycache__",
+ "migrations",
+ "**/create_sample_data.py",
+]
+skips = ["B101"]
+targets = ["dashboard_project"]
-[tool.setuptools.package-data]
-"dashboard_project" = ["static/**/*", "templates/**/*", "media/**/*"]
+[tool.coverage.run]
+source = ["dashboard_project"]
+omit = [
+ "dashboard_project/manage.py",
+ "dashboard_project/*/migrations/*",
+ "dashboard_project/*/tests/*",
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "pragma: no cover",
+ "def __repr__",
+ "raise NotImplementedError",
+ "if __name__ == .__main__.:",
+ "pass",
+ "raise ImportError",
+]
+
+[tool.django-stubs]
+django_settings_module = "dashboard_project.settings"
+
+[tool.mypy]
+python_version = "3.13"
+warn_return_any = true
+warn_unused_configs = true
+disallow_untyped_defs = false
+disallow_incomplete_defs = false
+plugins = ["mypy_django_plugin.main"]
+
+[[tool.mypy.overrides]]
+module = ["django.*", "rest_framework.*"]
+ignore_missing_imports = true
+
+[tool.pytest.ini_options]
+filterwarnings = [
+ "ignore::DeprecationWarning",
+ "ignore::PendingDeprecationWarning",
+]
+python_files = "test_*.py"
+testpaths = ["dashboard_project"]
+DJANGO_SETTINGS_MODULE = "dashboard_project.settings"
[tool.ruff]
-# Exclude a variety of commonly ignored directories.
+preview = true
+# Exclude a variety of commonly ignored directories
exclude = [
".bzr",
".direnv",
@@ -91,11 +144,9 @@ exclude = [
"site-packages",
"venv",
]
-
-# Same as Black.
+# Same as Black
line-length = 120
indent-width = 4
-
# Assume Python 3.13
target-version = "py313"
@@ -110,62 +161,8 @@ quote-style = "double"
indent-style = "space"
line-ending = "lf"
-[tool.bandit]
-exclude_dirs = [
- "tests",
- "venv",
- ".venv",
- ".git",
- "__pycache__",
- "migrations",
- "**/create_sample_data.py",
-]
-skips = ["B101"]
-targets = ["dashboard_project"]
+[tool.setuptools]
+packages = ["dashboard_project"]
-[tool.mypy]
-python_version = "3.13"
-warn_return_any = true
-warn_unused_configs = true
-disallow_untyped_defs = false
-disallow_incomplete_defs = false
-plugins = ["mypy_django_plugin.main"]
-
-[[tool.mypy.overrides]]
-module = ["django.*", "rest_framework.*"]
-ignore_missing_imports = true
-
-[tool.django-stubs]
-django_settings_module = "dashboard_project.settings"
-
-[tool.pytest.ini_options]
-DJANGO_SETTINGS_MODULE = "dashboard_project.settings"
-python_files = "test_*.py"
-testpaths = ["dashboard_project"]
-filterwarnings = [
- "ignore::DeprecationWarning",
- "ignore::PendingDeprecationWarning",
-]
-
-[tool.coverage.run]
-source = ["dashboard_project"]
-omit = [
- "dashboard_project/manage.py",
- "dashboard_project/*/migrations/*",
- "dashboard_project/*/tests/*",
-]
-
-[tool.coverage.report]
-exclude_lines = [
- "pragma: no cover",
- "def __repr__",
- "raise NotImplementedError",
- "if __name__ == .__main__.:",
- "pass",
- "raise ImportError",
-]
-
-[project.urls]
-"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
-"Source" = "https://github.com/kjanat/livegraphsdjango"
-"Bug Tracker" = "https://github.com/kjanat/livegraphsdjango/issues"
+[tool.setuptools.package-data]
+"dashboard_project" = ["static/__/*", "templates/__/*", "media/**/*"]
diff --git a/start.sh b/start.sh
index 04547ce..73a490a 100755
--- a/start.sh
+++ b/start.sh
@@ -1,14 +1,18 @@
#!/bin/bash
+
# Set UV_LINK_MODE to copy to avoid hardlink warnings
+
export UV_LINK_MODE=copy
# Check if Redis is running
+
if ! redis-cli ping >/dev/null 2>&1; then
echo "Starting Redis server..."
redis-server --daemonize yes
sleep 1
# Verify Redis is now running
+
if redis-cli ping >/dev/null 2>&1; then
echo "✅ Redis server is now running"
else
@@ -22,6 +26,7 @@ else
fi
# Set environment variables for Redis if it's running
+
if redis-cli ping >/dev/null 2>&1; then
export CELERY_BROKER_URL=redis://localhost:6379/0
export CELERY_RESULT_BACKEND=redis://localhost:6379/0
@@ -33,4 +38,5 @@ else
fi
# Start the application using foreman
+
foreman start