feat: add production deployment config

- Add docker-compose.prod.yml with env var support
- Add frontend/Dockerfile.prod with nginx for static serving
- Fix Zulip notification to run in thread pool (avoid blocking)
- Use Zulip time format for timezone-aware display
- Add Zulip @mentions for users matched by email

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Joyce
2026-01-21 14:29:52 -05:00
parent 26311c867a
commit 7fefd634f5
4 changed files with 129 additions and 13 deletions

View File

@@ -1,3 +1,4 @@
import asyncio
import logging
from uuid import UUID
@@ -146,7 +147,6 @@ async def sync_participant(participant_id: UUID, db: AsyncSession = Depends(get_
async def schedule_meeting(
data: ScheduleRequest, db: AsyncSession = Depends(get_db)
):
# 1. Validate Lead Time (2 hours)
min_start_time = datetime.now(timezone.utc) + timedelta(hours=2)
if data.start_time.replace(tzinfo=timezone.utc) < min_start_time:
raise HTTPException(
@@ -154,7 +154,6 @@ async def schedule_meeting(
detail="Meetings must be scheduled at least 2 hours in advance."
)
# 2. Fetch Participants
result = await db.execute(
select(Participant).where(Participant.id.in_(data.participant_ids))
)
@@ -166,9 +165,7 @@ async def schedule_meeting(
participant_dicts = [
{"name": p.name, "email": p.email} for p in participants
]
participant_names = [p.name for p in participants]
# 3. Send Notifications
email_success = await send_meeting_invite(
participant_dicts,
data.title,
@@ -177,10 +174,11 @@ async def schedule_meeting(
data.end_time
)
zulip_success = send_zulip_notification(
zulip_success = await asyncio.to_thread(
send_zulip_notification,
data.title,
data.start_time,
participant_names
participant_dicts
)
return {

View File

@@ -5,13 +5,44 @@ from datetime import datetime
logger = logging.getLogger(__name__)
def get_zulip_usernames_by_email(client: zulip.Client) -> dict[str, str]:
"""Fetch all Zulip users and return a mapping of email -> full_name for mentions."""
try:
result = client.get_users()
if result.get("result") == "success":
users_map = {
user["email"].lower(): user["full_name"]
for user in result.get("members", [])
if not user.get("is_bot", False)
}
return users_map
except Exception as e:
logger.warning(f"Failed to fetch Zulip users: {e}")
return {}
def format_participant_mentions(
participants: list[dict],
zulip_users: dict[str, str],
) -> str:
"""Format participants as Zulip mentions where possible, plain names otherwise."""
formatted = []
for p in participants:
email = p["email"].lower()
if email in zulip_users:
formatted.append(f'@**{zulip_users[email]}**')
else:
formatted.append(p["name"])
return ", ".join(formatted)
def send_zulip_notification(
title: str,
start_time: datetime,
participant_names: list[str],
participants: list[dict],
) -> bool:
if not settings.zulip_site or not settings.zulip_api_key or not settings.zulip_email:
logger.warning("Zulip credentials not configured. Skipping notification.")
return False
try:
@@ -21,13 +52,15 @@ def send_zulip_notification(
site=settings.zulip_site
)
formatted_time = start_time.strftime('%Y-%m-%d %H:%M %Z')
people = ", ".join(participant_names)
zulip_users = get_zulip_usernames_by_email(client)
people = format_participant_mentions(participants, zulip_users)
zulip_time = f"<time:{start_time.isoformat()}>"
content = (
f"📅 **Meeting Scheduled**\n"
f"**What:** {title}\n"
f"**When:** {formatted_time}\n"
f"**When:** {zulip_time}\n"
f"**Who:** {people}"
)
@@ -40,7 +73,6 @@ def send_zulip_notification(
result = client.send_message(request)
if result.get("result") == "success":
logger.info("Sent Zulip notification")
return True
else:
logger.error(f"Zulip API error: {result.get('msg')}")