feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
#38 Ipad app production readiness, Colony orchestration, Social posting Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #44
This commit was merged in pull request #44.
This commit is contained in:
@@ -5,6 +5,7 @@ Mobile Edge API — serves iPhone Edge and Android Phone Edge apps.
|
||||
|
||||
Surfaces:
|
||||
GET /mobile-edge/events — communication events for a lead
|
||||
GET /mobile-edge/bulk — coordinated iPad refresh bundle
|
||||
POST /mobile-edge/events — log a new communication event
|
||||
GET /mobile-edge/memory — memory facts for a lead
|
||||
POST /mobile-edge/imports — operator-assisted import of a recording/note
|
||||
@@ -54,6 +55,22 @@ def _tenant_scope(user) -> str:
|
||||
return user.tenant_id
|
||||
|
||||
|
||||
def _normalise_lead_ids(raw_value: Optional[str], max_items: int = 24) -> list[str]:
|
||||
if not raw_value:
|
||||
return []
|
||||
seen: set[str] = set()
|
||||
lead_ids: list[str] = []
|
||||
for part in raw_value.split(","):
|
||||
lead_id = part.strip()
|
||||
if not lead_id or lead_id in seen:
|
||||
continue
|
||||
seen.add(lead_id)
|
||||
lead_ids.append(lead_id)
|
||||
if len(lead_ids) >= max_items:
|
||||
break
|
||||
return lead_ids
|
||||
|
||||
|
||||
# ── Pydantic models ───────────────────────────────────────────────────────────
|
||||
|
||||
VALID_CHANNELS = {
|
||||
@@ -135,6 +152,12 @@ class SessionHeartbeat(BaseModel):
|
||||
metadata: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class MobileEdgeBulkRequest(BaseModel):
|
||||
lead_ids: list[str] = Field(default_factory=list, max_length=100)
|
||||
events_limit_per_lead: int = Field(default=4, ge=1, le=25)
|
||||
calendar_limit: int = Field(default=50, ge=1, le=200)
|
||||
|
||||
|
||||
# ── Communication Events ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/events", summary="List communication events for a lead")
|
||||
@@ -173,6 +196,134 @@ async def list_events(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/bulk", summary="Bulk mobile-edge refresh bundle")
|
||||
async def bulk_mobile_edge(
|
||||
request: Request,
|
||||
lead_ids: Optional[str] = Query(None, description="Comma-separated lead IDs to hydrate timeline events for"),
|
||||
events_limit_per_lead: int = Query(4, ge=1, le=25),
|
||||
calendar_limit: int = Query(50, ge=1, le=200),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Returns a single coordinated payload for native surface refreshes.
|
||||
|
||||
The iPad app uses this endpoint to avoid one request for alerts, one request
|
||||
for calendar, and then one request per lead timeline.
|
||||
"""
|
||||
return await _bulk_mobile_edge_payload(
|
||||
request=request,
|
||||
user=user,
|
||||
selected_lead_ids=_normalise_lead_ids(lead_ids),
|
||||
events_limit_per_lead=events_limit_per_lead,
|
||||
calendar_limit=calendar_limit,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/bulk", summary="Bulk mobile-edge refresh bundle")
|
||||
async def bulk_mobile_edge_post(
|
||||
request: Request,
|
||||
body: MobileEdgeBulkRequest,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
"""POST variant for native clients that need a larger explicit lead set."""
|
||||
seen: set[str] = set()
|
||||
selected_lead_ids = []
|
||||
for lead_id in body.lead_ids:
|
||||
normalized = lead_id.strip()
|
||||
if normalized and normalized not in seen:
|
||||
seen.add(normalized)
|
||||
selected_lead_ids.append(normalized)
|
||||
return await _bulk_mobile_edge_payload(
|
||||
request=request,
|
||||
user=user,
|
||||
selected_lead_ids=selected_lead_ids[:100],
|
||||
events_limit_per_lead=body.events_limit_per_lead,
|
||||
calendar_limit=body.calendar_limit,
|
||||
)
|
||||
|
||||
|
||||
async def _bulk_mobile_edge_payload(
|
||||
*,
|
||||
request: Request,
|
||||
user,
|
||||
selected_lead_ids: list[str],
|
||||
events_limit_per_lead: int,
|
||||
calendar_limit: int,
|
||||
):
|
||||
tenant_id = _tenant_scope(user)
|
||||
pool = _pool(request)
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
calendar_rows = await conn.fetch(
|
||||
"""
|
||||
SELECT calendar_event_id, lead_id, title, description, start_at, end_at,
|
||||
all_day, status, reminder_minutes, created_by, location, metadata, created_at
|
||||
FROM user_calendar_events
|
||||
WHERE tenant_id=$1 AND owner_user_id=$2
|
||||
AND status <> 'cancelled'
|
||||
ORDER BY start_at ASC LIMIT $3
|
||||
""",
|
||||
tenant_id, user.user_id, calendar_limit,
|
||||
)
|
||||
|
||||
if selected_lead_ids:
|
||||
event_rows = await conn.fetch(
|
||||
"""
|
||||
SELECT event_id, lead_id, channel, direction, provider, capture_mode,
|
||||
consent_state, timestamp, duration_seconds, summary, raw_reference,
|
||||
recording_ref, provider_metadata, created_at
|
||||
FROM (
|
||||
SELECT event_id, lead_id, channel, direction, provider, capture_mode,
|
||||
consent_state, timestamp, duration_seconds, summary, raw_reference,
|
||||
recording_ref, provider_metadata, created_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY lead_id ORDER BY timestamp DESC) AS row_number
|
||||
FROM edge_communication_events
|
||||
WHERE tenant_id=$1 AND lead_id = ANY($2::text[])
|
||||
) ranked_events
|
||||
WHERE row_number <= $3
|
||||
ORDER BY lead_id ASC, timestamp DESC
|
||||
""",
|
||||
tenant_id, selected_lead_ids, events_limit_per_lead,
|
||||
)
|
||||
else:
|
||||
event_rows = []
|
||||
|
||||
pending_insights = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM insight_recommendations WHERE tenant_id=$1 AND status='pending'",
|
||||
tenant_id,
|
||||
)
|
||||
upcoming_events = await conn.fetchval(
|
||||
"""
|
||||
SELECT COUNT(*) FROM user_calendar_events
|
||||
WHERE tenant_id=$1 AND owner_user_id=$2
|
||||
AND status='confirmed'
|
||||
AND start_at BETWEEN NOW() AND NOW() + INTERVAL '24 hours'
|
||||
""",
|
||||
tenant_id, user.user_id,
|
||||
)
|
||||
pending_transcriptions = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM edge_transcription_jobs WHERE tenant_id=$1 AND status='pending'",
|
||||
tenant_id,
|
||||
)
|
||||
|
||||
events_by_lead_id: dict[str, list[dict[str, Any]]] = {lead_id: [] for lead_id in selected_lead_ids}
|
||||
for row in event_rows:
|
||||
event = dict(row)
|
||||
events_by_lead_id.setdefault(event["lead_id"], []).append(event)
|
||||
|
||||
return {
|
||||
"calendar_events": [dict(r) for r in calendar_rows],
|
||||
"lead_events": events_by_lead_id,
|
||||
"alerts": {
|
||||
"pending_insights": pending_insights,
|
||||
"upcoming_calendar_events_24h": upcoming_events,
|
||||
"pending_transcriptions": pending_transcriptions,
|
||||
"generated_at": _now(),
|
||||
},
|
||||
"generated_at": _now(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/events", status_code=status.HTTP_201_CREATED, summary="Log a communication event")
|
||||
async def create_event(
|
||||
request: Request,
|
||||
|
||||
Reference in New Issue
Block a user