Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c2c5ef0
feat: add feedback widget and dashboard stats skeleton
DevanshuNEU Jan 29, 2026
4c92600
feat(landing): add waitlist modal for Pro plan signup
DevanshuNEU Jan 29, 2026
d095c1c
feat(landing): update hero to showcase GitHub import
DevanshuNEU Jan 29, 2026
27a2857
chore(landing): clean up footer, remove dead links
DevanshuNEU Jan 29, 2026
5329317
chore: rebrand CodeIntel → OpenCodeIntel across entire frontend
DevanshuNEU Jan 29, 2026
8762a52
fix(landing): fix search bar text color in light mode
DevanshuNEU Jan 29, 2026
9158bec
fix(docker): add Discord webhook env var to build args
DevanshuNEU Jan 29, 2026
025b757
feat(landing): Enterprise 'Contact Sales' now uses Discord modal
DevanshuNEU Jan 29, 2026
7dd0405
fix(landing): make search bar theme-aware
DevanshuNEU Jan 29, 2026
4a8d27e
fix(landing): disable search during auto-typing animation
DevanshuNEU Jan 29, 2026
f74561f
fix(landing): prevent duplicate searches via Enter key
DevanshuNEU Jan 29, 2026
e57acb0
fix(landing): add missing useState import in WaitlistModal
DevanshuNEU Jan 29, 2026
e66b611
fix(docs): correct GitHub org name OpenOpenCodeIntel to OpenCodeIntel
DevanshuNEU Jan 29, 2026
5ecf234
fix(backend): add rate_limit decorator to rate_limiter.py
DevanshuNEU Jan 29, 2026
7599dbd
fix(frontend): remove duplicate useState import in WaitlistModal
DevanshuNEU Jan 29, 2026
12f7d20
fix(frontend): add error state handling to FeedbackWidget
DevanshuNEU Jan 29, 2026
b993840
fix(frontend): remove duplicate useState import in FeedbackWidget
DevanshuNEU Jan 29, 2026
69b9c5f
fix(backend): add periodic cleanup to rate_limit to prevent memory gr…
DevanshuNEU Jan 29, 2026
90a9c55
fix(backend): require Request for rate limiting, enable proxy headers
DevanshuNEU Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,11 @@ SENTRY_DSN=

# Environment identifier
ENVIRONMENT=development

# -----------------------------------------------------------------------------
# Discord Webhooks - Feedback & Waitlist
# -----------------------------------------------------------------------------

# Discord webhook for receiving user feedback and waitlist signups
# Create at: Discord Server → Channel Settings → Integrations → Webhooks
DISCORD_FEEDBACK_WEBHOOK=https://discord.com/api/webhooks/your_webhook_id/your_webhook_token
5 changes: 3 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()" || exit 1

# Run with uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Run with uvicorn (proxy-headers enabled for rate limiting behind reverse proxy)
# Set FORWARDED_ALLOW_IPS env var in production to your proxy's IP range
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
Comment thread
DevanshuNEU marked this conversation as resolved.
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from routes.users import router as users_router
from routes.search_v2 import router as search_v2_router
from routes.github import router as github_router
from routes.feedback import router as feedback_router
from routes.ws_playground import websocket_playground_index
from routes.ws_repos import websocket_repo_indexing

Expand Down Expand Up @@ -94,6 +95,7 @@ async def dispatch(self, request: Request, call_next):
app.include_router(users_router, prefix=API_PREFIX)
app.include_router(search_v2_router, prefix=API_PREFIX)
app.include_router(github_router, prefix=API_PREFIX)
app.include_router(feedback_router, prefix=API_PREFIX)

