Skip to content

Commit ea7271e

Browse files
aapelivclaude
andcommitted
Add GetDashboardV2 RPC to aggregate dashboard content
The dashboard fires ~6 separate RPCs on load (reminders, surfing/hosting host requests, my events, community events, community discussions). Add a single GetDashboardV2 RPC on a new Dashboard service that returns all of them in one call, removing the per-request round-trip and interceptor overhead. The query logic for each section is extracted into module-level functions (get_reminders, list_host_requests, list_my_events, list_my_communities_discussions) that both the existing RPCs and the new aggregator share, so there's no duplicated query/visibility logic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 69f4cde commit ea7271e

9 files changed

Lines changed: 587 additions & 314 deletions

File tree

app/backend/src/couchers/server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
bugs_pb2_grpc,
2222
communities_pb2_grpc,
2323
conversations_pb2_grpc,
24+
dashboard_pb2_grpc,
2425
discussions_pb2_grpc,
2526
donations_pb2_grpc,
2627
editor_pb2_grpc,
@@ -53,6 +54,7 @@
5354
from couchers.servicers.bugs import Bugs
5455
from couchers.servicers.communities import Communities
5556
from couchers.servicers.conversations import Conversations
57+
from couchers.servicers.dashboard import Dashboard
5658
from couchers.servicers.discussions import Discussions
5759
from couchers.servicers.donations import Donations, Stripe
5860
from couchers.servicers.editor import Editor
@@ -108,6 +110,7 @@ def create_main_server(port: int, start_resource_sampler: bool = False) -> grpc.
108110
bugs_pb2_grpc.add_BugsServicer_to_server(Bugs(), server)
109111
communities_pb2_grpc.add_CommunitiesServicer_to_server(Communities(), server)
110112
conversations_pb2_grpc.add_ConversationsServicer_to_server(Conversations(), server)
113+
dashboard_pb2_grpc.add_DashboardServicer_to_server(Dashboard(), server)
111114
discussions_pb2_grpc.add_DiscussionsServicer_to_server(Discussions(), server)
112115
donations_pb2_grpc.add_DonationsServicer_to_server(Donations(), server)
113116
editor_pb2_grpc.add_EditorServicer_to_server(Editor(), server)

app/backend/src/couchers/servicers/account.py

Lines changed: 71 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,76 @@ def _volunteer_info_to_pb(volunteer: Volunteer, username: str) -> account_pb2.Ge
159159
)
160160

161161

