cleanup-and-test #1

Merged
Joyce merged 7 commits from cleanup-and-test into main 2026-01-23 20:44:14 +00:00
4 changed files with 129 additions and 13 deletions
Showing only changes of commit 7fefd634f5 - Show all commits

View File

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

View File

@@ -5,13 +5,44 @@ from datetime import datetime
logger = logging.getLogger(__name__) 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( def send_zulip_notification(
title: str, title: str,
start_time: datetime, start_time: datetime,
participant_names: list[str], participants: list[dict],
) -> bool: ) -> bool:
if not settings.zulip_site or not settings.zulip_api_key or not settings.zulip_email: 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 return False
try: try:
@@ -21,13 +52,15 @@ def send_zulip_notification(
site=settings.zulip_site site=settings.zulip_site
) )
formatted_time = start_time.strftime('%Y-%m-%d %H:%M %Z') zulip_users = get_zulip_usernames_by_email(client)
people = ", ".join(participant_names) people = format_participant_mentions(participants, zulip_users)
zulip_time = f"<time:{start_time.isoformat()}>"
content = ( content = (
f"📅 **Meeting Scheduled**\n" f"📅 **Meeting Scheduled**\n"
f"**What:** {title}\n" f"**What:** {title}\n"
f"**When:** {formatted_time}\n" f"**When:** {zulip_time}\n"
f"**Who:** {people}" f"**Who:** {people}"
) )
@@ -40,7 +73,6 @@ def send_zulip_notification(
result = client.send_message(request) result = client.send_message(request)
if result.get("result") == "success": if result.get("result") == "success":
logger.info("Sent Zulip notification")
return True return True
else: else:
logger.error(f"Zulip API error: {result.get('msg')}") logger.error(f"Zulip API error: {result.get('msg')}")

52
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,52 @@
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required}
POSTGRES_DB: ${POSTGRES_DB:-availability}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
backend:
build:
context: ./backend
dockerfile: Dockerfile
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-availability}
SYNC_DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-availability}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USER: ${SMTP_USER:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
ZULIP_SITE: ${ZULIP_SITE:-}
ZULIP_EMAIL: ${ZULIP_EMAIL:-}
ZULIP_API_KEY: ${ZULIP_API_KEY:-}
ZULIP_STREAM: ${ZULIP_STREAM:-general}
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
args:
VITE_API_URL: ${VITE_API_URL:?VITE_API_URL required}
ports:
- "8080:8080"
depends_on:
- backend
restart: unless-stopped
volumes:
postgres_data:

34
frontend/Dockerfile.prod Normal file
View File

@@ -0,0 +1,34 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# VITE_API_URL must be set at build time
ARG VITE_API_URL
ENV VITE_API_URL=${VITE_API_URL}
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# Handle client-side routing
RUN echo 'server { \
listen 8080; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]