mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-02-13 15:15:43 +01:00
feat: Add uv Docker, Postgres, and company linking
Introduces uv-based Docker workflow with non-root runtime, cached installs, and uv-run for web and Celery. Updates docker-compose to Postgres + Redis, loads .env, and removes source bind mount for reproducible builds. Switches settings to use Postgres when env is present with SQLite fallback; broadens allowed hosts for containerized development. Adds psycopg2-binary and updates sample env for Redis in Docker. Adds company scoping to external data models and links sessions during ingestion; provides management commands to seed a Jumbo company/users and sync external chat data into the dashboard. Includes .dockerignore, TypeScript config and typings, and minor template/docs tweaks. Requires database migration.
This commit is contained in:
53
.dockerignore
Normal file
53
.dockerignore
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Django
|
||||||
|
*.log
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
/static/
|
||||||
|
/media/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
.playwright-mcp/
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
node_modules/
|
||||||
14
.env.sample
14
.env.sample
@@ -1,8 +1,20 @@
|
|||||||
# .env.sample - rename to .env and update with actual credentials
|
# .env.sample - rename to .env and update with actual credentials
|
||||||
# Django settings
|
# Django settings
|
||||||
|
# Generate secret with e.g. `openssl rand -hex 32`
|
||||||
DJANGO_SECRET_KEY=your-secure-secret-key
|
DJANGO_SECRET_KEY=your-secure-secret-key
|
||||||
DJANGO_DEBUG=True
|
DJANGO_DEBUG=True
|
||||||
|
|
||||||
|
# Database configuration (optional - local development uses SQLite by default)
|
||||||
|
# Uncomment these to use PostgreSQL locally:
|
||||||
|
# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/dashboard_db
|
||||||
|
# POSTGRES_DB=dashboard_db
|
||||||
|
# POSTGRES_USER=postgres
|
||||||
|
# POSTGRES_PASSWORD=postgres
|
||||||
|
# POSTGRES_HOST=localhost
|
||||||
|
# POSTGRES_PORT=5432
|
||||||
|
#
|
||||||
|
# Note: Docker Compose automatically uses PostgreSQL via docker-compose.yml environment variables
|
||||||
|
|
||||||
# External API credentials
|
# External API credentials
|
||||||
EXTERNAL_API_USERNAME=your-api-username
|
EXTERNAL_API_USERNAME=your-api-username
|
||||||
EXTERNAL_API_PASSWORD=your-api-password
|
EXTERNAL_API_PASSWORD=your-api-password
|
||||||
@@ -10,7 +22,7 @@ EXTERNAL_API_PASSWORD=your-api-password
|
|||||||
# Redis settings for Celery
|
# Redis settings for Celery
|
||||||
REDIS_URL=redis://localhost:6379/0
|
REDIS_URL=redis://localhost:6379/0
|
||||||
CELERY_BROKER_URL=redis://localhost:6379/0
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||||
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
|
|
||||||
# Celery Task Schedule (in seconds)
|
# Celery Task Schedule (in seconds)
|
||||||
CHAT_DATA_FETCH_INTERVAL=3600
|
CHAT_DATA_FETCH_INTERVAL=3600
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -421,3 +421,6 @@ package-lock.json
|
|||||||
# Local database files
|
# Local database files
|
||||||
*.rdb
|
*.rdb
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|
||||||
|
# playwright
|
||||||
|
.playwright-mcp/
|
||||||
|
|||||||
55
Dockerfile
55
Dockerfile
@@ -1,43 +1,58 @@
|
|||||||
# Dockerfile
|
# Dockerfile
|
||||||
|
|
||||||
FROM python:3.13-slim
|
# Use a Python image with uv pre-installed
|
||||||
|
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||||
|
|
||||||
|
# Setup a non-root user
|
||||||
|
RUN groupadd --system --gid 999 nonroot \
|
||||||
|
&& useradd --system --gid 999 --uid 999 --create-home nonroot
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
ENV DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
ENV DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
||||||
|
|
||||||
# Set work directory
|
# Change the working directory to the `app` directory
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install UV for Python package management
|
# Enable bytecode compilation
|
||||||
|
ENV UV_COMPILE_BYTECODE=1
|
||||||
|
|
||||||
RUN pip install uv
|
# Copy from the cache instead of linking since it's a mounted volume
|
||||||
|
ENV UV_LINK_MODE=copy
|
||||||
|
|
||||||
# Copy project files
|
# Install dependencies (separate layer for caching)
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --frozen --no-install-project --no-dev
|
||||||
|
|
||||||
COPY pyproject.toml .
|
# Copy the project into the image
|
||||||
COPY uv.lock .
|
COPY . /app
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Install dependencies
|
# Sync the project (install the project itself)
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync --frozen --no-dev
|
||||||
|
|
||||||
RUN uv pip install -e .
|
# Place executables in the environment at the front of the path
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
# Change to the Django project directory
|
# Change to the Django project directory
|
||||||
|
|
||||||
WORKDIR /app/dashboard_project
|
WORKDIR /app/dashboard_project
|
||||||
|
|
||||||
# Collect static files
|
# Collect static files (runs as root)
|
||||||
|
RUN uv run manage.py collectstatic --noinput
|
||||||
|
|
||||||
RUN python manage.py collectstatic --noinput
|
# Fix ownership of dashboard_project directory for nonroot user
|
||||||
|
# This ensures db.sqlite3 and any files created during runtime are writable
|
||||||
|
RUN chown -R nonroot:nonroot /app/dashboard_project && \
|
||||||
|
chmod 775 /app/dashboard_project
|
||||||
|
|
||||||
# Change back to the app directory
|
# Change back to the app directory
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Run gunicorn
|
# Use the non-root user to run our application
|
||||||
|
USER nonroot
|
||||||
|
|
||||||
CMD ["gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000"]
|
# Run gunicorn via uv run to ensure it's in the environment
|
||||||
|
CMD ["uv", "run", "gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000", "--chdir", "dashboard_project"]
|
||||||
|
|||||||
18
bun.lock
18
bun.lock
@@ -4,6 +4,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.56.1",
|
"@playwright/test": "^1.56.1",
|
||||||
|
"@types/bun": "latest",
|
||||||
"markdownlint-cli2": "^0.18.1",
|
"markdownlint-cli2": "^0.18.1",
|
||||||
"oxlint": "^1.25.0",
|
"oxlint": "^1.25.0",
|
||||||
"oxlint-tsgolint": "^0.5.0",
|
"oxlint-tsgolint": "^0.5.0",
|
||||||
@@ -11,6 +12,9 @@
|
|||||||
"prettier-plugin-jinja-template": "^2.1.0",
|
"prettier-plugin-jinja-template": "^2.1.0",
|
||||||
"prettier-plugin-packagejson": "^2.5.19",
|
"prettier-plugin-packagejson": "^2.5.19",
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -54,18 +58,26 @@
|
|||||||
|
|
||||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
|
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@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/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"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=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||||
|
|
||||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
"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-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||||
@@ -74,6 +86,8 @@
|
|||||||
|
|
||||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"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=="],
|
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
|
||||||
@@ -238,8 +252,12 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
|
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||||
|
|
||||||
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|||||||
@@ -27,7 +27,21 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", get_random_secret_key())
|
|||||||
|
|
||||||
DEBUG = os.environ.get("DJANGO_DEBUG", "True") == "True"
|
DEBUG = os.environ.get("DJANGO_DEBUG", "True") == "True"
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
# Allow localhost, Docker IPs, and entire private network ranges
|
||||||
|
ALLOWED_HOSTS = [
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1",
|
||||||
|
"0.0.0.0", # nosec B104
|
||||||
|
".localhost",
|
||||||
|
# Allow all 192.168.x.x addresses (private network)
|
||||||
|
"192.168.*.*",
|
||||||
|
# Allow all 10.x.x.x addresses (Docker default)
|
||||||
|
"10.*.*.*",
|
||||||
|
# Allow all 172.16-31.x.x addresses (Docker)
|
||||||
|
"172.*.*.*",
|
||||||
|
# Wildcard for any other IPs (development only)
|
||||||
|
"*",
|
||||||
|
]
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
@@ -85,13 +99,28 @@ TEMPLATES = [
|
|||||||
WSGI_APPLICATION = "dashboard_project.wsgi.application"
|
WSGI_APPLICATION = "dashboard_project.wsgi.application"
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
# Use PostgreSQL when DATABASE_URL is set (Docker), otherwise SQLite (local dev)
|
||||||
|
|
||||||
DATABASES = {
|
if os.environ.get("DATABASE_URL"):
|
||||||
"default": {
|
# PostgreSQL configuration for Docker
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
DATABASES = {
|
||||||
"NAME": BASE_DIR / "db.sqlite3",
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": os.environ.get("POSTGRES_DB", "dashboard_db"),
|
||||||
|
"USER": os.environ.get("POSTGRES_USER", "postgres"),
|
||||||
|
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", "postgres"),
|
||||||
|
"HOST": os.environ.get("POSTGRES_HOST", "db"),
|
||||||
|
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# SQLite configuration for local development
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
Management command to set up Jumbo company, users, and link existing data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from accounts.models import Company, CustomUser
|
||||||
|
from data_integration.models import ChatSession, ExternalDataSource
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Set up Jumbo company, create users, and link existing external data"
|
||||||
|
|
||||||
|
def handle(self, *_args, **_options):
|
||||||
|
self.stdout.write("Setting up Jumbo company and data...")
|
||||||
|
|
||||||
|
# 1. Create Jumbo company
|
||||||
|
jumbo_company, created = Company.objects.get_or_create(
|
||||||
|
name="Jumbo", defaults={"description": "Jumbo Supermarkets - External API Data"}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo company"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(" Jumbo company already exists")
|
||||||
|
|
||||||
|
# 2. Create admin user for Jumbo
|
||||||
|
admin_created = False
|
||||||
|
if not CustomUser.objects.filter(username="jumbo_admin").exists():
|
||||||
|
CustomUser.objects.create_user( # nosec B106
|
||||||
|
username="jumbo_admin",
|
||||||
|
email="admin@jumbo.nl",
|
||||||
|
password="jumbo123",
|
||||||
|
company=jumbo_company,
|
||||||
|
is_company_admin=True,
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo admin: jumbo_admin / jumbo123"))
|
||||||
|
admin_created = True
|
||||||
|
else:
|
||||||
|
self.stdout.write(" Jumbo admin already exists")
|
||||||
|
|
||||||
|
# 3. Create regular users for Jumbo
|
||||||
|
jumbo_users = [
|
||||||
|
{
|
||||||
|
"username": "jumbo_analyst",
|
||||||
|
"email": "analyst@jumbo.nl",
|
||||||
|
"password": "jumbo123",
|
||||||
|
"is_company_admin": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "jumbo_manager",
|
||||||
|
"email": "manager@jumbo.nl",
|
||||||
|
"password": "jumbo123",
|
||||||
|
"is_company_admin": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
users_created = 0
|
||||||
|
for user_data in jumbo_users:
|
||||||
|
if not CustomUser.objects.filter(username=user_data["username"]).exists():
|
||||||
|
CustomUser.objects.create_user(
|
||||||
|
username=user_data["username"],
|
||||||
|
email=user_data["email"],
|
||||||
|
password=user_data["password"],
|
||||||
|
company=jumbo_company,
|
||||||
|
is_company_admin=user_data["is_company_admin"],
|
||||||
|
)
|
||||||
|
users_created += 1
|
||||||
|
|
||||||
|
if users_created:
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"✓ Created {users_created} Jumbo users"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(" Jumbo users already exist")
|
||||||
|
|
||||||
|
# 4. Link External Data Source to Jumbo company
|
||||||
|
try:
|
||||||
|
jumbo_ext_source = ExternalDataSource.objects.get(name="Jumbo API")
|
||||||
|
if not jumbo_ext_source.company:
|
||||||
|
jumbo_ext_source.company = jumbo_company
|
||||||
|
jumbo_ext_source.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS("✓ Linked Jumbo API data source to company"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(" Jumbo API data source already linked")
|
||||||
|
except ExternalDataSource.DoesNotExist:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING("⚠ Jumbo API external data source not found. Create it in admin first.")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Link existing chat sessions to Jumbo company
|
||||||
|
unlinked_sessions = ChatSession.objects.filter(company__isnull=True)
|
||||||
|
if unlinked_sessions.exists():
|
||||||
|
count = unlinked_sessions.update(company=jumbo_company)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"✓ Linked {count} existing chat sessions to Jumbo company"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(" All chat sessions already linked to companies")
|
||||||
|
|
||||||
|
# 6. Summary
|
||||||
|
total_sessions = ChatSession.objects.filter(company=jumbo_company).count()
|
||||||
|
total_users = CustomUser.objects.filter(company=jumbo_company).count()
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"\n✓ Setup complete!"
|
||||||
|
f"\n Company: {jumbo_company.name}"
|
||||||
|
f"\n Users: {total_users} (including {1 if admin_created or CustomUser.objects.filter(username='jumbo_admin').exists() else 0} admin)"
|
||||||
|
f"\n Chat sessions: {total_sessions}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write("\nLogin as jumbo_admin/jumbo123 to view the dashboard with Jumbo data.")
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Management command to sync Jumbo API data to dashboard app with proper company linking.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from accounts.models import Company, CustomUser
|
||||||
|
from dashboard.models import ChatSession, DataSource
|
||||||
|
from data_integration.models import ChatSession as ExtChatSession
|
||||||
|
from data_integration.models import ExternalDataSource
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Sync Jumbo API data to dashboard app with company linking"
|
||||||
|
|
||||||
|
def handle(self, *_args, **_options):
|
||||||
|
self.stdout.write("Starting Jumbo data sync to dashboard...")
|
||||||
|
|
||||||
|
# 1. Get or create Jumbo company
|
||||||
|
jumbo_company, created = Company.objects.get_or_create(
|
||||||
|
name="Jumbo", defaults={"description": "Jumbo Supermarkets - External API Data"}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo company"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(" Jumbo company already exists")
|
||||||
|
|
||||||
|
# 2. Get Jumbo external data source
|
||||||
|
try:
|
||||||
|
jumbo_ext_source = ExternalDataSource.objects.get(name="Jumbo API")
|
||||||
|
except ExternalDataSource.DoesNotExist:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR("✗ Jumbo API external data source not found. Please create it in admin first.")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Get or create DataSource linked to Jumbo company
|
||||||
|
jumbo_datasource, created = DataSource.objects.get_or_create(
|
||||||
|
name="Jumbo API Data",
|
||||||
|
company=jumbo_company,
|
||||||
|
defaults={
|
||||||
|
"description": "Chat sessions from Jumbo external API",
|
||||||
|
"external_source": jumbo_ext_source,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo DataSource"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(" Jumbo DataSource already exists")
|
||||||
|
|
||||||
|
# 4. Sync chat sessions from data_integration to dashboard
|
||||||
|
ext_sessions = ExtChatSession.objects.all()
|
||||||
|
synced_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
for ext_session in ext_sessions:
|
||||||
|
# Check if already synced
|
||||||
|
if ChatSession.objects.filter(data_source=jumbo_datasource, session_id=ext_session.session_id).exists():
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create dashboard ChatSession
|
||||||
|
ChatSession.objects.create(
|
||||||
|
data_source=jumbo_datasource,
|
||||||
|
session_id=ext_session.session_id,
|
||||||
|
start_time=ext_session.start_time,
|
||||||
|
end_time=ext_session.end_time,
|
||||||
|
ip_address=ext_session.ip_address,
|
||||||
|
country=ext_session.country or "",
|
||||||
|
language=ext_session.language or "",
|
||||||
|
messages_sent=ext_session.messages_sent or 0,
|
||||||
|
sentiment=ext_session.sentiment or "",
|
||||||
|
escalated=ext_session.escalated or False,
|
||||||
|
forwarded_hr=ext_session.forwarded_hr or False,
|
||||||
|
full_transcript=ext_session.full_transcript_url or "",
|
||||||
|
avg_response_time=ext_session.avg_response_time,
|
||||||
|
tokens=ext_session.tokens or 0,
|
||||||
|
tokens_eur=ext_session.tokens_eur,
|
||||||
|
category=ext_session.category or "",
|
||||||
|
initial_msg=ext_session.initial_msg or "",
|
||||||
|
user_rating=str(ext_session.user_rating) if ext_session.user_rating else "",
|
||||||
|
)
|
||||||
|
synced_count += 1
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"✓ Synced {synced_count} chat sessions (skipped {skipped_count} existing)")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Create admin user for Jumbo company if needed
|
||||||
|
if not CustomUser.objects.filter(company=jumbo_company, is_company_admin=True).exists():
|
||||||
|
CustomUser.objects.create_user( # nosec B106
|
||||||
|
username="jumbo_admin",
|
||||||
|
email="admin@jumbo.nl",
|
||||||
|
password="jumbo123",
|
||||||
|
company=jumbo_company,
|
||||||
|
is_company_admin=True,
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS("✓ Created Jumbo admin user: jumbo_admin / jumbo123"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(" Jumbo admin user already exists")
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"\n✓ Sync complete! Jumbo company now has {ChatSession.objects.filter(data_source__company=jumbo_company).count()} chat sessions"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write("\nLogin as jumbo_admin to view the dashboard with Jumbo data.")
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-05 18:20
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0001_initial"),
|
||||||
|
("data_integration", "0002_externaldatasource_error_count_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="chatsession",
|
||||||
|
name="company",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Company this session belongs to",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="external_chat_sessions",
|
||||||
|
to="accounts.company",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="externaldatasource",
|
||||||
|
name="company",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Company this data source belongs to",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="external_data_sources",
|
||||||
|
to="accounts.company",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,10 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
from accounts.models import Company
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class ChatSession(models.Model):
|
class ChatSession(models.Model):
|
||||||
session_id = models.CharField(max_length=255, unique=True)
|
session_id = models.CharField(max_length=255, unique=True)
|
||||||
|
company = models.ForeignKey(
|
||||||
|
Company,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="external_chat_sessions",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Company this session belongs to",
|
||||||
|
)
|
||||||
start_time = models.DateTimeField()
|
start_time = models.DateTimeField()
|
||||||
end_time = models.DateTimeField()
|
end_time = models.DateTimeField()
|
||||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||||
@@ -39,7 +48,15 @@ class ChatMessage(models.Model):
|
|||||||
|
|
||||||
class ExternalDataSource(models.Model):
|
class ExternalDataSource(models.Model):
|
||||||
name = models.CharField(max_length=255, default="External API")
|
name = models.CharField(max_length=255, default="External API")
|
||||||
api_url = models.URLField(default="<https://proto.notso.ai/jumbo/chats>")
|
company = models.ForeignKey(
|
||||||
|
Company,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="external_data_sources",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Company this data source belongs to",
|
||||||
|
)
|
||||||
|
api_url = models.URLField(default="https://proto.notso.ai/jumbo/chats")
|
||||||
auth_username = models.CharField(max_length=255, blank=True, null=True)
|
auth_username = models.CharField(max_length=255, blank=True, null=True)
|
||||||
auth_password = models.CharField(
|
auth_password = models.CharField(
|
||||||
max_length=255, blank=True, null=True
|
max_length=255, blank=True, null=True
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ def fetch_and_store_chat_data(source_id=None):
|
|||||||
session, created = ChatSession.objects.update_or_create(
|
session, created = ChatSession.objects.update_or_create(
|
||||||
session_id=data["session_id"],
|
session_id=data["session_id"],
|
||||||
defaults={
|
defaults={
|
||||||
|
"company": source.company, # Link to the company from the data source
|
||||||
"start_time": start_time,
|
"start_time": start_time,
|
||||||
"end_time": end_time,
|
"end_time": end_time,
|
||||||
"ip_address": data.get("ip_address"),
|
"ip_address": data.get("ip_address"),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% load crispy_forms_filters %}
|
||||||
<!-- templates/accounts/login.html -->
|
<!-- templates/accounts/login.html -->
|
||||||
{% extends 'base.html' %} {% load crispy_forms_tags %}
|
{% extends 'base.html' %} {% load crispy_forms_tags %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
|
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
command: gunicorn dashboard_project.wsgi:application --bind 0.0.0.0:8000
|
command: uv run gunicorn dashboard_project.wsgi:application --bind 0.0.0.0:8000 --chdir dashboard_project
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
|
||||||
- static_volume:/app/staticfiles
|
- static_volume:/app/staticfiles
|
||||||
- media_volume:/app/media
|
- media_volume:/app/media
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DEBUG=0
|
- DATABASE_URL=postgresql://postgres:postgres@db:5432/dashboard_db
|
||||||
- SECRET_KEY=your_secret_key_here
|
- POSTGRES_DB=dashboard_db
|
||||||
- ALLOWED_HOSTS=localhost,127.0.0.1
|
- POSTGRES_USER=postgres
|
||||||
- DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_HOST=db
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -24,7 +25,7 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:13
|
image: postgres:alpine
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data/
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
environment:
|
environment:
|
||||||
@@ -35,7 +36,7 @@ services:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:alpine
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
volumes:
|
volumes:
|
||||||
@@ -48,12 +49,16 @@ services:
|
|||||||
|
|
||||||
celery:
|
celery:
|
||||||
build: .
|
build: .
|
||||||
command: celery -A dashboard_project worker --loglevel=info
|
command: uv run celery -A dashboard_project worker --loglevel=info --workdir dashboard_project
|
||||||
volumes:
|
env_file:
|
||||||
- .:/app
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DEBUG=0
|
- DATABASE_URL=postgresql://postgres:postgres@db:5432/dashboard_db
|
||||||
- DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
- POSTGRES_DB=dashboard_db
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_HOST=db
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -62,12 +67,16 @@ services:
|
|||||||
|
|
||||||
celery-beat:
|
celery-beat:
|
||||||
build: .
|
build: .
|
||||||
command: celery -A dashboard_project beat --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
command: uv run celery -A dashboard_project beat --scheduler django_celery_beat.schedulers:DatabaseScheduler --workdir dashboard_project
|
||||||
volumes:
|
env_file:
|
||||||
- .:/app
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DEBUG=0
|
- DATABASE_URL=postgresql://postgres:postgres@db:5432/dashboard_db
|
||||||
- DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
- POSTGRES_DB=dashboard_db
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_HOST=db
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Redis and Celery Configuration
|
# Redis and Celery Configuration
|
||||||
|
|
||||||
This document explains how to set up and use Redis and Celery for background task processing in the LiveGraphs application.
|
This document explains how to set up and use Redis and Celery for background task processing in the LiveGraphs
|
||||||
|
application.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -72,7 +73,8 @@ Download and install from [microsoftarchive/redis](https://github.com/microsofta
|
|||||||
|
|
||||||
### SQLite Fallback
|
### SQLite Fallback
|
||||||
|
|
||||||
If Redis is not available, the application will automatically fall back to using SQLite for Celery tasks. This works well for development but is not recommended for production.
|
If Redis is not available, the application will automatically fall back to using SQLite for Celery tasks. This works
|
||||||
|
well for development but is not recommended for production.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ If this fails, check the following:
|
|||||||
|
|
||||||
## Fixing CSV Data Processing Issues
|
## Fixing CSV Data Processing Issues
|
||||||
|
|
||||||
If you see the error `zip() argument 2 is shorter than argument 1`, it means the data format doesn't match the expected headers. We've implemented a fix that:
|
If you see the error `zip() argument 2 is shorter than argument 1`, it means the data format doesn't match the expected
|
||||||
|
headers. We've implemented a fix that:
|
||||||
|
|
||||||
1. Pads shorter rows with empty strings
|
1. Pads shorter rows with empty strings
|
||||||
2. Uses more flexible date format parsing
|
2. Uses more flexible date format parsing
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"name": "livegraphs-django",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .; bun format:py",
|
"format": "prettier --write .; bun format:py",
|
||||||
"format:check": "prettier --check .; bun format:py -- --check",
|
"format:check": "prettier --check .; bun format:py -- --check",
|
||||||
@@ -16,11 +18,15 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.56.1",
|
"@playwright/test": "^1.56.1",
|
||||||
|
"@types/bun": "latest",
|
||||||
"markdownlint-cli2": "^0.18.1",
|
"markdownlint-cli2": "^0.18.1",
|
||||||
"oxlint": "^1.25.0",
|
"oxlint": "^1.25.0",
|
||||||
"oxlint-tsgolint": "^0.5.0",
|
"oxlint-tsgolint": "^0.5.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-jinja-template": "^2.1.0",
|
"prettier-plugin-jinja-template": "^2.1.0",
|
||||||
"prettier-plugin-packagejson": "^2.5.19"
|
"prettier-plugin-packagejson": "^2.5.19"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ dependencies = [
|
|||||||
"numpy>=2.3.4",
|
"numpy>=2.3.4",
|
||||||
"pandas>=2.3.3",
|
"pandas>=2.3.3",
|
||||||
"plotly>=6.4.0",
|
"plotly>=6.4.0",
|
||||||
|
"psycopg2-binary>=2.9.11",
|
||||||
"python-dotenv>=1.2.1",
|
"python-dotenv>=1.2.1",
|
||||||
"redis>=7.0.1",
|
"redis>=7.0.1",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
|
|||||||
@@ -413,6 +413,31 @@ prompt-toolkit==3.0.52 \
|
|||||||
--hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \
|
--hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \
|
||||||
--hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955
|
--hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955
|
||||||
# via click-repl
|
# via click-repl
|
||||||
|
psycopg2-binary==2.9.11 \
|
||||||
|
--hash=sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1 \
|
||||||
|
--hash=sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b \
|
||||||
|
--hash=sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee \
|
||||||
|
--hash=sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316 \
|
||||||
|
--hash=sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c \
|
||||||
|
--hash=sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3 \
|
||||||
|
--hash=sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f \
|
||||||
|
--hash=sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0 \
|
||||||
|
--hash=sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1 \
|
||||||
|
--hash=sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5 \
|
||||||
|
--hash=sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f \
|
||||||
|
--hash=sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c \
|
||||||
|
--hash=sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c \
|
||||||
|
--hash=sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c \
|
||||||
|
--hash=sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4 \
|
||||||
|
--hash=sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766 \
|
||||||
|
--hash=sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d \
|
||||||
|
--hash=sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60 \
|
||||||
|
--hash=sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8 \
|
||||||
|
--hash=sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f \
|
||||||
|
--hash=sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f \
|
||||||
|
--hash=sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa \
|
||||||
|
--hash=sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747
|
||||||
|
# via livegraphsdjango
|
||||||
pygments==2.19.2 \
|
pygments==2.19.2 \
|
||||||
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
|
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
|
||||||
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
|
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
|
||||||
|
|||||||
7
seed.spec.ts
Normal file
7
seed.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Test group", () => {
|
||||||
|
test("seed", async ({ page }) => {
|
||||||
|
// generate code here.
|
||||||
|
});
|
||||||
|
});
|
||||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
32
uv.lock
generated
32
uv.lock
generated
@@ -542,6 +542,7 @@ dependencies = [
|
|||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
{ name = "plotly" },
|
{ name = "plotly" },
|
||||||
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
@@ -579,6 +580,7 @@ requires-dist = [
|
|||||||
{ name = "numpy", specifier = ">=2.3.4" },
|
{ name = "numpy", specifier = ">=2.3.4" },
|
||||||
{ name = "pandas", specifier = ">=2.3.3" },
|
{ name = "pandas", specifier = ">=2.3.3" },
|
||||||
{ name = "plotly", specifier = ">=6.4.0" },
|
{ name = "plotly", specifier = ">=6.4.0" },
|
||||||
|
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||||
{ name = "redis", specifier = ">=7.0.1" },
|
{ name = "redis", specifier = ">=7.0.1" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
@@ -846,6 +848,36 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.2"
|
version = "2.19.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user