162+
def get_reminders(context: CouchersContext, session: Session) -> account_pb2.GetRemindersRes:
163+
user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
164+
165+
# responding to reqs comes first in desc order of when they were received
166+
host_has_sent_message = select(1).where(
167+
Message.conversation_id == HostRequest.conversation_id, Message.author_id == HostRequest.recipient_user_id
168+
)
169+
query = select(HostRequest.conversation_id, LiteUser).join(LiteUser, LiteUser.id == HostRequest.initiator_user_id)
170+
query = where_users_column_visible(query, context, HostRequest.initiator_user_id)
171+
query = where_moderated_content_visible(query, context, HostRequest, is_list_operation=True)
172+
pending_host_requests = session.execute(
173+
query.where(HostRequest.recipient_user_id == context.user_id)
174+
.where(HostRequest.status == HostRequestStatus.pending)
175+
.where(HostRequest.start_time > func.now())
176+
.where(~exists(host_has_sent_message))
177+
.order_by(HostRequest.conversation_id.asc())
178+
).all()
179+
reminders = [
180+
account_pb2.Reminder(
181+
respond_to_host_request_reminder=account_pb2.RespondToHostRequestReminder(
182+
host_request_id=host_request_id,
183+
surfer_user=lite_user_to_pb(session, lite_user, context),
184+
)
185+
)
186+
for host_request_id, lite_user in pending_host_requests
187+
]
188+
189+
# surfer needs to confirm accepted requests
190+
confirm_query = select(HostRequest.conversation_id, LiteUser).join(
191+
LiteUser, LiteUser.id == HostRequest.recipient_user_id
192+
)
193+
confirm_query = where_users_column_visible(confirm_query, context, HostRequest.recipient_user_id)
194+
confirm_query = where_moderated_content_visible(confirm_query, context, HostRequest, is_list_operation=True)
195+
accepted_host_requests = session.execute(
196+
confirm_query.where(HostRequest.initiator_user_id == context.user_id)
197+
.where(HostRequest.status == HostRequestStatus.accepted)
198+
.where(HostRequest.end_time > func.now())
199+
.order_by(HostRequest.end_time.asc())
200+
).all()
201+
reminders += [
202+
account_pb2.Reminder(
203+
confirm_host_request_reminder=account_pb2.ConfirmHostRequestReminder(
204+
host_request_id=host_request_id,
205+
host_user=lite_user_to_pb(session, lite_user, context),
206+
)
207+
)
208+
for host_request_id, lite_user in accepted_host_requests
209+
]
210+
211+
# references come second, in order of deadline, desc
212+
reminders += [
213+
account_pb2.Reminder(
214+
write_reference_reminder=account_pb2.WriteReferenceReminder(
215+
host_request_id=host_request_id,
216+
reference_type=reftype2api[reference_type],
217+
other_user=lite_user_to_pb(session, lite_user, context),
218+
)
219+
)
220+
for host_request_id, reference_type, _, lite_user in get_pending_references_to_write(session, context)
221+
]
222+
223+
if not has_completed_profile(session, user):
224+
reminders.append(account_pb2.Reminder(complete_profile_reminder=account_pb2.CompleteProfileReminder()))
225+
226+
if user.hosting_status in (HostingStatus.can_host, HostingStatus.maybe) and not user.has_completed_my_home:
227+
reminders.append(account_pb2.Reminder(complete_my_home_reminder=account_pb2.CompleteMyHomeReminder()))
228+
229+
return account_pb2.GetRemindersRes(reminders=reminders)
230+
231+
162232
class Account(account_pb2_grpc.AccountServicer):
163233
def GetAccountInfo(
164234
self, request: empty_pb2.Empty, context: CouchersContext, session: Session
@@ -747,75 +817,7 @@ def ListInviteCodes(
747817
def GetReminders(
748818
self, request: empty_pb2.Empty, context: CouchersContext, session: Session
749819
) -> account_pb2.GetRemindersRes:
750-
user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
751-
752-
# responding to reqs comes first in desc order of when they were received
753-
host_has_sent_message = select(1).where(
754-
Message.conversation_id == HostRequest.conversation_id, Message.author_id == HostRequest.recipient_user_id
755-
)
756-
query = select(HostRequest.conversation_id, LiteUser).join(
757-
LiteUser, LiteUser.id == HostRequest.initiator_user_id
758-
)
759-
query = where_users_column_visible(query, context, HostRequest.initiator_user_id)
760-
query = where_moderated_content_visible(query, context, HostRequest, is_list_operation=True)
761-
pending_host_requests = session.execute(
762-
query.where(HostRequest.recipient_user_id == context.user_id)
763-
.where(HostRequest.status == HostRequestStatus.pending)
764-
.where(HostRequest.start_time > func.now())
765-
.where(~exists(host_has_sent_message))
766-
.order_by(HostRequest.conversation_id.asc())
767-
).all()
768-
reminders = [
769-
account_pb2.Reminder(
770-
respond_to_host_request_reminder=account_pb2.RespondToHostRequestReminder(
771-
host_request_id=host_request_id,
772-
surfer_user=lite_user_to_pb(session, lite_user, context),
773-
)
774-
)
775-
for host_request_id, lite_user in pending_host_requests
776-
]
777-
778-
# surfer needs to confirm accepted requests
779-
confirm_query = select(HostRequest.conversation_id, LiteUser).join(
780-
LiteUser, LiteUser.id == HostRequest.recipient_user_id
781-
)
782-
confirm_query = where_users_column_visible(confirm_query, context, HostRequest.recipient_user_id)
783-
confirm_query = where_moderated_content_visible(confirm_query, context, HostRequest, is_list_operation=True)
784-
accepted_host_requests = session.execute(
785-
confirm_query.where(HostRequest.initiator_user_id == context.user_id)
786-
.where(HostRequest.status == HostRequestStatus.accepted)
787-
.where(HostRequest.end_time > func.now())
788-
.order_by(HostRequest.end_time.asc())
789-
).all()
790-
reminders += [
791-
account_pb2.Reminder(
792-
confirm_host_request_reminder=account_pb2.ConfirmHostRequestReminder(
793-
host_request_id=host_request_id,
794-
host_user=lite_user_to_pb(session, lite_user, context),
795-
)
796-
)
797-
for host_request_id, lite_user in accepted_host_requests
798-
]
799-
800-
# references come second, in order of deadline, desc
801-
reminders += [
802-
account_pb2.Reminder(
803-
write_reference_reminder=account_pb2.WriteReferenceReminder(
804-
host_request_id=host_request_id,
805-
reference_type=reftype2api[reference_type],
806-
other_user=lite_user_to_pb(session, lite_user, context),
807-
)
808-
)
809-
for host_request_id, reference_type, _, lite_user in get_pending_references_to_write(session, context)
810-
]
811-
812-
if not has_completed_profile(session, user):
813-
reminders.append(account_pb2.Reminder(complete_profile_reminder=account_pb2.CompleteProfileReminder()))
814-
815-
if user.hosting_status in (HostingStatus.can_host, HostingStatus.maybe) and not user.has_completed_my_home:
816-
reminders.append(account_pb2.Reminder(complete_my_home_reminder=account_pb2.CompleteMyHomeReminder()))
817-
818-
return account_pb2.GetRemindersRes(reminders=reminders)
820+
return get_reminders(context, session)
819821

820822
def GetMyVolunteerInfo(
821823
self, request: empty_pb2.Empty, context: CouchersContext, session: Session
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from sqlalchemy.orm import Session
2+
3+
from couchers.context import CouchersContext
4+
from couchers.proto import (
5+
conversations_pb2,
6+
dashboard_pb2,
7+
dashboard_pb2_grpc,
8+
discussions_pb2,
9+
events_pb2,
10+
requests_pb2,
11+
)
12+
from couchers.servicers.account import get_reminders
13+
from couchers.servicers.discussions import list_my_communities_discussions
14+
from couchers.servicers.events import list_my_events
15+
from couchers.servicers.requests import list_host_requests
16+
17+
# the dashboard shows a small preview of each section
18+
DASHBOARD_PAGE_SIZE = 3
19+
20+
UPCOMING_STATUSES = [
21+
conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
22+
conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED,
23+
]
24+
25+
26+
class Dashboard(dashboard_pb2_grpc.DashboardServicer):
27+
def GetDashboardV2(
28+
self, request: dashboard_pb2.GetDashboardV2Req, context: CouchersContext, session: Session
29+
) -> dashboard_pb2.GetDashboardV2Res:
30+
return dashboard_pb2.GetDashboardV2Res(
31+
reminders=get_reminders(context, session),
32+
surfing=list_host_requests(
33+
requests_pb2.ListHostRequestsReq(
34+
only_sent=True,
35+
only_active=True,
36+
status_in=UPCOMING_STATUSES,
37+
sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
38+
),
39+
context,
40+
session,
41+
),
42+
hosting=list_host_requests(
43+
requests_pb2.ListHostRequestsReq(
44+
only_received=True,
45+
only_active=True,
46+
status_in=UPCOMING_STATUSES,
47+
sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
48+
),
49+
context,
50+
session,
51+
),
52+
my_events=list_my_events(
53+
events_pb2.ListMyEventsReq(page_size=DASHBOARD_PAGE_SIZE),
54+
context,
55+
session,
56+
),
57+
community_events=list_my_events(
58+
events_pb2.ListMyEventsReq(
59+
page_size=DASHBOARD_PAGE_SIZE,
60+
my_communities=True,
61+
my_communities_exclude_global=True,
62+
),
63+
context,
64+
session,
65+
),
66+
discussions=list_my_communities_discussions(
67+
discussions_pb2.ListMyCommunitiesDiscussionsReq(page_size=DASHBOARD_PAGE_SIZE),
68+
context,
69+
session,
70+
),
71+
)

app/backend/src/couchers/servicers/discussions.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,39 @@ def generate_create_discussion_notifications(payload: jobs_pb2.GenerateCreateDis
9191
)
9292

9393

94+
def list_my_communities_discussions(
95+
request: discussions_pb2.ListMyCommunitiesDiscussionsReq, context: CouchersContext, session: Session
96+
) -> discussions_pb2.ListMyCommunitiesDiscussionsRes:
97+
page_size = min(MAX_PAGE_SIZE, request.page_size or MAX_PAGE_SIZE)
98+
next_page_id = int(request.page_token) if request.page_token else 2**63 - 1
99+
100+
discussions = (
101+
session.execute(
102+
where_moderated_content_visible(
103+
select(Discussion)
104+
.join(Cluster, Cluster.id == Discussion.owner_cluster_id)
105+
.join(ClusterSubscription, ClusterSubscription.cluster_id == Cluster.id)
106+
.where(ClusterSubscription.user_id == context.user_id)
107+
.where(Cluster.is_official_cluster)
108+
.where(Cluster.small_community_features_enabled)
109+
.where(Discussion.id <= next_page_id)
110+
.order_by(Discussion.id.desc())
111+
.limit(page_size + 1),
112+
context,
113+
Discussion,
114+
is_list_operation=True,
115+
)
116+
)
117+
.scalars()
118+
.all()
119+
)
120+
121+
return discussions_pb2.ListMyCommunitiesDiscussionsRes(
122+
discussions=[discussion_to_pb(session, d, context) for d in discussions[:page_size]],
123+
next_page_token=str(discussions[-1].id) if len(discussions) > page_size else None,
124+
)
125+
126+
94127
class Discussions(discussions_pb2_grpc.DiscussionsServicer):
95128
def CreateDiscussion(
96129
self, request: discussions_pb2.CreateDiscussionReq, context: CouchersContext, session: Session
@@ -285,31 +318,4 @@ def DeleteDiscussion(
285318
def ListMyCommunitiesDiscussions(
286319
self, request: discussions_pb2.ListMyCommunitiesDiscussionsReq, context: CouchersContext, session: Session
287320
) -> discussions_pb2.ListMyCommunitiesDiscussionsRes:
288-
page_size = min(MAX_PAGE_SIZE, request.page_size or MAX_PAGE_SIZE)
289-
next_page_id = int(request.page_token) if request.page_token else 2**63 - 1
290-
291-
discussions = (
292-
session.execute(
293-
where_moderated_content_visible(
294-
select(Discussion)
295-
.join(Cluster, Cluster.id == Discussion.owner_cluster_id)
296-
.join(ClusterSubscription, ClusterSubscription.cluster_id == Cluster.id)
297-
.where(ClusterSubscription.user_id == context.user_id)
298-
.where(Cluster.is_official_cluster)
299-
.where(Cluster.small_community_features_enabled)
300-
.where(Discussion.id <= next_page_id)
301-
.order_by(Discussion.id.desc())
302-
.limit(page_size + 1),
303-
context,
304-
Discussion,
305-
is_list_operation=True,
306-
)
307-
)
308-
.scalars()
309-
.all()
310-
)
311-
312-
return discussions_pb2.ListMyCommunitiesDiscussionsRes(
313-
discussions=[discussion_to_pb(session, d, context) for d in discussions[:page_size]],
314-
next_page_token=str(discussions[-1].id) if len(discussions) > page_size else None,
315-
)
321+
return list_my_communities_discussions(request, context, session)

0 commit comments

Comments
 (0)