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:
2025-11-05 20:22:07 +01:00
parent 81d1469e18
commit 2236eeb9a5
21 changed files with 563 additions and 51 deletions

View File

@@ -27,7 +27,21 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", get_random_secret_key())
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
@@ -85,13 +99,28 @@ TEMPLATES = [
WSGI_APPLICATION = "dashboard_project.wsgi.application"
# Database
# Use PostgreSQL when DATABASE_URL is set (Docker), otherwise SQLite (local dev)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
if os.environ.get("DATABASE_URL"):
# PostgreSQL configuration for Docker
DATABASES = {
"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

View File

@@ -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.")

View File

@@ -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.")

View File

@@ -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",
),
),
]

View File

@@ -1,10 +1,19 @@
import os
from accounts.models import Company
from django.db import models
class ChatSession(models.Model):
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()
end_time = models.DateTimeField()
ip_address = models.GenericIPAddressField(null=True, blank=True)
@@ -39,7 +48,15 @@ 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>")
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_password = models.CharField(
max_length=255, blank=True, null=True

View File

@@ -144,6 +144,7 @@ def fetch_and_store_chat_data(source_id=None):
session, created = ChatSession.objects.update_or_create(
session_id=data["session_id"],
defaults={
"company": source.company, # Link to the company from the data source
"start_time": start_time,
"end_time": end_time,
"ip_address": data.get("ip_address"),

View File

@@ -1,3 +1,4 @@
{% load crispy_forms_filters %}
<!-- templates/accounts/login.html -->
{% extends 'base.html' %} {% load crispy_forms_tags %}
{% block title %}