diff --git a/.dockerignore b/.dockerignore index 97156f2..2ff924c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,38 @@ +# Dependencies **/node_modules **/.venv -**/.git -**/__pycache__ + +# Build outputs **/.next +**/__pycache__ +**/*.pyc +**/*.pyo + +# Version control +**/.git +**/.gitignore + +# Test artifacts **/.pytest_cache +**/tests +**/test_*.py +**/*_test.py +**/.coverage + +# Dev-only data +**/chroma_db +**/uploads +**/sql_app.db + +# Secrets — pass at runtime via env_file or environment +**/.env +**/.env.* + +# Misc +**/bloats +**/.vscode +**/.claude +**/postman_collection.json +**/lint_results*.txt +**/test_output.txt +**/test.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3ad10a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,95 @@ +# ============================================================================= +# FRONTEND — build stages +# ============================================================================= + +FROM node:20-alpine AS frontend-deps +WORKDIR /app +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +FROM frontend-deps AS frontend-builder +WORKDIR /app +COPY frontend/ . + +ARG NEXT_PUBLIC_ENVIRONMENT +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_BACKEND_URL_PROD +ARG NEXT_PUBLIC_FRONTEND_URL_PROD +ARG NEXT_PUBLIC_BACKEND_URL_STAGING +ARG NEXT_PUBLIC_FRONTEND_URL_STAGING +ARG NEXT_PUBLIC_BACKEND_URL_DEV +ARG NEXT_PUBLIC_FRONTEND_URL_DEV + +ENV NEXT_PUBLIC_ENVIRONMENT=$NEXT_PUBLIC_ENVIRONMENT \ + NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ + NEXT_PUBLIC_BACKEND_URL_PROD=$NEXT_PUBLIC_BACKEND_URL_PROD \ + NEXT_PUBLIC_FRONTEND_URL_PROD=$NEXT_PUBLIC_FRONTEND_URL_PROD \ + NEXT_PUBLIC_BACKEND_URL_STAGING=$NEXT_PUBLIC_BACKEND_URL_STAGING \ + NEXT_PUBLIC_FRONTEND_URL_STAGING=$NEXT_PUBLIC_FRONTEND_URL_STAGING \ + NEXT_PUBLIC_BACKEND_URL_DEV=$NEXT_PUBLIC_BACKEND_URL_DEV \ + NEXT_PUBLIC_FRONTEND_URL_DEV=$NEXT_PUBLIC_FRONTEND_URL_DEV + +RUN npm run build + +FROM node:20-alpine AS frontend +WORKDIR /app + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY --from=frontend-builder /app/public ./public +COPY --from=frontend-builder /app/.next/standalone ./ +COPY --from=frontend-builder /app/.next/static ./.next/static + +RUN chown -R nextjs:nodejs /app +USER nextjs + +ENV NODE_ENV=production \ + PORT=3000 \ + HOSTNAME="0.0.0.0" + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 + +CMD ["node", "server.js"] + + +# ============================================================================= +# BACKEND — build stages +# ============================================================================= + +FROM python:3.10-slim AS backend-builder +WORKDIR /app + +RUN pip install --no-cache-dir uv +ENV UV_HTTP_TIMEOUT=300 + +COPY backend/pyproject.toml backend/uv.lock ./ +RUN uv sync --frozen --no-cache + +FROM python:3.10-slim AS backend +WORKDIR /app + +RUN pip install --no-cache-dir uv + +COPY --from=backend-builder /app/.venv /app/.venv + +ENV PATH="/app/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +COPY backend/ . + +RUN adduser --disabled-password --gecos '' appuser && \ + chown -R appuser:appuser /app && \ + chmod +x /app/start.sh +USER appuser + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 + +CMD ["/app/start.sh"] diff --git a/backend/Pipfile b/backend/Pipfile deleted file mode 100644 index d61ea53..0000000 --- a/backend/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] - -[requires] -python_version = "3.13" diff --git a/backend/__init__.py b/backend/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/alembic/env.py b/backend/alembic/env.py index b78b452..a209f7e 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -30,7 +30,6 @@ WhatsAppCampaignMessage, ) - # Alembic Config object config = context.config @@ -41,14 +40,11 @@ POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") POSTGRES_DB = os.getenv("POSTGRES_DB") -if all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): +if os.getenv("DATABASE_URL"): + config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL")) +elif all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): database_url = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%")) -else: - # Fallback: if individual vars are missing, try the full DATABASE_URL (e.g., from Heroku, Render, Railway) - fallback_url = os.getenv("DATABASE_URL") - if fallback_url: - config.set_main_option("sqlalchemy.url", fallback_url.replace("%", "%%")) # Logging setup if config.config_file_name is not None: diff --git a/backend/app/api/analytics.py b/backend/app/api/analytics.py index 99dd9a8..e42f99b 100644 --- a/backend/app/api/analytics.py +++ b/backend/app/api/analytics.py @@ -13,6 +13,7 @@ from app.services.analysis_agent import generate_followup_content from app.models.widget import GuestMessage from app.core.response_wrapper import success_response +from app.core.config import settings as app_settings router = APIRouter() @@ -44,7 +45,7 @@ def get_analytics_overview( ) total_sessions = query.count() - + # Better: Count distinct Guest IDs in sessions query total_guests = query.with_entities(ChatSession.guest_id).distinct().count() @@ -179,8 +180,6 @@ class FollowUpRequest(BaseModel): type: str # "email" or "transcript" extra_info: Optional[str] = "" -from app.core.config import settings as app_settings - @router.post("/followup", response_model=None) async def generate_followup( request: FollowUpRequest, diff --git a/backend/app/api/business.py b/backend/app/api/business.py index 5954a32..ab3ef04 100644 --- a/backend/app/api/business.py +++ b/backend/app/api/business.py @@ -142,6 +142,29 @@ def _enrich_plan_fields(response: BusinessResponse, business: Business, db: Sess response.plan_features = features +@router.post("/business/validate-key", response_model=None) +async def validate_api_key( + key_data: dict, # Using dict to avoid importing ValidateKeyRequest for now or just generic + current_user: User = Depends(get_current_user) +): + """Validate a Google Gemini API Key.""" + api_key = key_data.get("api_key") + if not api_key: + raise HTTPException(status_code=400, detail="API Key is required") + + try: + from google import genai + client = genai.Client(api_key=api_key) + client.models.generate_content( + model='gemini-2.0-flash', + contents='Test' + ) + + except Exception as e: + print(f"Key Validation Failed: {e}") + raise HTTPException(status_code=400, detail=f"Invalid API Key: {str(e)}") + + @router.post("/business/generate-intents", response_model=None) async def generate_intents( current_user: User = Depends(get_current_user), diff --git a/backend/app/api/widget.py b/backend/app/api/widget.py index 84f5438..e74c29e 100644 --- a/backend/app/api/widget.py +++ b/backend/app/api/widget.py @@ -20,6 +20,7 @@ from app.services.agent_service import run_conversation from app.auth.router import get_current_user from app.core.response_wrapper import success_response +from app.services.analysis_agent import analyze_session, persist_analysis # Additional Schema for Updating Settings from pydantic import BaseModel @@ -863,8 +864,6 @@ def get_session_details_widget( ] }) -from app.services.analysis_agent import analyze_session, persist_analysis - @router.post("/session/{session_id}/analyze", response_model=None) async def analyze_chat_session(session_id: str, db: Session = Depends(get_db)): # 1. Verify session exists diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py index 25f7884..d23b0ff 100644 --- a/backend/app/auth/router.py +++ b/backend/app/auth/router.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from fastapi.responses import RedirectResponse +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session from app.db.session import get_db from app.models.user import User @@ -12,8 +13,6 @@ router = APIRouter(prefix="/auth", tags=["auth"]) -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials - security = HTTPBearer() # --- Dependency: Get Current User --- diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 47118d9..08a4df1 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -85,6 +85,19 @@ class LocalConfig(BaseConfig): ] CORS_ALLOW_ORIGIN_REGEX: str = ".*" +class StagingConfig(BaseConfig): + FRONTEND_URI: str = "https://taimakoai.onrender.com" + FRONTEND_REDIRECT_URI: str = "https://taimakoai.onrender.com/auth/callback" + + CORS_ORIGINS: List[str] = [ + "https://taimakoai.onrender.com", + ] + + ALLOWED_HOSTS: List[str] = [ + "taimako.onrender.com", + "taimakoai.onrender.com", + ] + class ProductionConfig(BaseConfig): FRONTEND_URI: str = os.getenv("FRONTEND_LIVE_URI", "https://taimako.dubem.xyz") FRONTEND_REDIRECT_URI: str = f"{os.getenv('FRONTEND_LIVE_URI', 'https://taimako.dubem.xyz')}/auth/callback" @@ -92,17 +105,17 @@ class ProductionConfig(BaseConfig): # Paystack PAYSTACK_SECRET_KEY: str = os.getenv("PAYSTACK_LIVE_SECRET_KEY", "") PAYSTACK_PUBLIC_KEY: str = os.getenv("PAYSTACK_LIVE_PUBLIC_KEY", "") - + CORS_ORIGINS: List[str] = [ - "https://taimako.dubem.xyz", - "https://www.taimako.dubem.xyz" + "https://taimako.dubem.xyz", + "https://www.taimako.dubem.xyz", ] CORS_ALLOW_ORIGIN_REGEX: str = r"https?://.*" - + ALLOWED_HOSTS: List[str] = [ - "taimako.dubem.xyz", + "taimako.dubem.xyz", "api.taimako.dubem.xyz", - "*.taimako.dubem.xyz" + "*.taimako.dubem.xyz", ] # USE_HTTPS_REDIRECT: bool = True # Uncomment if handling SSL termination directly @@ -112,8 +125,7 @@ def get_settings(): if env == "production": return ProductionConfig() elif env == "staging": - # Treating staging similar to prod for now, or can be its own class - return ProductionConfig() + return StagingConfig() return LocalConfig() settings = get_settings() diff --git a/backend/app/db/session.py b/backend/app/db/session.py index 4dfa0a3..4554ab7 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -10,15 +10,15 @@ POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") POSTGRES_DB = os.getenv("POSTGRES_DB") -# Build PostgreSQL URL if all required parts are present -if all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): +# DATABASE_URL takes priority — cloud platforms (Render, Railway, Heroku) provide this directly +if os.getenv("DATABASE_URL"): + SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL") +# Build from individual vars when DATABASE_URL is absent (docker-compose local dev) +elif all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): SQLALCHEMY_DATABASE_URL = ( f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" ) -# Allow direct DATABASE_URL override (common on Heroku, Render, Railway, etc.) -elif os.getenv("DATABASE_URL"): - SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL") -# Final fallback: SQLite for quick local development, cause why not, eh? +# Final fallback: SQLite for quick local development else: DATABASE_PATH = os.getenv("DATABASE_PATH", "./sql_app.db") SQLALCHEMY_DATABASE_URL = f"sqlite:///{DATABASE_PATH}" diff --git a/backend/app/main copy.py b/backend/app/main copy.py deleted file mode 100644 index a382b60..0000000 --- a/backend/app/main copy.py +++ /dev/null @@ -1,108 +0,0 @@ -from fastapi import FastAPI -from fastapi.exceptions import HTTPException, RequestValidationError -from fastapi.middleware.cors import CORSMiddleware -from fastapi.middleware.trustedhost import TrustedHostMiddleware -from slowapi import Limiter, _rate_limit_exceeded_handler -from slowapi.util import get_remote_address -from slowapi.errors import RateLimitExceeded -from slowapi.middleware import SlowAPIMiddleware -import os - -from app.api.routes import router as api_router -from app.auth.router import router as auth_router -from app.core.exception_handler import ( - http_exception_handler, - validation_exception_handler, - general_exception_handler -) -from app.core.security_headers import SecurityHeadersMiddleware - -# Create tables (if not using alembic, but we are. Keeping for dev convenience or removing if strictly alembic) -# Base.metadata.create_all(bind=engine) - -# Initialize Rate Limiter -limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"]) - -app = FastAPI( - title="Agentic RAG API", - description="API for Agentic RAG with Google OAuth2 and Multi-Agent Delegation.", - version="1.0.0", - docs_url="/docs", - redoc_url="/redoc" -) - -# Associate limiter with app -app.state.limiter = limiter -app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) - -# Register exception handlers -app.add_exception_handler(HTTPException, http_exception_handler) -app.add_exception_handler(RequestValidationError, validation_exception_handler) -app.add_exception_handler(Exception, general_exception_handler) - -# Environment -environment = os.getenv("ENVIRONMENT", "local") - -# Rate Limit Middleware -app.add_middleware(SlowAPIMiddleware) - -# Security Middlewares -# I am commenting out the HTTPS redirect middleware for now -# because this project is hosted behind a reverse proxy -# if environment in ["production", "staging"]: - # app.add_middleware(HTTPSRedirectMiddleware) - -app.add_middleware( - TrustedHostMiddleware, - allowed_hosts=["localhost", "127.0.0.1", "*.taimako.dubem.xyz", "taimako.dubem.xyz", "*.staging.taimako.ai", "api.taimako.dubem.xyz"] -) - -app.add_middleware(SecurityHeadersMiddleware) - -# Configure CORS -# origins = [ -# "http://localhost:3000", -# "http://127.0.0.1:5500", -# "http://localhost:8000", -# ] - -origins = [ - "http://localhost:3000", - "http://localhost:8000", - "https://taimako.dubem.xyz", - "https://taimako.dubem.xyz/", -] - -app.add_middleware( - CORSMiddleware, - allow_origin_regex=".*", - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(api_router) -app.include_router(auth_router) - -# Import and include business router -from app.api.business import router as business_router -app.include_router(business_router) - -from app.api.widget import router as widget_router -app.include_router(widget_router, prefix="/widgets", tags=["widgets"]) - -from app.api.analytics import router as analytics_router -app.include_router(analytics_router, prefix="/analytics", tags=["analytics"]) - -from app.api.escalation import router as escalation_router -app.include_router(escalation_router, prefix="/escalations", tags=["escalations"]) - -from app.core.response_wrapper import success_response - -@app.get("/") -async def root(): - return success_response(message="Agentic RAG API is running") - -@app.get("/health") -async def health_check(): - return {"status": "healthy"} diff --git a/backend/app/main.py b/backend/app/main.py index 66f0f78..79904aa 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,6 +3,20 @@ from starlette.middleware.sessions import SessionMiddleware from sqladmin import Admin +from app.api.routes import router as api_router +from app.auth.router import router as auth_router +from app.api.business import router as business_router +from app.api.widget import router as widget_router +from app.api.analytics import router as analytics_router +from app.api.escalation import router as escalation_router +from app.api.subscription import router as subscription_router +from app.api.plans import router as plans_router +from app.api.whatsapp import router as whatsapp_router +from app.api.whatsapp_templates import router as whatsapp_templates_router +from app.api.whatsapp_contacts import router as whatsapp_contacts_router +from app.api.whatsapp_campaigns import router as whatsapp_campaigns_router +from app.api.orders import router as orders_router + from app.core.config import settings from app.db.session import engine from app.core.exception_handler import ( @@ -11,6 +25,7 @@ general_exception_handler ) from app.core.middleware import register_middleware +from app.core.response_wrapper import success_response from app.admin.auth import AdminAuth from app.admin.views import ( UserAdmin, BusinessAdmin, PlanAdmin, PaymentTransactionAdmin, @@ -61,45 +76,20 @@ app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(Exception, general_exception_handler) -from app.api.routes import router as api_router -from app.auth.router import router as auth_router app.include_router(api_router) app.include_router(auth_router) - -from app.api.business import router as business_router app.include_router(business_router) - -from app.api.widget import router as widget_router app.include_router(widget_router, prefix="/widgets", tags=["widgets"]) - -from app.api.analytics import router as analytics_router app.include_router(analytics_router, prefix="/analytics", tags=["analytics"]) - -from app.api.escalation import router as escalation_router app.include_router(escalation_router, prefix="/escalations", tags=["escalations"]) - -from app.api.subscription import router as subscription_router app.include_router(subscription_router, tags=["subscription"]) - -from app.api.plans import router as plans_router app.include_router(plans_router, tags=["plans"]) - -from app.api.whatsapp import router as whatsapp_router app.include_router(whatsapp_router, prefix="/whatsapp", tags=["whatsapp"]) - -from app.api.whatsapp_templates import router as whatsapp_templates_router app.include_router(whatsapp_templates_router, prefix="/whatsapp", tags=["whatsapp"]) - -from app.api.whatsapp_contacts import router as whatsapp_contacts_router app.include_router(whatsapp_contacts_router, prefix="/whatsapp", tags=["whatsapp"]) - -from app.api.whatsapp_campaigns import router as whatsapp_campaigns_router app.include_router(whatsapp_campaigns_router, prefix="/whatsapp", tags=["whatsapp"]) - -from app.api.orders import router as orders_router app.include_router(orders_router) -from app.core.response_wrapper import success_response @app.get("/") async def root(): diff --git a/backend/app/services/agent_service.py b/backend/app/services/agent_service.py index 775f053..036e27b 100644 --- a/backend/app/services/agent_service.py +++ b/backend/app/services/agent_service.py @@ -1,3 +1,15 @@ +import logging +import warnings +from typing import Optional + +from google.adk.runners import Runner +from google.genai import types + +from app.services.agent_system.service import session_service, init_session +from app.services.agent_system.agent_factory import AgentFactory + +warnings.filterwarnings("ignore") + try: from app.services.rag_service import rag_service except ImportError: @@ -12,18 +24,6 @@ def query(self, text, user_id): USER_ID = "test_user" SESSION_ID = "test_session" -from google.adk.runners import Runner -from google.genai import types -import logging -from typing import Optional - -# Import from new modular structure -from app.services.agent_system.service import session_service, init_session -from app.services.agent_system.agent_factory import AgentFactory - -import warnings -warnings.filterwarnings("ignore") - logging.basicConfig(level=logging.ERROR) diff --git a/backend/postman_collection.json b/backend/postman_collection.json deleted file mode 100644 index 5279620..0000000 --- a/backend/postman_collection.json +++ /dev/null @@ -1,350 +0,0 @@ -{ - "info": { - "_postman_id": "agentic-rag-api-collection", - "name": "Agentic RAG API", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Auth", - "item": [ - { - "name": "Signup", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"user@example.com\",\n \"password\": \"securepassword\",\n \"name\": \"Test User\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/auth/signup", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "signup" - ] - } - }, - "response": [] - }, - { - "name": "Login", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var jsonData = pm.response.json();", - "if (jsonData.status === 'success') {", - " pm.environment.set(\"access_token\", jsonData.data.access_token);", - " pm.environment.set(\"refresh_token\", jsonData.data.refresh_token);", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"user@example.com\",\n \"password\": \"securepassword\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/auth/login", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "login" - ] - } - }, - "response": [] - }, - { - "name": "Google Login", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/auth/google/login", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "google", - "login" - ] - } - }, - "response": [] - }, - { - "name": "Refresh Token", - "request": { - "method": "POST", - "header": [], - "url": { - "raw": "{{base_url}}/auth/refresh?refresh_token={{refresh_token}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "refresh" - ], - "query": [ - { - "key": "refresh_token", - "value": "{{refresh_token}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Me", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/auth/me", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "me" - ] - } - }, - "response": [] - }, - { - "name": "Logout", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{base_url}}/auth/logout", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "logout" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Documents", - "item": [ - { - "name": "Upload Documents", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "files", - "type": "file", - "src": [] - } - ] - }, - "url": { - "raw": "{{base_url}}/documents/upload", - "host": [ - "{{base_url}}" - ], - "path": [ - "documents", - "upload" - ] - } - }, - "response": [] - }, - { - "name": "List Documents", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/documents", - "host": [ - "{{base_url}}" - ], - "path": [ - "documents" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "RAG", - "item": [ - { - "name": "Process Documents", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{base_url}}/rag/process", - "host": [ - "{{base_url}}" - ], - "path": [ - "rag", - "process" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Chat", - "item": [ - { - "name": "Chat", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"message\": \"What is in the document?\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/chat", - "host": [ - "{{base_url}}" - ], - "path": [ - "chat" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Root", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/", - "host": [ - "{{base_url}}" - ], - "path": [ - "" - ] - } - }, - "response": [] - } - ], - "variable": [ - { - "key": "base_url", - "value": "http://localhost:8000", - "type": "string" - } - ] -} \ No newline at end of file diff --git a/backend/start.sh b/backend/start.sh new file mode 100644 index 0000000..1b64f76 --- /dev/null +++ b/backend/start.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +alembic upgrade head +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1 --proxy-headers --forwarded-allow-ips '*' diff --git a/backend/tests/api/test_auth_google.py b/backend/tests/api/test_auth_google.py index 214fe40..7effc00 100644 --- a/backend/tests/api/test_auth_google.py +++ b/backend/tests/api/test_auth_google.py @@ -2,6 +2,7 @@ from app.main import app from app.core.config import settings from app.core.security import create_access_token +from app.models.user import User # noqa: F401 def test_login_google_redirect(client): # Debug: Print all routes @@ -38,8 +39,6 @@ def test_google_callback(mock_get_user_info, mock_exchange_code, client): assert "access_token" in response.headers["location"] assert "refresh_token" in response.headers["location"] -from app.models.user import User - def test_get_me_unauthorized(client): response = client.get("/auth/me") assert response.status_code == 403 # HTTPBearer raises 403 when token is missing diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 62843da..9741e3e 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -51,7 +51,6 @@ # ===== Core Fixtures ===== - @pytest.fixture(scope="function") def db_session(): """Fresh in-memory SQLite database per test.""" diff --git a/backend/tests/integration/test_escalation_flow.py b/backend/tests/integration/test_escalation_flow.py index 5c60c58..fb15b3d 100644 --- a/backend/tests/integration/test_escalation_flow.py +++ b/backend/tests/integration/test_escalation_flow.py @@ -105,7 +105,7 @@ def refresh_side_effect(instance): assert "pending" in result_json def test_escalation_api(): - # Only if we can spin up a real DB or mock everything. + # Only if we can spin up a real DB or mock everything. # Validating the router exists and endpoint signature is correct. # The endpoint needs a real DB session. # Since I cannot easily setup a full integration test environment in this turn without checking concurrency, diff --git a/backend/tests/unit/tools/test_escalate_to_human.py b/backend/tests/unit/tools/test_escalate_to_human.py index 7a14bc1..054e828 100644 --- a/backend/tests/unit/tools/test_escalate_to_human.py +++ b/backend/tests/unit/tools/test_escalate_to_human.py @@ -111,7 +111,6 @@ def test_creates_escalation_record( def refresh_side_effect(instance): instance.id = "esc-789" mock_db_session.refresh.side_effect = refresh_side_effect - escalate_to_human( reason="User frustrated", user_message="I need help!", diff --git a/bloats/SUBSCRIPTION_PLAN.md b/bloats/SUBSCRIPTION_PLAN.md new file mode 100644 index 0000000..b831895 --- /dev/null +++ b/bloats/SUBSCRIPTION_PLAN.md @@ -0,0 +1,366 @@ +To set up a subscription API for your FastAPI project using Paystack, you should follow a "Transaction-First" flow. This is the most reliable way to handle the initial payment and automatic subscription. + +### 1. High-Level Flow +1. **Selection:** User selects a plan (`nexus` or `flux`). +2. **Initialization:** Your API calls Paystack's **Initialize Transaction** endpoint with the specific `plan_code`. +3. **Payment:** The user is redirected to Paystack to pay. Upon successful payment, Paystack automatically creates a **Subscription** and a **Customer**. +4. **Notification:** Paystack sends a `subscription.create` and `charge.success` event to your **Webhook**. +5. **Provisioning:** Your webhook updates your database to grant the user access based on the plan. + +--- + +### 2. Implementation Steps + +#### Step 1: Configuration & Plan Mapping +Store your Paystack secret key and map your internal plan names to the Paystack `plan_code` you created in your dashboard. + +```python +# .env +PAYSTACK_SECRET_KEY=sk_test_xxxxxx +PAYSTACK_WEBHOOK_SECRET=xxxxxx # For signature verification + +# config.py +PLAN_MAPPING = { + "nexus": "PLN_nexus_code_here", + "flux": "PLN_flux_code_here" +} +``` + +#### Step 2: Database Models +You need to track the subscription status and the Paystack unique identifiers. + +```python +class User(Base): + __tablename__ = "users" + id = Column(Integer, primary_key=True) + email = Column(String, unique=True) + paystack_customer_code = Column(String, nullable=True) + subscription_code = Column(String, nullable=True) + subscription_status = Column(String, default="inactive") # active, non-renewing, attention, cancelled + plan_type = Column(String, nullable=True) # nexus, flux +``` + +#### Step 3: Paystack Service (FastAPI Utility) +Use `httpx` for asynchronous requests to Paystack. + +```python +import httpx +from .config import PAYSTACK_SECRET_KEY + +async def initialize_subscription(email: str, plan_code: str): + url = "https://api.paystack.co/transaction/initialize" + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + payload = { + "email": email, + "plan": plan_code, + "callback_url": "https://yourfrontend.com/payment-verify" + } + async with httpx.AsyncClient() as client: + response = await client.post(url, json=payload, headers=headers) + return response.json() +``` + +#### Step 4: The Initialization Endpoint +This is what the user clicks to start their subscription. + +```python +@app.post("/subscribe/{plan_name}") +async def start_subscription(plan_name: str, current_user: User = Depends(get_current_user)): + if plan_name not in PLAN_MAPPING: + raise HTTPException(status_code=400, detail="Invalid plan") + + plan_code = PLAN_MAPPING[plan_name] + res = await initialize_subscription(current_user.email, plan_code) + + if res["status"]: + # Return the authorization_url to the frontend to redirect the user + return {"checkout_url": res["data"]["authorization_url"]} + raise HTTPException(status_code=400, detail="Could not initialize payment") +``` + +#### Step 5: Webhook Handler (Crucial for Security) +Paystack will notify your backend when payments occur or subscriptions are created. **You must verify the signature.** + +```python +import hmac +import hashlib + +@app.post("/paystack/webhook") +async def paystack_webhook(request: Request, x_paystack_signature: str = Header(None)): + # 1. Verify Signature + body = await request.body() + computed_signature = hmac.new( + PAYSTACK_WEBHOOK_SECRET.encode(), + body, + hashlib.sha512 + ).hexdigest() + + if computed_signature != x_paystack_signature: + raise HTTPException(status_code=401, detail="Invalid signature") + + payload = await request.json() + event = payload["event"] + data = payload["data"] + + # 2. Handle Events + if event == "subscription.create": + # Store the subscription_code and customer_code in DB + user_email = data["customer"]["email"] + # Update user record: set subscription_code, status='active' + + elif event == "charge.success": + # Initial or recurring payment succeeded + # Ensure user access is extended + + elif event == "subscription.disable" or event == "invoice.payment_failed": + # Revoke access or notify user + pass + + return {"status": "success"} +``` + +#### Step 6: Subscription Management (Cancellation) +To cancel, you need the `subscription_code` and `email_token` (returned by Paystack during the `subscription.create` event). + +```python +async def cancel_subscription(subscription_code: str, email_token: str): + url = "https://api.paystack.co/subscription/disable" + payload = {"code": subscription_code, "token": email_token} + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + async with httpx.AsyncClient() as client: + response = await client.post(url, json=payload, headers=headers) + return response.status_code == 200 +``` + +#### Step 7: Subscription Management (Renewal) + + +### 3. Key Tips for Your Setup +* **Don't rely on the frontend callback:** The user might close their browser before the callback triggers. Always rely on the **Webhook** to update the subscription status in your database. +* **Customer Creation:** You don't need to manually create a customer first. Calling the `initialize` endpoint with an email automatically creates or retrieves the customer on Paystack's end. +* **Metadata:** You can pass `metadata: {"user_id": 123}` in the initialization request. This metadata will be sent back in the webhook payload, making it easier to link the payment to your local user ID. +* **Testing:** Use Paystack's test cards (e.g., the "Success" card) to trigger the `subscription.create` webhook locally using a tool like **ngrok** to expose your FastAPI server. + + +Subscription Renewal & Upgrade Implementation + +Since Paystack handles renewals automatically, your primary job is to listen for success/failure events and provide a flow for users to switch between your nexus and flux plans. + +1. Handling Automatic Renewals + +Paystack manages the billing cycle for you.[1][2] You only need to respond to the webhooks to keep your database in sync. + +Webhook Logic for Renewals + +When a renewal occurs, Paystack sends a charge.success event.[1][2][3] The payload will contain the plan code and the subscription_code. + +code +Python +download +content_copy +expand_less +# Inside your webhook handler +if event == "charge.success": + data = payload["data"] + # Check if this charge belongs to a subscription + subscription_code = data.get("plan", {}).get("subscription_code") + + if subscription_code: + # This is a renewal payment + user = db.query(User).filter(User.subscription_code == subscription_code).first() + if user: + user.subscription_status = "active" + user.last_payment_date = datetime.utcnow() + db.commit() + +elif event == "invoice.payment_failed": + # Renewal failed (e.g., insufficient funds) + subscription_code = data["subscription"]["subscription_code"] + user = db.query(User).filter(User.subscription_code == subscription_code).first() + if user: + user.subscription_status = "attention" # User has access but needs to pay + db.commit() +2. Plan Upgrades (e.g., Nexus → Flux) + +Paystack does not have a "Change Plan" button for an existing subscription ID.[3] To upgrade a user, you must disable the old subscription and create a new one. + +Strategy A: Immediate Upgrade (Charge Now) + +Use this if you want the user to pay the full price of the new plan immediately. + +Initialize Transaction: Call the initialize endpoint with the new plan_code.[1][3] + +Verify & Cleanup: Once the user pays, disable the old nexus subscription and store the new flux subscription code. + +Strategy B: Seamless Upgrade (Use Existing Card) + +If you already have the user's authorization_code (from their first payment), you can create the new subscription via API without the user typing their card details again. + +code +Python +download +content_copy +expand_less +async def upgrade_user_plan(user: User, new_plan_name: str): + # 1. Get the new plan code + new_plan_code = PLAN_MAPPING[new_plan_name] + + # 2. Disable current subscription first + # (Optional: You can wait for the new one to succeed first) + await cancel_subscription(user.subscription_code, user.email_token) + + # 3. Create new subscription using saved authorization + url = "https://api.paystack.co/subscription" + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + payload = { + "customer": user.paystack_customer_code, + "plan": new_plan_code, + "authorization": user.last_authorization_code # Stored during first payment + } + + async with httpx.AsyncClient() as client: + res = await client.post(url, json=payload, headers=headers) + data = res.json() + if data["status"]: + user.subscription_code = data["data"]["subscription_code"] + user.plan_type = new_plan_name + # Paystack will charge the card immediately for the new plan + return {"message": "Upgraded successfully"} +3. Plan Downgrades + +Downgrades are usually handled at the end of the current billing cycle to avoid "loosing" money the user already paid. + +Request received: User clicks "Downgrade to Nexus".[3] + +Set to Cancel: Call the Paystack Disable Subscription endpoint, but only at the end of the period or use the "non-renewing" status logic. + +New Subscription: On the day the old plan expires, your system (via a background task or the subscription.disable webhook) creates the new lower-tier subscription. + +4. Important Considerations + +Proration: Paystack does not support native proration (e.g., charging only the $5 difference for half a month). Most developers either: + +Charge Full Price: Start the new plan immediately (simplest). + +Delay Upgrade: Wait until the current month ends, then switch. + +Retry Logic: Paystack does not retry failed subscription renewals.[1][2][3] If a renewal fails, the status becomes attention.[6] You should email the user with a payment link to reactivate.[1][7] + +Authorization Storage: Always save the authorization_code from the first successful charge.success event. You need this code to perform manual charges or programmatically create new subscriptions for that user later.[3] + + +Yes, you absolutely should. + +While you could hardcode the plan codes in your config.py, saving them to a local database table is the professional approach. Here is why it’s better and how to implement it: + +1. Why you should save them locally + +Performance: You can display your pricing page instantly without waiting for a Paystack API response. + +Feature Mapping: Paystack only stores the "price" and "interval." Your database can store the features (e.g., "nexus" gets 10 projects, "flux" gets unlimited). + +Data Integrity: You can create a foreign key relationship between your Subscriptions table and your Plans table. + +Dynamic Updates: If you decide to add a third plan (e.g., "spark") later, you can just click a "Sync Plans" button in your admin panel instead of redeploying code. + +2. Proposed Database Schema + +You should create a Plan model that mirrors the Paystack data but adds your own application-specific fields. + +code +Python +download +content_copy +expand_less +class Plan(Base): + __tablename__ = "plans" + + id = Column(Integer, primary_key=True) + name = Column(String, unique=True) # nexus, flux, spark + plan_code = Column(String, unique=True) # PLN_wfc5nh... + amount = Column(Integer) # Store in kobo (e.g., 1500000) + interval = Column(String) # monthly, annually + currency = Column(String, default="NGN") + + # App-specific fields (Not in Paystack) + is_active = Column(Boolean, default=True) + features = Column(JSON) # ["Unlimited Storage", "Priority Support"] + max_users = Column(Integer) # Internal limit for your logic +3. Implementation Plan: The "Sync" Script + +Create a utility function or a background task to keep your local database in sync with Paystack. + +code +Python +download +content_copy +expand_less +import httpx + +async def sync_paystack_plans(db: Session): + url = "https://api.paystack.co/plan" + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + paystack_plans = response.json()["data"] + + for p in paystack_plans: + # Check if plan exists + db_plan = db.query(Plan).filter(Plan.plan_code == p["plan_code"]).first() + + if db_plan: + # Update existing plan (price/name might have changed) + db_plan.amount = p["amount"] + db_plan.name = p["name"] + else: + # Create new plan + new_plan = Plan( + name=p["name"], + plan_code=p["plan_code"], + amount=p["amount"], + interval=p["interval"], + currency=p["currency"] + # set default features here + ) + db.add(new_plan) + + db.commit() +4. How this changes your Subscription Flow + +Now, instead of passing a string to your initialization endpoint, you use the database ID. + +Before: POST /subscribe/nexus +After: POST /subscribe/{plan_id} + +code +Python +download +content_copy +expand_less +@app.post("/subscribe/{plan_id}") +async def start_subscription(plan_id: int, db: Session = Depends(get_db)): + plan = db.query(Plan).filter(Plan.id == plan_id).first() + if not plan: + raise HTTPException(status_code=404, detail="Plan not found") + + # Use the plan_code from the DB to initialize Paystack + res = await initialize_subscription(current_user.email, plan.plan_code) + return res +5. Important Warning: Price Changes + +If you change a plan's price on Paystack: + +Existing subscribers are not affected. They will continue to pay the old price they signed up with. + +New subscribers will pay the new price. + +Local DB: Your local plans table should be updated (via the sync script) so that your pricing page shows the correct new price to potential customers. + +Recommendation: + +Create the table. + +Add a "Sync" endpoint (protected for admins only). + +Manually add "features" to the rows in the database after syncing, or create a simple Admin UI to manage the extra metadata. diff --git a/bloats/add_column.py b/bloats/add_column.py new file mode 100644 index 0000000..6de8b78 --- /dev/null +++ b/bloats/add_column.py @@ -0,0 +1,30 @@ +import sqlite3 +import os + +db_path = 'sql_app.db' +print(f"Connecting to {db_path}...") + +if not os.path.exists(db_path): + print(f"Error: {db_path} not found!") + exit(1) + +try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + # Check if column exists first to avoid error if run multiple times + cursor.execute("PRAGMA table_info(chat_sessions)") + columns = [info[1] for info in cursor.fetchall()] + + if 'sentiment_score' in columns: + print("Column 'sentiment_score' already exists.") + else: + print("Adding column 'sentiment_score'...") + cursor.execute("ALTER TABLE chat_sessions ADD COLUMN sentiment_score FLOAT") + conn.commit() + print("Column added successfully.") + +except Exception as e: + print(f"Migration Error: {e}") +finally: + if conn: + conn.close() diff --git a/bloats/api_docs.md b/bloats/api_docs.md new file mode 100644 index 0000000..f7ee152 --- /dev/null +++ b/bloats/api_docs.md @@ -0,0 +1,253 @@ +# API Documentation + +Base URL: `http://localhost:8000` (or `http://localhost:8100` if running via Docker default in Makefile) + +## Authentication +All endpoints except authentication routes require a valid JWT token in the Authorization header: +``` +Authorization: Bearer +``` + +## Endpoints + +### Authentication + +#### 1. Sign Up +- **URL**: `/auth/signup` +- **Method**: `POST` +- **Content-Type**: `application/json` + +**Request Body**: +```json +{ + "email": "user@example.com", + "password": "securepassword", + "name": "John Doe" +} +``` + +#### 2. Login +- **URL**: `/auth/login` +- **Method**: `POST` +- **Content-Type**: `application/json` + +**Request Body**: +```json +{ + "email": "user@example.com", + "password": "securepassword" +} +``` + +**Response**: +```json +{ + "status": "success", + "data": { + "access_token": "eyJ...", + "refresh_token": "eyJ...", + "token_type": "bearer" + } +} +``` + +### Business Management + +#### 3. Create Business Profile +Create a business profile with custom agent configuration. + +- **URL**: `/business` +- **Method**: `POST` +- **Content-Type**: `application/json` +- **Auth**: Required + +**Request Body**: +```json +{ + "business_name": "Acme Support", + "description": "Customer support for Acme products", + "website": "https://acme.com", + "custom_agent_instruction": "Always be professional and mention our 24/7 support availability." +} +``` + +**Response**: +```json +{ + "status": "success", + "message": "Business profile created successfully", + "data": { + "id": "uuid", + "user_id": "uuid", + "business_name": "Acme Support", + "description": "Customer support for Acme products", + "website": "https://acme.com", + "custom_agent_instruction": "Always be professional and mention our 24/7 support availability.", + "created_at": "2025-12-08T00:00:00", + "updated_at": "2025-12-08T00:00:00" + } +} +``` + +#### 4. Get Business Profile +Retrieve the current user's business profile. + +- **URL**: `/business` +- **Method**: `GET` +- **Auth**: Required + +**Response**: +```json +{ + "status": "success", + "data": { + "id": "uuid", + "user_id": "uuid", + "business_name": "Acme Support", + "description": "Customer support for Acme products", + "website": "https://acme.com", + "custom_agent_instruction": "Always be professional and mention our 24/7 support availability.", + "created_at": "2025-12-08T00:00:00", + "updated_at": "2025-12-08T00:00:00" + } +} +``` + +#### 5. Update Business Profile +Update business profile fields. + +- **URL**: `/business` +- **Method**: `PUT` +- **Content-Type**: `application/json` +- **Auth**: Required + +**Request Body** (all fields optional): +```json +{ + "business_name": "Updated Name", + "description": "New description", + "website": "https://newsite.com", + "custom_agent_instruction": "New instructions for the agent" +} +``` + +### Document Management + +#### 6. Upload Documents +Uploads one or more files to the staging area. + +- **URL**: `/documents/upload` +- **Method**: `POST` +- **Content-Type**: `multipart/form-data` +- **Auth**: Required + +**Request Body**: +- `files`: List of files to upload (Binary) + +**Response**: +```json +{ + "status": "success", + "message": "Files uploaded successfully", + "data": { + "files": [ + "document1.pdf", + "notes.txt" + ] + } +} +``` + +#### 7. List Documents +Get all documents for the current user. + +- **URL**: `/documents` +- **Method**: `GET` +- **Auth**: Required + +**Response**: +```json +{ + "status": "success", + "data": [ + "document1.pdf", + "notes.txt" + ] +} +``` + +#### 8. Process Documents +Triggers the processing of uploaded documents (text splitting, embedding, and indexing). + +- **URL**: `/rag/process` +- **Method**: `POST` +- **Auth**: Required + +**Response**: +```json +{ + "status": "success", + "data": [ + { + "filename": "document1.pdf", + "chunks_created": 15, + "status": "success" + }, + { + "filename": "notes.txt", + "chunks_created": 8, + "status": "success" + } + ] +} +``` + +### Chat + +#### 9. Chat with Agent +Interact with the AI agent which uses your business configuration and processed documents as context. + +> **Note**: You must create a business profile before using this endpoint. + +- **URL**: `/chat` +- **Method**: `POST` +- **Content-Type**: `application/json` +- **Auth**: Required + +**Request Body**: +```json +{ + "message": "What are your support hours?" +} +``` + +**Response**: +```json +{ + "status": "success", + "data": { + "response": "We offer 24/7 support availability for all our customers. You can reach us anytime...", + "sources": [] + } +} +``` + +## Key Features + +- **User-Scoped Data**: Each user's documents and business configuration are isolated +- **Dynamic Agent**: Agent is created with your business name and custom instructions +- **Context-Aware**: Agent only accesses your uploaded documents +- **Session Management**: Conversation history is maintained per user + +## Running the API + +You can access the interactive Swagger UI at `/docs` (e.g., `http://localhost:8000/docs`) to test endpoints directly in the browser. + +## Workflow + +1. **Sign up** or **Login** to get authentication tokens +2. **Create a business profile** with your business details and custom agent instructions +3. **Upload documents** related to your business +4. **Process documents** to make them searchable +5. **Chat with the agent** - it will use your business context and documents to provide relevant responses + diff --git a/bloats/business_plan.md b/bloats/business_plan.md new file mode 100644 index 0000000..0094566 --- /dev/null +++ b/bloats/business_plan.md @@ -0,0 +1,36 @@ +# Taimako: Intelligent Customer Engagement for African SMEs + +## Executive Summary +Taimako is a next-generation customer engagement platform tailored specifically for the unique needs of Small and Medium-sized Enterprises (SMEs) in Africa. We democratize access to enterprise-grade Artificial Intelligence, enabling local businesses to automate customer support, capture valuable leads 24/7, and gain deep insights into their market—without the complexity or cost of traditional enterprise software. + +## The Problem +As African SMEs digitize, they face critical challenges in scaling their operations: +1. **Missed Opportunities**: Business owners cannot be online 24/7. Inquiries that come in after hours or during peak times often go unanswered, resulting in lost sales. +2. **Lack of Customer Insight**: Most SMEs rely on gut feeling rather than data. They know *how many* sales they made, but they rarely know *who* visited their site, *where* they came from, or *why* they didn't buy. +3. **Fragmented Communication**: Managing inquiries across WhatsApp, phone calls, and website forms is chaotic and inefficient. + +## The Solution: An Agent That Works for You +Taimako provides an "always-on" AI employee that lives on the business's website. It is not just a chatbot; it is a context-aware sales and support agent. + +### 1. Intelligent Automated Support +Our embedded widget acts as the first line of defense. It instantly answers customer questions about pricing, services, and business hours by "reading" the customized knowledge base provided by the business owner. It handles routine queries so the business owner can focus on high-value work. + +### 2. Actionable Business Intelligence +We turn passive website traffic into actionable data. Our dashboard provides clear, non-technical insights: +* **Visitor Intent**: Understand exactly *why* people are visiting (e.g., "70% of visitors asked about Delivery Pricing"). +* **Geographic Insights**: See where potential customers are located to better target marketing efforts. +* **Traffic Sources**: Identify whether customers are coming from Google, Instagram, or direct referrals. + +### 3. Lead Capture & Conversion +The AI proactively identifies high-intent visitors and captures their contact details (Name, Phone, Email), turning anonymous traffic into concrete leads. + +## Technology Foundation +To ensure reliability, speed, and scalability, Taimako is built on a modern, high-performance technology stack: +* **Backend**: Powered by **FastAPI**, ensuring that our AI processing is lightning-fast and capable of handling thousands of simultaneous conversations without lag. +* **Frontend**: Built with **Next.js**, delivering a seamless, app-like experience for both the business dashboard and the customer-facing widget, ensuring smooth performance on all devices, especially mobile, which is critical for the African market. + +## Market Opportunity +The African digital economy is exploding, with millions of SMEs coming online. These businesses are leapfrogging traditional desktop-first tools and demanding mobile-centric, automated, and intelligent solutions. Taimako is positioned to be the operating system for this new wave of digital commerce, providing the essential tools for growth in a competitive manufacturing and service landscape. + +## Conclusion +Taimako is more than software; it is a growth partner. By automating the "busy work" of customer service and illuminating the "blind spots" of customer behavior, we empower African entrepreneurs to scale their businesses faster and smarter. diff --git a/bloats/check_business.py b/bloats/check_business.py new file mode 100644 index 0000000..0eaf14b --- /dev/null +++ b/bloats/check_business.py @@ -0,0 +1,19 @@ +import os +import sys + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.db.session import engine +from sqlalchemy.orm import sessionmaker + +from app.models.business import Business + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +db = SessionLocal() + +businesses = db.query(Business).all() +for b in businesses: + print(f"Business: {b.business_name} | Email: {b.user.email if b.user else 'No User'}") + print(f"Tier: {b.subscription_tier}") + print(f"AI Responses: {b.allocated_ai_responses}") + print(f"Daily Sessions: {b.allocated_daily_sessions}") + print("-" * 40) diff --git a/bloats/check_tx.py b/bloats/check_tx.py new file mode 100644 index 0000000..6a29ad5 --- /dev/null +++ b/bloats/check_tx.py @@ -0,0 +1,27 @@ +import sys +import os +import json + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.db.session import engine +from sqlalchemy.orm import sessionmaker + +from app.models.payment import PaymentTransaction + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +db = SessionLocal() + +txs = db.query(PaymentTransaction).order_by(PaymentTransaction.created_at.desc()).limit(5).all() +for tx in txs: + print(f"TX {tx.id} - TYPE: {tx.transaction_type} - REF: {tx.reference} - CREATED_AT: {tx.created_at}") + if tx.transaction_metadata: + meta = tx.transaction_metadata + if isinstance(meta, str): + try: + meta = json.loads(meta) + except (ValueError, TypeError): + pass + + data = meta.get('data', {}) if isinstance(meta, dict) else {} + print("Metadata inside payload:", dict(data.get('metadata', {}))) + print("-" * 40) diff --git a/bloats/drop_chat_session.py b/bloats/drop_chat_session.py new file mode 100644 index 0000000..10b940b --- /dev/null +++ b/bloats/drop_chat_session.py @@ -0,0 +1,57 @@ +import sqlite3 + +def clean_db(): + conn = sqlite3.connect('sql_app.db') + cursor = conn.cursor() + + try: + print("Dropping chat_sessions table...") + cursor.execute("DROP TABLE IF EXISTS chat_sessions") + print("Dropped chat_sessions.") + except Exception as e: + print(f"Error dropping chat_sessions: {e}") + + # Dropping column in sqlite is tricky, usually requires recreating table. + # But since Alembic batch operations handle this, and the column might not exist if previous run failed halfway. + # Actually, if previous run failed on FK creation, the column might be there. + # But we can't easily drop column in pure sqlite without new table. + # However, if we just drop chat_sessions, the FK constraint from guest_messages might linger if it wasn't named? + # No, FK is on the guest_messages table. + # If I don't drop the column `session_id` from `guest_messages`, Alembic might fail saying column exists or similar. + # Alembic's `add_column` will fail if column exists. + + # Let's inspect if column exists. + cursor.execute("PRAGMA table_info(guest_messages)") + columns = [info[1] for info in cursor.fetchall()] + if 'session_id' in columns: + print("Column session_id exists in guest_messages.") + # We must remove it. + # SQLite way: create new table without col, copy, drop old, rename. + # Too risky to do manually? + # Maybe I should just manually modify the migration file to skip adding column if exists? + # NO, that's hacking. + # Let's try to remove it using sqlite approach in this script. + + print("Recreating guest_messages without session_id...") + # Get schema + cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='guest_messages'") + cursor.fetchone()[0] + # create_sql has the session_id definition? maybe. + # Easier: Rename table to _old, create new table (how to get schema?), copy, drop _old. + # Actually, let's just use the fact that I can use python to do this cleanly if I had the Model. + # But I don't want to load app stack. + + # Alternative: Just use alembic downgrade? + # I can try `alembic downgrade e2cd2430b4a5` (the base revision). + # But downgrading requires the migration to have been marked as done? It failed, so it's not marked? + # Or maybe it IS marked but failed? + # No, if it exits with 1, it probably didn't update alembic_version. + pass + else: + print("Column session_id does not exist.") + + conn.commit() + conn.close() + +if __name__ == "__main__": + clean_db() diff --git a/bloats/emergent_ventures_application.md b/bloats/emergent_ventures_application.md new file mode 100644 index 0000000..26cdc94 --- /dev/null +++ b/bloats/emergent_ventures_application.md @@ -0,0 +1,90 @@ +# Emergent Ventures Grant Application - TaimakoAI + +## 1. About You +**Instructions:** *The first part of the proposal should be about you. Tell us your personal story, and how it relates to what you wish to do. We probably don't care much about your formal education, credentials, or awards, unless they're particularly germane to who you are or your idea. Do tell us your background briefly, but credentials are not what will impress us.* + +[USER TO COMPLETE: Insert your personal story here. Focus on...] +* **Motivation:** Why do you care about African SMEs? Did you grow up in a family of small business owners? Have you seen the struggle of digitization first-hand? +* **The Spark:** What specific moment or experience led you to build Taimako? +* **Agency:** Examples of times you’ve taken initiative or built something from scratch. + +> **Drafting Tip:** "I grew up watching local businesses struggle not because they lacked quality products, but because they lacked the tools to communicate effectively at scale. My background in software engineering allows me to bridge this gap, but my drive comes from seeing the potential of the African digital economy unlocked." + +--- + +## 2. Consensus View +**Instructions:** *Second, what is one mainstream or "consensus" view that you absolutely agree with? (This is our version of a "trick" question, reversing the now-fashionable contrarianism.)* + +**Proposed Answer:** +"I absolutely agree with the consensus view that **talent is equally distributed, but opportunity is not.**" + +*Why this works:* This aligns perfectly with the mission of TaimakoAI (democratizing access to enterprise-grade AI for African SMEs). It positions your startup not just as a tech product, but as a mechanism for correcting this imbalance by providing the "opportunity" (tools) to the "talent" (SME owners). + +*Alternative Option:* +"I agree with the consensus that **software is eating the world**, but with the caveat that it hasn't finished its meal yet—especially in emerging markets where the 'last mile' of digitization is still being built." + +--- + +## 3. The Idea +**Instructions:** *The third part should be about your idea. Convince us that this is a great idea worth investing in, and tell us what is new or unusual in your vision and understanding. What's the problem you intend to solve?* + +**The Problem: The "Always-On" Gap in African Commerce** +As African SMEs digitize, they face a critical bottleneck: scaling human attention. Business owners are overwhelmed by inquiries across fragmented channels (WhatsApp, phone, Instagram), often missing sales that come in after hours or during peak times. They rely on gut feeling rather than data, lacking visibility into visitor intent ("70% asked about delivery") or traffic sources. Existing solutions are either too expensive (Enterprise SaaS) or too simple (basic FAQ bots). + +**The Solution: Taimako - The AI Employee for African SMEs** +Taimako is not just a chatbot; it is an intelligent, context-aware "AI employee" that lives on a business's website. It democratizes enterprise-grade AI for the African market by: + +1. **Intelligent Automation:** Instantly answering customer queries about pricing, services, and hours by "reading" the business's unique knowledge base. It handles the 80% of routine queries so owners can focus on high-value work. +2. **Actionable Intelligence:** Turning passive traffic into deep insights. Our dashboard visualizes visitor intent, sentiment, and geographic data, replacing "gut feeling" with data-driven decision making. +3. **Proactive Lead Capture:** Identifying high-intent visitors and capturing their contact details (Name, Phone, Email), turning anonymous browsers into concrete leads. +4. **Multi-Channel Integration:** Taimako meets customers where they are. Beyond the website widget, we provide seamless integration with **WhatsApp, Telegram, and Instagram**, allowing businesses to manage all their customer interactions from a single, unified dashboard. + +**What is New/Unusual?** +Most AI tools are built for the West and ported to Africa. Taimako is built *for* the African context, prioritizing: +* **Mobile-First Experiences:** Critical for a region where most internet access is mobile. +* **WhatsApp-Style Simplicity:** Mirroring the interfaces users already know and trust. +* **Lightweight Integration:** Optimized for slower connections and older devices. +We aren't just selling software; we are building the "operating system" for the next generation of African digital commerce. + +--- + +## Bonus: Taimako in a Tweet +**Option 1 (Focus on Problem/Solution):** +"African SMEs lose millions in sales because they can't be online 24/7. Taimako is the 'AI Employee' that lives on their site—answering queries, capturing leads, and closing sales while the owner sleeps. Enterprise-grade AI, built for the African context. 🚀🌍 #AfricanTech #AI #SME" + +**Option 2 (Focus on Empowerment):** +"Democratizing AI for African commerce. Taimako isn't just a chatbot; it's a context-aware sales agent that turns passive website traffic into revenue for SMEs. From WhatsApp to Web, we handle the busy work so entrepreneurs can scale. 📈🤖" + +--- + +## 4. Budget (Ballpark) +**Instructions:** *If you have a ballpark budget (with revenue sources and expenses), let us know the bare basics now; we won't hold you to it strictly.* + +**Estimated Monthly Run Rate (Year 1): $2,000 - $3,000** + +* **Infrastructure (AWS/DigitalOcean):** $500/mo (Hosting FastAPI backend, Next.js frontend, Postgres DB). +* **AI Inference (OpenAI/Anthropic/Local LLMs):** $1,000/mo (Scaling with user base). +* **Third-party Services (Email/SMS/Auth):** $200/mo. +* **Marketing & Outreach:** $500/mo. + +**Ask:** A grant of **$10,000 - $25,000** would allow us to: +1. Cover infrastructure costs for the first 12 months. +2. Aggressively onboard the first 100 beta users without charging them, accelerating our feedback loop. +3. Fine-tune our proprietary models on local dialects and commerce patterns. + +--- + +## 5. Project Status +**Instructions:** *Also (if applicable) tell us how long you have been working on this project or idea, whether you will be working on it full time or part time...* + +**Status:** +I have been working on TaimakoAI for [X months/weeks]. The MVP is currently **live in development**, featuring: +* Use of **FastAPI** for a high-performance backend. +* **Next.js** for a responsive, mobile-first frontend. +* Core features implemented: Customizable widgets, real-time analytics dashboard, and automated escalation workflows. + +**Commitment:** +I am working on this [Full-time / Part-time]. + +**Partners/Support:** +[Mention any co-founders or early advisors here. If solo, emphasize your ability to execute across the full stack.] diff --git a/bloats/emergent_ventures_proposal.md b/bloats/emergent_ventures_proposal.md new file mode 100644 index 0000000..6a3f526 --- /dev/null +++ b/bloats/emergent_ventures_proposal.md @@ -0,0 +1,69 @@ +# Emergent Ventures Grant Application - TaimakoAI + +## 1. About You +I am a software engineer deeply passionate about unlocking the potential of the African digital economy. Growing up, I watched brilliant local entrepreneurs with incredible products struggle to scale simply because they lacked the tools to communicate effectively and manage their operations efficiently. While the West debates the nuances of AGI, African SMEs are still trying to figure out how to answer customer queries at 2 AM without hiring a night shift. + +My technical background allows me to bridge this gap, but my drive comes from a conviction that technology should be an equalizer, not a gatekeeper. I built Taimako not as an academic exercise, but as a direct response to the "always-on" problem I saw friends and family facing in their businesses. I have a bias for action and a history of building practical solutions to real-world problems, and I am committed to making enterprise-grade AI accessible to the millions of small business owners who are the backbone of our economy. + +--- + +## 2. Consensus View +I absolutely agree with the consensus view that **talent is equally distributed, but opportunity is not.** + +This isn't just a philosophical stance; it is the core thesis of TaimakoAI. We are building tools to distribute opportunity. By giving a small fashion vendor in Lagos the same 24/7 customer service capabilities as a global e-commerce giant, we are leveling the playing field and allowing talent to rise based on merit, not infrastructure. + +--- + +## 3. The Idea +**The Problem: The "Always-On" Gap in African Commerce** +As African SMEs digitize, they face a critical bottleneck: scaling human attention. Business owners are overwhelmed by inquiries across fragmented channels (WhatsApp, phone, Instagram), often missing sales that come in after hours or during peak times. They rely on gut feeling rather than data, lacking visibility into visitor intent ("70% asked about delivery") or traffic sources. Existing solutions are either too expensive (Enterprise SaaS) or too simple (basic FAQ bots). + +**The Solution: Taimako - The AI Employee for African SMEs** +Taimako is an intelligent, context-aware "AI employee" that lives on a business's website and communication channels. It democratizes enterprise-grade AI for the African market by: + +1. **Intelligent Automation:** Instantly answering customer queries about pricing, services, and hours by "reading" the business's unique knowledge base. It handles the 80% of routine queries so owners can focus on high-value work. +2. **Actionable Intelligence:** Turning passive traffic into deep insights. Our dashboard visualizes visitor intent, sentiment, and geographic data, replacing "gut feeling" with data-driven decision making. +3. **Proactive Lead Capture:** Identifying high-intent visitors and capturing their contact details (Name, Phone, Email), turning anonymous browsers into concrete leads. +4. **Multi-Channel Integration:** Taimako meets customers where they are. We provide seamless integration with **WhatsApp, Telegram, and Instagram**, allowing businesses to manage all their customer interactions from a single, unified dashboard. + +**What is New/Unusual?** +Most AI tools are built for the West and ported to Africa. Taimako is built *for* the African context from day one. We prioritize **mobile-first experiences** (critical for a region where most internet access is mobile), **WhatsApp-style simplicity** (mirroring the interfaces users already know and trust), and **lightweight integration** optimized for slower connections and older devices. We aren't just selling software; we are building the "operating system" for the next generation of African digital commerce. + +--- + +## Bonus: Taimako in a Tweet +"African SMEs lose millions in sales because they can't be online 24/7. Taimako is the 'AI Employee' that lives on their site—answering queries, capturing leads, and closing sales while the owner sleeps. Enterprise-grade AI, built for the African context. 🚀🌍 #AfricanTech #AI #SME" + +--- + +## 4. Budget (Ballpark) +I am requesting a grant of **$25,000** to cover our runway for the next 12 months. + +**Estimated Monthly Run Rate (Year 1): ~$2,000** +* **Infrastructure (AWS/DigitalOcean):** $500/mo (Hosting FastAPI backend, Next.js frontend, Postgres DB). +* **AI Inference (OpenAI/Anthropic/Local LLMs):** $1,000/mo (Scaling with user base). +* **Third-party Services (Email/SMS/Auth):** $200/mo. +* **Marketing & Outreach:** $500/mo. + +**Impact of Funding:** +This grant will allow us to: +1. Cover infrastructure costs for the first year, removing financial pressure. +2. Aggressively onboard the first 100 beta users for free, accelerating our feedback loop without friction. +3. Fine-tune our proprietary models on local dialects and commerce patterns to increase accuracy and trust. + +--- + +## 5. Project Status +**Status:** +I have been working on TaimakoAI full-time for the past **3 months**. The MVP is currently **live in development** and functioning. + +**Key Features Implemented:** +* High-performance backend built with **FastAPI**. +* Responsive, mobile-first frontend built with **Next.js**. +* Customizable chat widgets, real-time analytics dashboard, and automated escalation workflows are fully operational. + +**Commitment:** +I am working on this project **Full-time**. + +**Partners/Support:** +I am currently a solo founder executing across the full stack (Backend, Frontend, AI Engineering). I am actively building a network of early adopters and advisors within the local SME community to guide product development. diff --git a/bloats/populate_plans.py b/bloats/populate_plans.py new file mode 100644 index 0000000..f84cbdf --- /dev/null +++ b/bloats/populate_plans.py @@ -0,0 +1,49 @@ +import sys +import os + +# Ensure app is in python path +sys.path.append(os.getcwd()) + +from app.db.session import SessionLocal +from app.models.plan import Plan +from app.core.subscription import TIER_LIMITS + +def populate_plans(): + db = SessionLocal() + try: + print("Starting plan population...") + for tier_name, features in TIER_LIMITS.items(): + plan_code = tier_name + name = tier_name.capitalize() + description = features.get("description", "") + + # Check if plan exists + existing_plan = db.query(Plan).filter(Plan.plan_code == plan_code).first() + if existing_plan: + print(f"Updating plan: {name}") + existing_plan.name = name + existing_plan.description = description + existing_plan.features = features + # Keep existing price/currency if set manually, otherwise defaults apply for new + else: + print(f"Creating plan: {name}") + new_plan = Plan( + plan_code=plan_code, + name=name, + description=description, + features=features, + price=0, + currency="NGN" + ) + db.add(new_plan) + + db.commit() + print("Plan population completed successfully.") + except Exception as e: + print(f"Error populating plans: {e}") + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + populate_plans() diff --git a/bloats/reindex_vectors.py b/bloats/reindex_vectors.py new file mode 100644 index 0000000..c20f2e1 --- /dev/null +++ b/bloats/reindex_vectors.py @@ -0,0 +1,71 @@ +import sys +import os + +# Add backend directory to sys.path to allow imports +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from app.db.session import SessionLocal +from app.services.vector_db import vector_db +from app.services.rag_service import rag_service +from sqlalchemy import text + +def reindex(): + print("Starting Re-indexing Process...") + + # 1. Reset Vector DB + try: + print("Deleting existing vector collection 'rag_documents'...") + try: + vector_db.client.delete_collection("rag_documents") + print("✅ Collection deleted.") + except Exception as e: + # It might complain if it doesn't exist + print(f"⚠️ Delete collection message (proceeding): {e}") + + print("Recreating collection 'rag_documents'...") + # This updates the reference in the singleton vector_db instance too + vector_db.collection = vector_db.client.create_collection("rag_documents") + print("✅ Collection recreated.") + + except Exception as e: + print(f"❌ Error resetting vector DB: {e}") + return + + db = SessionLocal() + try: + # 2. Reset Document Status in Postgres + print("Resetting all document statuses to 'pending' in Postgres...") + + # Using SQLAlchemy Core for bulk update + db.execute(text("UPDATE documents SET status = 'pending', error_message = NULL")) + db.commit() + print("✅ Documents marked as pending.") + + # 3. Trigger Processing + print("Triggering processing for all users...") + + # Get all distinct user_ids with documents + # We need to process per user because RAG service expects user_id + result = db.execute(text("SELECT DISTINCT user_id FROM documents")) + user_ids = [row[0] for row in result] + + print(f"Found {len(user_ids)} users with documents.") + + for user_id in user_ids: + print(f"Processing documents for user: {user_id}...") + # process_documents handles fetching the API key via Business model + results = rag_service.process_documents(user_id, db) + + success_count = sum(1 for r in results if r.status == "success") + error_count = len(results) - success_count + print(f" User {user_id}: {success_count} succeeded, {error_count} failed.") + + print("\n✅ Re-indexing Complete!") + + except Exception as e: + print(f"\n❌ Error during re-indexing: {e}") + finally: + db.close() + +if __name__ == "__main__": + reindex() diff --git a/bloats/reset_password.py b/bloats/reset_password.py new file mode 100644 index 0000000..1ac5ec2 --- /dev/null +++ b/bloats/reset_password.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Quick script to reset a user's password.""" +import sys + +# Add the app directory to the path +sys.path.insert(0, '/app') + +from app.db.session import SessionLocal +from app.models.user import User +from app.core.security import get_password_hash + +def reset_password(email: str, new_password: str): + db = SessionLocal() + try: + user = db.query(User).filter(User.email == email).first() + if not user: + print(f"User with email {email} not found!") + return False + + user.hashed_password = get_password_hash(new_password) + db.commit() + print(f"Password reset successfully for {email}") + return True + finally: + db.close() + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python reset_password.py ") + sys.exit(1) + + email = sys.argv[1] + new_password = sys.argv[2] + reset_password(email, new_password) diff --git a/bloats/reset_vector_db.py b/bloats/reset_vector_db.py new file mode 100644 index 0000000..3c5e659 --- /dev/null +++ b/bloats/reset_vector_db.py @@ -0,0 +1,27 @@ +import chromadb +from app.core.config import settings + +def reset_vector_db(): + print(f"Connecting to ChromaDB at {settings.CHROMA_DB_DIR}...") + try: + client = chromadb.PersistentClient(path=settings.CHROMA_DB_DIR) + + # Try to get the collection to see if it exists + try: + client.get_collection(name="rag_documents") + print("Found existing collection 'rag_documents'. Deleting...") + client.delete_collection(name="rag_documents") + print("✅ Collection 'rag_documents' deleted.") + except Exception as e: + print(f"Collection 'rag_documents' not found or could not be accessed: {e}") + + print("Recreating collection 'rag_documents'...") + # We recreate it to ensure it's fresh and ready for new embeddings + client.create_collection(name="rag_documents") + print("✅ Collection 'rag_documents' created successfully.") + + except Exception as e: + print(f"❌ Error resetting vector DB: {e}") + +if __name__ == "__main__": + reset_vector_db() diff --git a/bloats/run_alter.py b/bloats/run_alter.py new file mode 100644 index 0000000..13534ee --- /dev/null +++ b/bloats/run_alter.py @@ -0,0 +1,13 @@ +import sys +import os +from sqlalchemy import text + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.db.session import engine + +with engine.begin() as conn: + try: + conn.execute(text("ALTER TABLE payment_transactions ADD COLUMN raw_webhook_payload JSON;")) + print("Successfully added column raw_webhook_payload to payment_transactions") + except Exception as e: + print(f"Error adding column: {e}") diff --git a/bloats/security_incident_report.md b/bloats/security_incident_report.md new file mode 100644 index 0000000..4f1b7e9 --- /dev/null +++ b/bloats/security_incident_report.md @@ -0,0 +1,48 @@ +# Security Incident Report +**Date:** January 31, 2026 +**Project:** TaimakoAI +**Severity:** Critical +**Status:** Resolved (Monitoring Required) + +## Executive Summary +On January 31, 2026, during routine maintenance, a **Critical Remote Code Execution (RCE)** vulnerability was detected in the frontend application. The vulnerability allowed unauthorized actors to execute system commands on the server. The attack vector was identified as a known security flaw in **Next.js 16.0.3**. Immediate remediation was performed by patching the software and restricting network configurations. + +## 1. Incident Details +- **Component:** `taimako_frontend` (Next.js Application) +- **Vulnerability Type:** Remote Code Execution (RCE) via Deserialization (CVE-2025-55182 / CVE-2025-66478) +- **Affected Version:** Next.js `16.0.3` +- **Detected:** January 31, 2026, 17:21 PM (local time) based on 502 Bad Gateway investigation. + +## 2. Root Cause Analysis +The application was running an outdated version of Next.js (`16.0.3`) which contained a critical vulnerability in the React Server Components (RSC) payload handling. +- **Mechanism:** Attackers sent maliciously crafted HTTP requests that the server deserialized, resulting in arbitrary shell command execution. +- **Exploitation:** Logs confirmed active exploitation where attackers ran commands to list directories and print environment variables. + +## 3. Detection & Evidence +The incident was discovered while investigating `502 Bad Gateway` errors. Review of the Docker logs (`docker logs taimako_frontend`) revealed: +- **Abnormal Error Dumps**: `NEXT_REDIRECT` errors containing output of system commands. +- **Command Execution**: + - `ls -la /var/www/.env*` (Attempting to locate secret files) + - `id`, `uname` (System reconnaissance) + - `base64` verification logic. +- **Environment Leak**: Error stack traces displayed the contents of environment variables, including configuration keys. + +## 4. Resolution & Mitigation +The following corrective actions were taken immediately: +1. **Software Patch**: Upgraded `next` dependency from `16.0.3` to `^16.0.7` (Current installed: `16.1.6`). +2. **Configuration Hardening**: + - Refactored Backend configuration to enforce **Strict CORS** policies in production. + - Centralized middleware management. +3. **Secret Rotation (Required User Action)**: + - Initiated rotation protocol for `POSTGRES_PASSWORD`, `JWT_SECRET`, and `GOOGLE_CLIENT_SECRET`. + +## 5. Impact Assessment +- **Data Confidentiality**: **High Risk**. Environment variables were exposed in logs. Secrets must be assumed compromised. +- **Data Integrity**: **Medium Risk**. Attackers had shell access, but no evidence of database deletion was found in the limited log window. +- **Availability**: **High Impact**. The attack caused the frontend service to crash repeatedly (502 errors). + +## 6. Recommendations & Next Steps +1. **Immediate**: Complete the rotation of all production secrets (Database, JWT, API Keys). +2. **Deployment**: Re-deploy all services with the patched Docker images. +3. **Monitoring**: Monitor logs for the next 48 hours for any "NEXT_REDIRECT" anomalies or suspicious IP activity. +4. **Process**: Implement a dependency scanning tool (e.g., Dependabot or Snyk) to catch upstream vulnerabilities earlier. diff --git a/bloats/simulate_webhook.py b/bloats/simulate_webhook.py new file mode 100644 index 0000000..d324a46 --- /dev/null +++ b/bloats/simulate_webhook.py @@ -0,0 +1,44 @@ +import os +import hmac +import hashlib +import json +import httpx +import sys + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.core.config import settings + +SECRET = settings.PAYSTACK_SECRET_KEY +if not SECRET: + SECRET = "sk_test_fake" + +payload = { + "event": "charge.success", + "data": { + "reference": "fake_ref_12345", + "amount": 5000, + "customer": { + "email": "test@venco.africa" + }, + "metadata": { + "is_upgrade": True, + "tier": "flux" + } + } +} + +body = json.dumps(payload).encode('utf-8') +signature = hmac.new( + key=SECRET.encode('utf-8'), + msg=body, + digestmod=hashlib.sha512 +).hexdigest() + +headers = { + "x-paystack-signature": signature, + "Content-Type": "application/json" +} + +resp = httpx.post("http://localhost:8000/webhooks/paystack", content=body, headers=headers) +print("Status:", resp.status_code) +print("Response:", resp.text) diff --git a/bloats/test.txt b/bloats/test.txt new file mode 100644 index 0000000..d670460 --- /dev/null +++ b/bloats/test.txt @@ -0,0 +1 @@ +test content diff --git a/bloats/test_output.txt b/bloats/test_output.txt new file mode 100644 index 0000000..56fc9f1 --- /dev/null +++ b/bloats/test_output.txt @@ -0,0 +1,52 @@ +============================= test session starts ============================== +platform darwin -- Python 3.13.2, pytest-9.0.1, pluggy-1.6.0 +rootdir: /Users/vencomacbook/Desktop/sten/TaimakoAI/backend +configfile: pyproject.toml +plugins: anyio-4.11.0, Faker-38.2.0 +collected 2 items + +tests/test_escalation_unit.py .. [100%] + +=============================== warnings summary =============================== +app/db/base.py:3 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/db/base.py:3: MovedIn20Warning: The ``declarative_base()`` function is now available as sqlalchemy.orm.declarative_base(). (deprecated since: 2.0) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) + Base = declarative_base() + +app/schemas/user.py:23 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/schemas/user.py:23: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class UserResponse(UserBase): + +.venv/lib/python3.13/site-packages/google/cloud/aiplatform/models.py:52 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/google/cloud/aiplatform/models.py:52: FutureWarning: Support for google-cloud-storage < 3.0.0 will be removed in a future version of google-cloud-aiplatform. Please upgrade to google-cloud-storage >= 3.0.0. + from google.cloud.aiplatform.utils import gcs_utils + +.venv/lib/python3.13/site-packages/litellm/types/llms/anthropic.py:531 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/litellm/types/llms/anthropic.py:531: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class AnthropicResponseContentBlockToolUse(BaseModel): + +.venv/lib/python3.13/site-packages/litellm/types/rag.py:181 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/litellm/types/rag.py:181: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class RAGIngestRequest(BaseModel): + +app/services/agent_system/tool_schemas.py:7 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:7: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class GetContextInput(BaseModel): + +app/services/agent_system/tool_schemas.py:24 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:24: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class SayHelloInput(BaseModel): + +app/services/agent_system/tool_schemas.py:47 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:47: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class ContextOutput(BaseModel): + +app/services/agent_system/tool_schemas.py:67 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:67: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class GreetingOutput(BaseModel): + +app/services/agent_system/tool_schemas.py:82 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:82: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class FarewellOutput(BaseModel): + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +======================== 2 passed, 10 warnings in 0.07s ======================== diff --git a/bloats/test_upload.txt b/bloats/test_upload.txt new file mode 100644 index 0000000..d670460 --- /dev/null +++ b/bloats/test_upload.txt @@ -0,0 +1 @@ +test content diff --git a/bloats/update_tiers.py b/bloats/update_tiers.py new file mode 100644 index 0000000..d16917e --- /dev/null +++ b/bloats/update_tiers.py @@ -0,0 +1,27 @@ + +from app.db.session import SessionLocal +from app.models.plan import Plan + +def main(): + db = SessionLocal() + tiers_map = { + "spark": 1, + "ignite": 2, + "blaze": 3, + "inferno": 4 + } + + plans = db.query(Plan).all() + for plan in plans: + name = plan.name.lower() + if name in tiers_map: + plan.tier = tiers_map[name] + else: + plan.tier = 0 + + db.commit() + db.close() + print("Tiers updated successfully.") + +if __name__ == "__main__": + main() diff --git a/bloats/verify_fix_startup.py b/bloats/verify_fix_startup.py new file mode 100644 index 0000000..000b7cd --- /dev/null +++ b/bloats/verify_fix_startup.py @@ -0,0 +1,23 @@ + +import sys +import os + +# Add the project root to the python path +sys.path.append(os.getcwd()) + +try: + print("Attempting to import app.main...") + print("Successfully imported app.main without AttributeError.") +except AttributeError as e: + print(f"Caught expected AttributeError: {e}") + sys.exit(1) +except Exception as e: + print(f"Caught unexpected exception: {e}") + # We only care about the specific AttributeError for now, other errors might be due to missing env vars etc. + # But if it's the specific error we fixed, it shouldn't happen. + if "type object 'User' has no attribute 'is_model'" in str(e): + print("Verification FAILED: The specific AttributeError still exists.") + sys.exit(1) + else: + print("Verification PASSED (ignoring unrelated errors).") + sys.exit(0) diff --git a/bloats/verify_subscription.py b/bloats/verify_subscription.py new file mode 100644 index 0000000..0f13313 --- /dev/null +++ b/bloats/verify_subscription.py @@ -0,0 +1,69 @@ +from app.models.business import Business +from app.models.user import User +from app.db.session import SessionLocal + +# Mock the database session +db = SessionLocal() + +def verify_subscription_defaults(): + print("Verifying Subscription Defaults...") + # Clean up previous test + existing = db.query(Business).filter(Business.business_name == "Subscription Test").first() + if existing: + db.delete(existing) + db.commit() + + # Create User for test if needed + user = db.query(User).filter(User.email == "test@sub.com").first() + if not user: + user = User(email="test@sub.com") + db.add(user) + db.commit() + db.refresh(user) + + # Simulate Logic from API (renewing a spark plan) + business = Business( + user_id=user.id, + business_name="Subscription Test", + subscription_tier="spark", + allocated_ai_responses=100, + used_ai_responses=0, + allocated_escalations=5, + used_escalations=0, + allocated_messages_per_session=20, + allocated_daily_sessions=50, + allocated_whitelisted_domains=1 + ) + db.add(business) + db.commit() + db.refresh(business) + + assert business.subscription_tier == "spark" + assert business.allocated_ai_responses == 100 + assert business.used_ai_responses == 0 + assert business.allocated_escalations == 5 + print("✅ Defaults Verified") + return business + +def verify_responses_used(business_id): + print("Verifying AI Responses Deduction logic...") + + b = db.query(Business).filter(Business.id == business_id).first() + initial_used = b.used_ai_responses + b.used_ai_responses += 1 # simulated chat message sent + db.commit() + + b_refreshed = db.query(Business).filter(Business.id == business_id).first() + assert b_refreshed.used_ai_responses == initial_used + 1 + assert (b_refreshed.allocated_ai_responses - b_refreshed.used_ai_responses) == 99 + print("✅ AI Responses Used DB Persistence Verified") + +try: + business = verify_subscription_defaults() + verify_responses_used(business.id) + print("ALL TESTS PASSED") +except Exception as e: + print(f"TEST FAILED: {e}") +finally: + db.close() + diff --git a/docker-compose.yml b/docker-compose.yml index 6334486..d9c1a7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,15 +33,16 @@ services: restart: "no" backend: - build: ./backend + build: + context: . + dockerfile: Dockerfile + target: backend container_name: taimako_backend + restart: unless-stopped ports: - "8000:8000" - volumes: - - ./backend:/app - - /app/.venv + env_file: .env environment: - - PYTHONUNBUFFERED=1 - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:${POSTGRES_PORT}/${POSTGRES_DB} depends_on: postgres: @@ -67,17 +68,25 @@ services: restart: unless-stopped frontend: - build: ./frontend + build: + context: . + dockerfile: Dockerfile + target: frontend + args: + NEXT_PUBLIC_ENVIRONMENT: ${NEXT_PUBLIC_ENVIRONMENT:-production} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-} + NEXT_PUBLIC_BACKEND_URL_PROD: ${NEXT_PUBLIC_BACKEND_URL_PROD:-} + NEXT_PUBLIC_FRONTEND_URL_PROD: ${NEXT_PUBLIC_FRONTEND_URL_PROD:-} + NEXT_PUBLIC_BACKEND_URL_STAGING: ${NEXT_PUBLIC_BACKEND_URL_STAGING:-} + NEXT_PUBLIC_FRONTEND_URL_STAGING: ${NEXT_PUBLIC_FRONTEND_URL_STAGING:-} + NEXT_PUBLIC_BACKEND_URL_DEV: ${NEXT_PUBLIC_BACKEND_URL_DEV:-} + NEXT_PUBLIC_FRONTEND_URL_DEV: ${NEXT_PUBLIC_FRONTEND_URL_DEV:-} container_name: taimako_frontend + restart: unless-stopped ports: - "3000:3000" - volumes: - - ./frontend:/app - - /app/node_modules - stdin_open: true - tty: true depends_on: - backend volumes: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/frontend/design_docs.md b/frontend/design_docs.md deleted file mode 100644 index 40e5b23..0000000 --- a/frontend/design_docs.md +++ /dev/null @@ -1,132 +0,0 @@ -The following Design PRD is based on an analysis of Attio’s current live product, public design documentation, and the "Linear-style" SaaS aesthetic they pioneered. This document serves as a blueprint for developers and designers to replicate the Attio look and feel. - -*** - -# Product Requirement Document: Design System & UI/UX -**Project:** Replicating Attio Design Aesthetic -**Target Output:** High-fidelity, data-dense SaaS Interface -**Core Philosophy:** "Power through Progressive Disclosure" - -## 1. Design Philosophy -Attio's design is defined by **structural minimalism**. It avoids unnecessary decoration, relying on strict hierarchy, subtle borders, and precise spacing to organize high-density data. -* **Data-First:** Content is never hidden behind "pretty" wrappers. Tables and lists are the heroes. -* **Subtle Depth:** The interface is mostly flat but uses multiple layers of "elevation" (z-index) separated by delicate borders and ultra-soft shadows, not heavy drop shadows. -* **Squircle Geometry:** A softening of strict geometry. Icons and containers often sit somewhere between a square and a circle. - ---- - -## 2. Design Tokens (The DNA) - -### 2.1 Typography -**Font Family:** `Inter` (Sans-serif) -* **Source:** Google Fonts / rsms.me -* **Rendering:** Optimize for legibility. Use `-webkit-font-smoothing: antialiased`. - -**Scale & Usage:** -* **Body (Base):** 13px or 14px (Attio leans smaller than the standard 16px to fit more data). - * *Weight:* Regular (400) for text, Medium (500) for high-visibility data. - * *Color:* `Neutral-900` (Light Mode), `Neutral-100` (Dark Mode). -* **Headings:** - * `H1`: 24px, Medium (500), Tracking -0.02em. - * `H2`: 20px, Medium (500), Tracking -0.01em. - * `H3` (Section Headers): 11px or 12px, Uppercase, Bold (700), Tracking +0.04em, Color: `Neutral-500`. -* **Monospace:** `JetBrains Mono` or `Fira Code` (used sparingly for API keys/IDs). - -### 2.2 Color Palette (Replication Values) -Attio uses a "semantic" color system. Do not use raw hex codes in components; use tokens. - -**Neutrals (The Skeleton):** -* **Background (Light):** `#FFFFFF` (Page), `#F5F7F9` (Sidebar/Secondary). -* **Background (Dark):** `#1D1E20` (Page), `#161618` (Sidebar - *Note: This is a warm, charcoal dark, not pure black*). -* **Borders:** - * Light Mode: `#E2E4E7` (Subtle), `#D0D5DD` (Strong). - * Dark Mode: `rgba(255, 255, 255, 0.08)` (Subtle). - -**Brand Colors:** -* **Primary Blue:** `#2E5BFF` (approx) – Used for primary buttons, active states, and links. -* **Accent Purple:** `#6E56CF` – Used for "Magical" or AI features. - -**Functional / Status Colors:** -* **Success:** `#22C55E` (Green-500) -* **Warning:** `#F59E0B` (Amber-500) -* **Error:** `#EF4444` (Red-500) -* *Implementation Note:* Status tags always use a transparent background (e.g., `bg-green-500/10`) with the solid color as text. - -### 2.3 Spacing & Grid -* **Base Unit:** 4px. All spacing, sizing, and line-heights must be multiples of 4 (e.g., 4, 8, 12, 16, 20, 24). -* **Density:** High. - * *Padding inside buttons:* 6px 12px (Small), 8px 16px (Medium). - * *Table row height:* 32px (Compact), 40px (Standard). - -### 2.4 Shapes & Radius -* **Global Radius:** 6px or 8px (Small components like inputs/buttons). -* **Container Radius:** 12px (Modals, Cards). -* **Icon Radius:** "Squircle" (Superellipse). In CSS, approximate this with `border-radius: 22%` for a smooth look, or use an SVG mask. Attio explicitly mentions a **30% corner radius** for app icons. - ---- - -## 3. Core UI Components - -### 3.1 Buttons -* **Primary:** - * Background: Brand Blue (`#2E5BFF`). - * Text: White. - * Shadow: `0 1px 2px rgba(0,0,0,0.1)`. - * Hover: Lighten by 5%. -* **Secondary (Standard):** - * Background: White (Light Mode), Charcoal (Dark Mode). - * Border: 1px solid `Neutral-200`. - * Text: `Neutral-700`. - * Shadow: `0 1px 2px rgba(0,0,0,0.05)`. -* **Ghost / Tertiary:** - * Background: Transparent. - * Hover: `Neutral-100` (Light), `White/5` (Dark). - -### 3.2 Inputs & Forms -* **Style:** Minimalist. No heavy backgrounds. -* **Default State:** White background, 1px Border `Neutral-200`, Radius 6px. -* **Focus State:** Border color changes to Brand Blue. **Important:** Add a "Focus Ring" shadow: `box-shadow: 0 0 0 2px rgba(46, 91, 255, 0.2)`. -* **Typography:** 14px text. Placeholder text should be `Neutral-400`. - -### 3.3 The "Attio Card" -Most content lives in cards. -* **Background:** White. -* **Border:** 1px solid `Neutral-200` (Light) or `White/10` (Dark). -* **Shadow:** `0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03)`. -* **Header:** Often has a subtle bottom border (`1px solid Neutral-100`) separating title from content. - -### 3.4 Navigation (Sidebar) -* **Width:** Fixed (e.g., 240px) but collapsible. -* **Item State:** - * *Inactive:* Text `Neutral-500`, Icon `Neutral-400`. - * *Hover:* Background `Neutral-100`, Text `Neutral-900`. - * *Active:* Background `Neutral-200` (or subtle Brand Tint), Text `Brand Primary`. -* **Section Headers:** Tiny, uppercase text (see Typography). - ---- - -## 4. Key Visual Patterns to Implement - -### 4.1 "Inset" Icons -Attio icons often look like they are pressed *into* the page or floating just above. -* **Technique:** Use a 1px inner border on icon containers. -* **CSS:** `box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08);` - -### 4.2 The "Command K" Palette -A central search/action bar is mandatory for this style. -* **Visuals:** Centered modal, heavy backdrop blur (`backdrop-filter: blur(4px)`), large shadow. -* **Interaction:** Instant appearance on `Cmd+K`. - -### 4.3 Progressive Disclosure Tables -Tables should look simple at first glance. -* **Hidden Actions:** "Edit", "Delete", or "Open" buttons should only appear on **hover** of the table row. -* **Truncation:** Long text should fade out or truncate with an ellipsis `...`. - -### 4.4 Loaders & States -* **Skeleton Loading:** Never use spinning wheels for initial load. Use pulsing grey blocks (`bg-neutral-200`) that match the shape of the content (avatar circle, text line). -* **"AI Thinking":** Use a subtle gradient shimmer animation for any AI-generated text or fields. - -## 5. Assets & Implementation Guide -1. **Icon Set:** Use **Heroicons** (closest open-source match) or **Phosphor Icons** (very popular in this aesthetic). Set stroke width to `1.5px` or `2px` (Medium). -2. **CSS Framework:** Tailwind CSS is highly recommended as its default utility classes (`border-gray-200`, `text-sm`, `tracking-tight`) align perfectly with this PRD. -3. **Dark Mode:** It is not optional. Build with CSS variables (`var(--bg-primary)`) from day one to support toggling. \ No newline at end of file diff --git a/frontend/example_site.html b/frontend/example_site.html deleted file mode 100644 index 095d843..0000000 --- a/frontend/example_site.html +++ /dev/null @@ -1,632 +0,0 @@ - - - - - - Nimbus — Smart SaaS Platform - - - - - - - - - - -
- - - -
-
- Trusted by teams — 202k+ users -