# WebSocket endpoints (versioned)
app.add_api_websocket_route(f"{API_PREFIX}/ws/index/{{repo_id}}", websocket_index)
Expand Down
121 changes: 121 additions & 0 deletions backend/routes/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
Feedback routes - handles user feedback and waitlist signups
Posts to Discord webhook server-side to keep webhook URL secret
"""
import os
import httpx
from datetime import datetime
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, EmailStr
from typing import Optional
from services.rate_limiter import rate_limit

router = APIRouter(prefix="/feedback", tags=["feedback"])

DISCORD_WEBHOOK_URL = os.getenv("DISCORD_FEEDBACK_WEBHOOK")

MOOD_CONFIG = {
"frustrated": {"emoji": "😠", "color": 0xEF4444, "label": "Frustrated"},
"meh": {"emoji": "😐", "color": 0xEAB308, "label": "Meh"},
"good": {"emoji": "😊", "color": 0x22C55E, "label": "Good"},
"love": {"emoji": "🤩", "color": 0x8B5CF6, "label": "Love it!"},
}


class FeedbackRequest(BaseModel):
mood: str
message: Optional[str] = None
email: Optional[EmailStr] = None


class WaitlistRequest(BaseModel):
email: EmailStr
plan: str # "pro" or "enterprise"


async def post_to_discord(embed: dict) -> bool:
"""Post an embed to Discord webhook."""
if not DISCORD_WEBHOOK_URL:
return False

try:
async with httpx.AsyncClient() as client:
response = await client.post(
DISCORD_WEBHOOK_URL,
json={"embeds": [embed]},
timeout=10.0
)
return response.status_code == 204
except Exception:
return False


@router.post("")
@rate_limit(requests_per_minute=5)
async def submit_feedback(request: Request, body: FeedbackRequest):
"""Submit user feedback - posts to Discord."""
if not DISCORD_WEBHOOK_URL:
raise HTTPException(status_code=503, detail="Feedback service unavailable")

mood_info = MOOD_CONFIG.get(body.mood, MOOD_CONFIG["good"])

embed = {
"title": "💬 New Feedback",
"color": mood_info["color"],
"fields": [
{"name": "Mood", "value": f"{mood_info['emoji']} {mood_info['label']}", "inline": True},
],
"footer": {"text": "OpenCodeIntel Feedback"},
"timestamp": datetime.utcnow().isoformat(),
}

if body.email:
embed["fields"].append({"name": "User", "value": body.email, "inline": True})
Comment thread
DevanshuNEU marked this conversation as resolved.

if body.message:
embed["fields"].append({"name": "Message", "value": body.message[:1000], "inline": False})

success = await post_to_discord(embed)
if not success:
raise HTTPException(status_code=500, detail="Failed to submit feedback")

return {"success": True}


@router.post("/waitlist")
@rate_limit(requests_per_minute=3)
async def join_waitlist(request: Request, body: WaitlistRequest):
"""Join waitlist for Pro or Enterprise plan."""
if not DISCORD_WEBHOOK_URL:
raise HTTPException(status_code=503, detail="Waitlist service unavailable")

is_enterprise = body.plan.lower() == "enterprise"

if is_enterprise:
embed = {
"title": "🏢 Enterprise Inquiry",
"color": 0x8B5CF6,
"fields": [
{"name": "Email", "value": body.email, "inline": True},
{"name": "Plan", "value": "Enterprise (Custom)", "inline": True},
],
"footer": {"text": "OpenCodeIntel Enterprise"},
"timestamp": datetime.utcnow().isoformat(),
}
else:
embed = {
"title": "🚀 New Waitlist Signup",
"color": 0x3B82F6,
"fields": [
{"name": "Email", "value": body.email, "inline": True},
{"name": "Plan Interest", "value": "Pro ($19/month)", "inline": True},
],
"footer": {"text": "OpenCodeIntel Waitlist"},
"timestamp": datetime.utcnow().isoformat(),
}

success = await post_to_discord(embed)
if not success:
raise HTTPException(status_code=500, detail="Failed to join waitlist")

return {"success": True}
75 changes: 74 additions & 1 deletion backend/services/rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,84 @@
Prevents abuse and manages request quotas
"""
import time
from typing import Optional, Dict
from typing import Optional, Dict, Callable
from datetime import datetime, timedelta
from functools import wraps
import hashlib
import secrets
from dataclasses import dataclass
from fastapi import HTTPException, Request


# In-memory rate limit storage (per-process, resets on restart)
_rate_limit_store: Dict[str, list] = {}
_last_cleanup: float = 0.0
_CLEANUP_INTERVAL_SEC = 60


def rate_limit(requests_per_minute: int = 60):
"""
Simple rate limit decorator for FastAPI routes.
Uses in-memory storage - suitable for single-instance deployments.
For production, use Redis-backed RateLimiter class instead.

IMPORTANT: Routes using this decorator MUST include `request: Request` as
a parameter. For correct client IP detection behind a reverse proxy,
configure Uvicorn with:
--proxy-headers --forwarded-allow-ips="<your-proxy-ips>"
This ensures request.client.host reflects the real client IP from
X-Forwarded-For header, not the proxy's IP.
"""
def decorator(func: Callable):
@wraps(func)
async def wrapper(*args, **kwargs):
# Get Request from kwargs (FastAPI injects it by parameter name)
request = kwargs.get('request')
if not request:
for arg in args:
if isinstance(arg, Request):
request = arg
break

if not request or not isinstance(request, Request):
raise HTTPException(
status_code=500,
detail="Rate limiting requires Request parameter in route"
)

# Get client IP (relies on Uvicorn proxy-headers config for real IP)
client_id = request.client.host if request.client else "unknown"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
key = f"{func.__name__}:{client_id}"
now = time.time()
window_start = now - 60

# Periodic cleanup of stale keys to prevent unbounded memory growth
global _last_cleanup
if now - _last_cleanup > _CLEANUP_INTERVAL_SEC:
for k in list(_rate_limit_store.keys()):
timestamps = _rate_limit_store.get(k, [])
if not timestamps or timestamps[-1] <= window_start:
_rate_limit_store.pop(k, None)
_last_cleanup = now

# Clean old entries and get current count
if key not in _rate_limit_store:
_rate_limit_store[key] = []

_rate_limit_store[key] = [t for t in _rate_limit_store[key] if t > window_start]

if len(_rate_limit_store[key]) >= requests_per_minute:
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded. Max {requests_per_minute} requests per minute."
)

_rate_limit_store[key].append(now)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return await func(*args, **kwargs)

return wrapper
return decorator


@dataclass
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ services:
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
- API_KEY=${API_KEY}
- BACKEND_API_URL=http://backend:8000
- DISCORD_FEEDBACK_WEBHOOK=${DISCORD_FEEDBACK_WEBHOOK}
- FORWARDED_ALLOW_IPS=*
volumes:
- ./backend/repos:/app/repos
- ./backend:/app
Expand Down
Loading