diff --git a/Dockerfile b/Dockerfile
index e322a6f..36eb166 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,36 +5,28 @@ WORKDIR /app
ENV UV_COMPILE_BYTECODE=1
-# Copy dependency files
COPY pyproject.toml uv.lock ./
-# Install dependencies
RUN uv sync --frozen --no-dev --no-install-project
-# Copy application code
COPY iotDashboard/ ./iotDashboard/
COPY manage.py ./
COPY create_user.py ./
-# Sync the project
RUN uv sync --frozen --no-dev
-# Stage 2: Runtime
FROM python:3.13-alpine
WORKDIR /app
-# Install runtime dependencies
RUN apk add --no-cache postgresql-client
-# Copy virtual environment and application
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/iotDashboard/ /app/iotDashboard/
COPY --from=builder /app/manage.py /app/
COPY --from=builder /app/create_user.py /app/
-# Create non-root user
RUN adduser -D -u 1000 appuser && \
chown -R appuser:appuser /app
@@ -45,5 +37,4 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 3000
-# Run Django with uvicorn for ASGI
CMD ["python", "-m", "uvicorn", "iotDashboard.asgi:application", "--host", "0.0.0.0", "--port", "3000"]
diff --git a/db_migrations/Dockerfile b/db_migrations/Dockerfile
index 4b777c8..885e0a8 100644
--- a/db_migrations/Dockerfile
+++ b/db_migrations/Dockerfile
@@ -4,7 +4,7 @@ WORKDIR /app
COPY pyproject.toml ./
-RUN uv sync
+RUN uv sync --no-cache
COPY . .
diff --git a/db_migrations/alembic/versions/20251215_2124_7c71d43d53e3_add_users_table.py b/db_migrations/alembic/versions/20251215_2124_7c71d43d53e3_add_users_table.py
deleted file mode 100644
index b9fbdb4..0000000
--- a/db_migrations/alembic/versions/20251215_2124_7c71d43d53e3_add_users_table.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""add users table
-
-Revision ID: 7c71d43d53e3
-Revises: 4b84a36e13f5
-Create Date: 2025-12-15 21:24:36.718471+00:00
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '7c71d43d53e3'
-down_revision: Union[str, Sequence[str], None] = '4b84a36e13f5'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- """Upgrade schema."""
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('users',
- sa.Column('id', sa.Text(), nullable=False),
- sa.Column('username', sa.Text(), nullable=False),
- sa.Column('email', sa.Text(), nullable=False),
- sa.Column('password_hash', sa.Text(), nullable=False),
- sa.Column('is_active', sa.Boolean(), nullable=True),
- sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('email'),
- sa.UniqueConstraint('username')
- )
- op.create_index('idx_users_email', 'users', ['email'], unique=False)
- op.create_index('idx_users_username', 'users', ['username'], unique=False)
- op.drop_index(op.f('dashboard_l_is_defa_033b71_idx'), table_name='dashboard_layouts')
- op.drop_index(op.f('dashboard_l_name_c36020_idx'), table_name='dashboard_layouts')
- op.drop_index(op.f('dashboard_layouts_name_349f3640_like'), table_name='dashboard_layouts', postgresql_ops={'name': 'varchar_pattern_ops'})
- op.drop_table('dashboard_layouts')
- op.drop_table('django_migrations')
- op.drop_table('iotDashboard_device')
- op.drop_constraint(op.f('telemetry_device_id_fkey'), 'telemetry', type_='foreignkey')
- op.create_foreign_key(None, 'telemetry', 'devices', ['device_id'], ['id'])
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- """Downgrade schema."""
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_constraint(None, 'telemetry', type_='foreignkey')
- op.create_foreign_key(op.f('telemetry_device_id_fkey'), 'telemetry', 'devices', ['device_id'], ['id'], ondelete='CASCADE')
- op.create_table('iotDashboard_device',
- sa.Column('id', sa.BIGINT(), sa.Identity(always=False, start=1, increment=1, minvalue=1, maxvalue=9223372036854775807, cycle=False, cache=1), autoincrement=True, nullable=False),
- sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=False),
- sa.Column('ip', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
- sa.Column('protocol', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
- sa.PrimaryKeyConstraint('id', name=op.f('iotDashboard_device_pkey'))
- )
- op.create_table('django_migrations',
- sa.Column('id', sa.BIGINT(), sa.Identity(always=False, start=1, increment=1, minvalue=1, maxvalue=9223372036854775807, cycle=False, cache=1), autoincrement=True, nullable=False),
- sa.Column('app', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
- sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
- sa.Column('applied', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
- sa.PrimaryKeyConstraint('id', name=op.f('django_migrations_pkey'))
- )
- op.create_table('dashboard_layouts',
- sa.Column('id', sa.BIGINT(), sa.Identity(always=False, start=1, increment=1, minvalue=1, maxvalue=9223372036854775807, cycle=False, cache=1), autoincrement=True, nullable=False),
- sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
- sa.Column('config', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=False),
- sa.Column('is_default', sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
- sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
- sa.PrimaryKeyConstraint('id', name=op.f('dashboard_layouts_pkey')),
- sa.UniqueConstraint('name', name=op.f('dashboard_layouts_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
- )
- op.create_index(op.f('dashboard_layouts_name_349f3640_like'), 'dashboard_layouts', ['name'], unique=False, postgresql_ops={'name': 'varchar_pattern_ops'})
- op.create_index(op.f('dashboard_l_name_c36020_idx'), 'dashboard_layouts', ['name'], unique=False)
- op.create_index(op.f('dashboard_l_is_defa_033b71_idx'), 'dashboard_layouts', ['is_default'], unique=False)
- op.drop_index('idx_users_username', table_name='users')
- op.drop_index('idx_users_email', table_name='users')
- op.drop_table('users')
- # ### end Alembic commands ###
diff --git a/db_migrations/alembic/versions/20251215_2156_1dfb0bb45f93_add_cascade_delete_to_telemetry_foreign_.py b/db_migrations/alembic/versions/20251215_2156_1dfb0bb45f93_add_cascade_delete_to_telemetry_foreign_.py
deleted file mode 100644
index 7b4326c..0000000
--- a/db_migrations/alembic/versions/20251215_2156_1dfb0bb45f93_add_cascade_delete_to_telemetry_foreign_.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""add cascade delete to telemetry foreign key
-
-Revision ID: 1dfb0bb45f93
-Revises: 7c71d43d53e3
-Create Date: 2025-12-15 21:56:13.260281+00:00
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '1dfb0bb45f93'
-down_revision: Union[str, Sequence[str], None] = '7c71d43d53e3'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- """Upgrade schema - only update telemetry foreign key."""
- # Drop old foreign key constraint
- op.drop_constraint('telemetry_device_id_fkey', 'telemetry', type_='foreignkey')
-
- # Add new foreign key constraint with CASCADE delete
- op.create_foreign_key(
- 'telemetry_device_id_fkey',
- 'telemetry', 'devices',
- ['device_id'], ['id'],
- ondelete='CASCADE'
- )
-
-
-def downgrade() -> None:
- """Downgrade schema - revert foreign key change."""
- # Drop CASCADE foreign key
- op.drop_constraint('telemetry_device_id_fkey', 'telemetry', type_='foreignkey')
-
- # Add back original foreign key without CASCADE
- op.create_foreign_key(
- 'telemetry_device_id_fkey',
- 'telemetry', 'devices',
- ['device_id'], ['id']
- )
diff --git a/db_migrations/pyproject.toml b/db_migrations/pyproject.toml
index f22bc29..0bd1f87 100644
--- a/db_migrations/pyproject.toml
+++ b/db_migrations/pyproject.toml
@@ -6,5 +6,7 @@ readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"alembic>=1.17.0",
+ "dotenv>=0.9.9",
"sqlalchemy>=2.0.44",
+ "psycopg2-binary>=2.9.10",
]
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index a1ed40b..a1a1eff 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -8,6 +8,16 @@ RUN npm ci
COPY . .
+ARG VITE_API_URL=/api
+ARG VITE_DEVICE_MANAGER_URL
+ARG VITE_MQTT_BROKER
+ARG VITE_MQTT_PORT=8883
+
+ENV VITE_API_URL=$VITE_API_URL
+ENV VITE_DEVICE_MANAGER_URL=$VITE_DEVICE_MANAGER_URL
+ENV VITE_MQTT_BROKER=$VITE_MQTT_BROKER
+ENV VITE_MQTT_PORT=$VITE_MQTT_PORT
+
RUN npm run build
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
index 0ec0f59..4c94f06 100644
--- a/frontend/nginx.conf
+++ b/frontend/nginx.conf
@@ -15,19 +15,6 @@ server {
try_files $uri $uri/ /index.html;
}
- # Proxy API requests to Django backend
- location /api/ {
- proxy_pass http://django:3000;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection 'upgrade';
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache_bypass $http_upgrade;
- }
-
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index f92559b..c4c20f9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,7 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter, Routes, Route, Link, NavLink, Navigate } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'
+import { useState } from 'react'
import { WellnessStateProvider } from './hooks/useWellnessState'
import { AuthProvider, useAuth } from './contexts/AuthContext'
import Dashboard from './pages/Dashboard'
@@ -19,11 +20,12 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
function AppLayout({ children }: { children: React.ReactNode }) {
const { logout } = useAuth()
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
return (
-
+
-
+
{/* Navbar */}
@@ -44,7 +46,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
{/* Page content */}
-
+
{children}
@@ -52,48 +54,71 @@ function AppLayout({ children }: { children: React.ReactNode }) {
{/* Sidebar */}
-