Smart insights and automations that scale your team

-

Nimbus connects all your data sources, surfaces actionable insights, and automates repeated work — so teams focus on outcomes, not plumbing.

- -
- - Watch intro -
- -
-
Avg time saved
12 hrs/wk
-
Integrations
45+
-
SLA uptime
99.95%
-
- -
No credit card required • Cancel anytime
-
- - -
- - -
-
-

Unified Data Layer

-

Connect databases, warehouses, and apps in minutes — schema mapping and transformations included.

-
- -
-

Automations

-

Trigger flows from events, schedule jobs, or use pre-built templates to automate repetitive work.

-
- -
-

Collaborative Notebooks

-

Share dashboards, write notes, and assign tasks directly to teammates in context.

-
-
- - -
-

Plans that scale with you

-
-
-
-
-
Starter
-
For small teams
-
-
$0 / mo
-
-
    -
  • Up to 3 users
  • -
  • Basic integrations
  • -
  • Email support
  • -
-
- -
-
- -
-
-
-
Pro
-
Growing teams
-
-
$29 / mo
-
-
    -
  • Unlimited users
  • -
  • All integrations
  • -
  • Priority support
  • -
-
- -
-
- -
-
-
-
Enterprise
-
Advanced security
-
-
Custom
-
-
    -
  • SAML & SSO
  • -
  • Dedicated support
  • -
  • SLAs & compliance
  • -
-
- -
-
-
-
- - -
-
-
"Nimbus saved our analytics team 30% of their time."
-
— Maya Chen, Head of Analytics, Tranquil
-
- -
-
"Building automations was delightfully fast."
-
— David Ruiz, CTO, Verto
-
-
- - -
-
-
Instant demo available
-
Click "Create account" to see a live demo of the dashboard and automations.
-
- - -
- -
-
© Nimbus — Built with care
-
Privacy · Terms · Status
-
-
- - - - - - - - diff --git a/frontend/lint_results.txt b/frontend/lint_results.txt deleted file mode 100644 index 151ca4d..0000000 --- a/frontend/lint_results.txt +++ /dev/null @@ -1,103 +0,0 @@ - -> frontend@0.1.0 lint -> eslint - - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/auth/login/page.tsx - 49:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 128:20 error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/auth/signup/page.tsx - 56:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/analytics/page.tsx - 5:10 warning 'AreaChart' is defined but never used @typescript-eslint/no-unused-vars - 5:21 warning 'BarChart' is defined but never used @typescript-eslint/no-unused-vars - 5:31 warning 'PieChart' is defined but never used @typescript-eslint/no-unused-vars - 5:41 warning 'Activity' is defined but never used @typescript-eslint/no-unused-vars - 5:51 warning 'Users' is defined but never used @typescript-eslint/no-unused-vars - 5:58 warning 'Globe' is defined but never used @typescript-eslint/no-unused-vars - 5:65 warning 'Target' is defined but never used @typescript-eslint/no-unused-vars - 11:7 warning 'FunnelChart' is assigned a value but never used @typescript-eslint/no-unused-vars - 11:24 warning 'data' is defined but never used @typescript-eslint/no-unused-vars - 11:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 29:46 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 110:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/business/page.tsx - 53:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 87:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/documents/page.tsx - 72:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 86:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 104:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/handoff/page.tsx - 4:10 warning 'motion' is defined but never used @typescript-eslint/no-unused-vars - 5:17 warning 'Bell' is defined but never used @typescript-eslint/no-unused-vars - 5:23 warning 'Mail' is defined but never used @typescript-eslint/no-unused-vars - 5:49 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars - 5:78 warning 'ShieldAlert' is defined but never used @typescript-eslint/no-unused-vars - 152:68 error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/layout.tsx - 4:10 warning 'useRouter' is defined but never used @typescript-eslint/no-unused-vars - 4:21 warning 'usePathname' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/page.tsx - 12:23 warning 'title' is defined but never used @typescript-eslint/no-unused-vars - 12:141 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 92:46 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 124:71 error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities - 124:78 error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities - 187:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 213:33 warning 'i' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/sessions/[id]/page.tsx - 5:10 warning 'MessageSquare' is defined but never used @typescript-eslint/no-unused-vars - 5:66 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars - 22:20 warning 'setMessages' is assigned a value but never used @typescript-eslint/no-unused-vars - 123:20 error 'MapPin' is not defined react/jsx-no-undef - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/sessions/page.tsx - 5:68 warning 'User' is defined but never used @typescript-eslint/no-unused-vars - 5:88 warning 'AlertCircle' is defined but never used @typescript-eslint/no-unused-vars - 6:8 warning 'Card' is defined but never used @typescript-eslint/no-unused-vars - 8:8 warning 'Input' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-interactions/page.tsx - 7:10 warning 'User' is defined but never used @typescript-eslint/no-unused-vars - 419:37 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 420:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 421:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 422:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-settings/page.tsx - 30:10 warning 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars - 113:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 128:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 152:79 error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities - 359:21 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 359:21 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/widget/[public_widget_id]/page.tsx - 9:3 warning 'RotateCcw' is defined but never used @typescript-eslint/no-unused-vars - 236:20 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 330:11 warning 'businessName' is assigned a value but never used @typescript-eslint/no-unused-vars - 441:35 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 441:35 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - 623:31 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 625:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 626:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 627:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/components/dashboard/DocumentList.tsx - 5:41 warning 'Check' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/lib/api.ts - 98:18 warning '_' is defined but never used @typescript-eslint/no-unused-vars - 218:67 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - -✖ 66 problems (17 errors, 49 warnings) - diff --git a/frontend/lint_results_2.txt b/frontend/lint_results_2.txt deleted file mode 100644 index 662b868..0000000 --- a/frontend/lint_results_2.txt +++ /dev/null @@ -1,85 +0,0 @@ - -> frontend@0.1.0 lint -> eslint - - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/auth/login/page.tsx - 50:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/auth/signup/page.tsx - 57:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/analytics/page.tsx - 5:10 warning 'Activity' is defined but never used @typescript-eslint/no-unused-vars - 5:20 warning 'Users' is defined but never used @typescript-eslint/no-unused-vars - 5:27 warning 'Globe' is defined but never used @typescript-eslint/no-unused-vars - 5:34 warning 'Target' is defined but never used @typescript-eslint/no-unused-vars - 11:7 warning 'FunnelChart' is assigned a value but never used @typescript-eslint/no-unused-vars - 11:24 warning 'data' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/business/page.tsx - 53:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 87:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/documents/page.tsx - 72:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 86:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 104:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/handoff/page.tsx - 4:10 warning 'motion' is defined but never used @typescript-eslint/no-unused-vars - 5:17 warning 'Bell' is defined but never used @typescript-eslint/no-unused-vars - 5:23 warning 'Mail' is defined but never used @typescript-eslint/no-unused-vars - 5:49 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars - 5:78 warning 'ShieldAlert' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/layout.tsx - 4:10 warning 'useRouter' is defined but never used @typescript-eslint/no-unused-vars - 4:21 warning 'usePathname' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/page.tsx - 12:23 warning 'title' is defined but never used @typescript-eslint/no-unused-vars - 92:46 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 213:33 warning 'i' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/sessions/[id]/page.tsx - 5:10 warning 'MessageSquare' is defined but never used @typescript-eslint/no-unused-vars - 5:66 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars - 22:20 warning 'setMessages' is assigned a value but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/sessions/page.tsx - 5:68 warning 'User' is defined but never used @typescript-eslint/no-unused-vars - 5:88 warning 'AlertCircle' is defined but never used @typescript-eslint/no-unused-vars - 6:8 warning 'Card' is defined but never used @typescript-eslint/no-unused-vars - 8:8 warning 'Input' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-interactions/page.tsx - 7:10 warning 'User' is defined but never used @typescript-eslint/no-unused-vars - 419:37 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 420:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 421:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 422:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-settings/page.tsx - 30:10 warning 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars - 113:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 128:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 359:21 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 359:21 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/widget/[public_widget_id]/page.tsx - 9:3 warning 'RotateCcw' is defined but never used @typescript-eslint/no-unused-vars - 236:20 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 330:11 warning 'businessName' is assigned a value but never used @typescript-eslint/no-unused-vars - 441:35 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 441:35 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - 623:31 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 625:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 626:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 627:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/components/dashboard/DocumentList.tsx - 5:41 warning 'Check' is defined but never used @typescript-eslint/no-unused-vars - -✖ 50 problems (5 errors, 45 warnings) - diff --git a/frontend/lint_results_multipage.txt b/frontend/lint_results_multipage.txt deleted file mode 100644 index be4ac95..0000000 --- a/frontend/lint_results_multipage.txt +++ /dev/null @@ -1,71 +0,0 @@ - -> frontend@0.1.0 lint -> eslint - - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/analytics/page.tsx - 5:10 warning 'Activity' is defined but never used @typescript-eslint/no-unused-vars - 5:20 warning 'Users' is defined but never used @typescript-eslint/no-unused-vars - 5:27 warning 'Globe' is defined but never used @typescript-eslint/no-unused-vars - 5:34 warning 'Target' is defined but never used @typescript-eslint/no-unused-vars - 11:7 warning 'FunnelChart' is assigned a value but never used @typescript-eslint/no-unused-vars - 11:24 warning 'data' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/documents/page.tsx - 72:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 86:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 104:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/handoff/page.tsx - 4:10 warning 'motion' is defined but never used @typescript-eslint/no-unused-vars - 5:17 warning 'Bell' is defined but never used @typescript-eslint/no-unused-vars - 5:23 warning 'Mail' is defined but never used @typescript-eslint/no-unused-vars - 5:49 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars - 5:78 warning 'ShieldAlert' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/layout.tsx - 4:10 warning 'useRouter' is defined but never used @typescript-eslint/no-unused-vars - 4:21 warning 'usePathname' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/page.tsx - 12:23 warning 'title' is defined but never used @typescript-eslint/no-unused-vars - 213:33 warning 'i' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/sessions/[guestId]/[sessionId]/page.tsx - 6:48 warning 'Send' is defined but never used @typescript-eslint/no-unused-vars - 19:9 warning 'guestId' is assigned a value but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/sessions/page.tsx - 8:8 warning 'Card' is defined but never used @typescript-eslint/no-unused-vars - 11:10 warning 'cn' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-interactions/page.tsx - 7:10 warning 'User' is defined but never used @typescript-eslint/no-unused-vars - 419:37 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 420:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 421:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 422:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-settings/page.tsx - 30:10 warning 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars - 113:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 128:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 359:21 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 359:21 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/widget/[public_widget_id]/page.tsx - 9:3 warning 'RotateCcw' is defined but never used @typescript-eslint/no-unused-vars - 236:20 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 330:11 warning 'businessName' is assigned a value but never used @typescript-eslint/no-unused-vars - 441:35 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 441:35 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - 623:31 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 625:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 626:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 627:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/components/dashboard/DocumentList.tsx - 5:41 warning 'Check' is defined but never used @typescript-eslint/no-unused-vars - -✖ 42 problems (0 errors, 42 warnings) - diff --git a/frontend/lint_results_refactor.txt b/frontend/lint_results_refactor.txt deleted file mode 100644 index 3653608..0000000 --- a/frontend/lint_results_refactor.txt +++ /dev/null @@ -1,74 +0,0 @@ - -> frontend@0.1.0 lint -> eslint - - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/analytics/page.tsx - 5:10 warning 'Activity' is defined but never used @typescript-eslint/no-unused-vars - 5:20 warning 'Users' is defined but never used @typescript-eslint/no-unused-vars - 5:27 warning 'Globe' is defined but never used @typescript-eslint/no-unused-vars - 5:34 warning 'Target' is defined but never used @typescript-eslint/no-unused-vars - 11:7 warning 'FunnelChart' is assigned a value but never used @typescript-eslint/no-unused-vars - 11:24 warning 'data' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/documents/page.tsx - 72:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 86:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - 104:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/handoff/page.tsx - 4:10 warning 'motion' is defined but never used @typescript-eslint/no-unused-vars - 5:17 warning 'Bell' is defined but never used @typescript-eslint/no-unused-vars - 5:23 warning 'Mail' is defined but never used @typescript-eslint/no-unused-vars - 5:49 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars - 5:78 warning 'ShieldAlert' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/layout.tsx - 4:10 warning 'useRouter' is defined but never used @typescript-eslint/no-unused-vars - 4:21 warning 'usePathname' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/page.tsx - 12:23 warning 'title' is defined but never used @typescript-eslint/no-unused-vars - 213:33 warning 'i' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/sessions/page.tsx - 4:18 warning 'AnimatePresence' is defined but never used @typescript-eslint/no-unused-vars - 6:32 warning 'ChevronRight' is defined but never used @typescript-eslint/no-unused-vars - 7:36 warning 'Send' is defined but never used @typescript-eslint/no-unused-vars - 8:3 warning 'MoreVertical' is defined but never used @typescript-eslint/no-unused-vars - 8:17 warning 'Mail' is defined but never used @typescript-eslint/no-unused-vars - 8:23 warning 'Phone' is defined but never used @typescript-eslint/no-unused-vars - 10:8 warning 'Card' is defined but never used @typescript-eslint/no-unused-vars - 18:6 warning 'ViewState' is defined but never used @typescript-eslint/no-unused-vars - 203:39 warning 'i' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-interactions/page.tsx - 7:10 warning 'User' is defined but never used @typescript-eslint/no-unused-vars - 419:37 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 420:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 421:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 422:38 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/dashboard/widget-settings/page.tsx - 30:10 warning 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars - 113:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 128:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 359:21 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 359:21 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/app/widget/[public_widget_id]/page.tsx - 9:3 warning 'RotateCcw' is defined but never used @typescript-eslint/no-unused-vars - 236:20 warning 'e' is defined but never used @typescript-eslint/no-unused-vars - 330:11 warning 'businessName' is assigned a value but never used @typescript-eslint/no-unused-vars - 441:35 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element - 441:35 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text - 623:31 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 625:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 626:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - 627:32 warning 'node' is defined but never used @typescript-eslint/no-unused-vars - -/Users/vencomacbook/Desktop/sten/Agentic_CX/frontend/src/components/dashboard/DocumentList.tsx - 5:41 warning 'Check' is defined but never used @typescript-eslint/no-unused-vars - -✖ 47 problems (0 errors, 47 warnings) - diff --git a/frontend/src/config.ts b/frontend/src/config.ts index ffc1802..9b318cd 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -8,7 +8,7 @@ const getBackendUrl = (): string => { case 'production': return process.env.NEXT_PUBLIC_BACKEND_URL_PROD || 'https://api.taimako.dubem.xyz'; case 'staging': - return process.env.NEXT_PUBLIC_BACKEND_URL_STAGING || 'https://api.staging.taimako.ai'; + return process.env.NEXT_PUBLIC_BACKEND_URL_STAGING || 'https://taimako.onrender.com'; case 'dev': return process.env.NEXT_PUBLIC_BACKEND_URL_DEV || 'https://api.dev.taimako.ai'; case 'local': @@ -22,7 +22,7 @@ const getFrontendUrl = (): string => { case 'production': return process.env.NEXT_PUBLIC_FRONTEND_URL_PROD || 'https://taimako.dubem.xyz'; case 'staging': - return process.env.NEXT_PUBLIC_FRONTEND_URL_STAGING || 'https://app.staging.taimako.ai'; + return process.env.NEXT_PUBLIC_FRONTEND_URL_STAGING || 'https://taimakoai.onrender.com'; case 'dev': return process.env.NEXT_PUBLIC_FRONTEND_URL_DEV || 'https://app.dev.taimako.ai'; case 'local': diff --git a/frontend/tests/unit/config.test.ts b/frontend/tests/unit/config.test.ts index c4b666b..eb2e995 100644 --- a/frontend/tests/unit/config.test.ts +++ b/frontend/tests/unit/config.test.ts @@ -35,8 +35,8 @@ describe('config', () => { process.env.NEXT_PUBLIC_ENVIRONMENT = 'staging'; const config = await import('@/config'); - expect(config.BACKEND_URL).toBe('https://api.staging.taimako.ai'); - expect(config.FRONTEND_URL).toBe('https://app.staging.taimako.ai'); + expect(config.BACKEND_URL).toBe('https://taimako.onrender.com'); + expect(config.FRONTEND_URL).toBe('https://taimakoai.onrender.com'); }); it('returns production URLs for production environment', async () => {