dashboard
import "github.com/Ask-Atlas/AskAtlas/api/internal/dashboard"
Package dashboard implements the GET /api/me/dashboard service surface (ASK-155): the aggregated home-page payload combining enrolled courses for the current term, recently updated study guides, practice stats + recent sessions, and file totals + recent files.
The endpoint is deliberately a single round-trip with ~10 underlying queries because the home page is the most-loaded authenticated surface and we want one network hop to render it. Each section is independent; a DB error in any one section fails the whole request (per spec) rather than returning partial data that the frontend would have to render around.
Soft-deleted entities are excluded everywhere -- counts, lists, and aggregate sums all filter on the same predicates the rest of the API uses (study_guides.deleted_at IS NULL, files.deletion_status IS NULL).
Index
- Constants
- type DashboardCourseSummary
- type DashboardCoursesSection
- type DashboardData
- type DashboardFileSummary
- type DashboardFilesSection
- type DashboardPracticeSection
- type DashboardSessionSummary
- type DashboardStudyGuideSummary
- type DashboardStudyGuidesSection
- type GetDashboardParams
- type MemberRole
- type Repository
- type Service
Constants
const (
// RecentGuidesLimit caps the number of guides returned in the
// study_guides.recent array. Matches the spec's "Hardcoded
// limits" section.
RecentGuidesLimit int32 = 5
// RecentSessionsLimit caps the number of sessions returned in
// the practice.recent_sessions array.
RecentSessionsLimit int32 = 5
// RecentFilesLimit caps the number of files returned in the
// files.recent array.
RecentFilesLimit int32 = 5
// MaxCourses caps the number of courses returned in the
// courses.courses array. The dashboard is for the *current
// term* and most users have <10 courses per term, so the cap
// is a safety floor rather than a real product limit.
MaxCourses int32 = 10
)
type DashboardCourseSummary
DashboardCourseSummary mirrors api.DashboardCourseSummary on the wire. Includes the viewer's role + the section's term so the home page can render "CPTS 322 (Spring 2026, student)" without follow-up requests.
type DashboardCourseSummary struct {
ID uuid.UUID
Department string
Number string
Title string
Role MemberRole
SectionTerm string
}
type DashboardCoursesSection
DashboardCoursesSection is the courses block of the dashboard payload. CurrentTerm is *string to encode "no enrollments at all" as JSON null; CurrentTerm being non-nil but EnrolledCount==0 is not a valid state (the term resolver only returns a term when the user has at least one enrollment).
type DashboardCoursesSection struct {
EnrolledCount int32
CurrentTerm *string
Courses []DashboardCourseSummary
}
type DashboardData
DashboardData is the assembled payload returned by Service.GetDashboard. All four sections are always present (never nil); their inner list fields default to empty slices, count fields to 0.
type DashboardData struct {
Courses DashboardCoursesSection
StudyGuides DashboardStudyGuidesSection
Practice DashboardPracticeSection
Files DashboardFilesSection
}
type DashboardFileSummary
DashboardFileSummary mirrors api.DashboardFileSummary on the wire.
type DashboardFileSummary struct {
ID uuid.UUID
Name string
MimeType string
UpdatedAt time.Time
}
type DashboardFilesSection
DashboardFilesSection is the files block of the dashboard payload. TotalSize is bytes (int64) because file sizes can exceed int32 limits in aggregate.
type DashboardFilesSection struct {
TotalCount int32
TotalSize int64
Recent []DashboardFileSummary
}
type DashboardPracticeSection
DashboardPracticeSection is the practice block of the dashboard payload. All three integer fields are 0 when the user has no completed sessions; RecentSessions is an empty slice in that case (never nil).
type DashboardPracticeSection struct {
SessionsCompleted int32
TotalQuestionsAnswered int32
OverallAccuracy int32
RecentSessions []DashboardSessionSummary
}
type DashboardSessionSummary
DashboardSessionSummary mirrors api.DashboardSessionSummary on the wire. ScorePercentage is the rounded per-session accuracy (correct_answers / total_questions * 100) computed in Go so the SQL stays simple; if total_questions is 0 the percentage is 0 rather than divide-by-zero (mirroring the overall_accuracy behavior).
type DashboardSessionSummary struct {
ID uuid.UUID
QuizTitle string
StudyGuideTitle string
ScorePercentage int32
CompletedAt time.Time
}
type DashboardStudyGuideSummary
DashboardStudyGuideSummary mirrors api.DashboardStudyGuideSummary on the wire.
type DashboardStudyGuideSummary struct {
ID uuid.UUID
Title string
CourseDepartment string
CourseNumber string
UpdatedAt time.Time
}
type DashboardStudyGuidesSection
DashboardStudyGuidesSection is the study_guides block of the dashboard payload.
type DashboardStudyGuidesSection struct {
CreatedCount int32
Recent []DashboardStudyGuideSummary
}
type GetDashboardParams
GetDashboardParams is the input to Service.GetDashboard. ViewerID is resolved from the Clerk JWT in the handler before this struct is constructed; the service never sees the JWT directly. No filters or pagination on this endpoint -- the home page wants the same shape every render.
type GetDashboardParams struct {
ViewerID uuid.UUID
}
type MemberRole
MemberRole names the values stored in the course_role enum and surfaced via the dashboard's per-course summary. The string values match the openapi enum exactly so the mapper can cast directly without a switch.
type MemberRole string
const (
MemberRoleStudent MemberRole = "student"
MemberRoleTA MemberRole = "ta"
MemberRoleInstructor MemberRole = "instructor"
)
type Repository
Repository is the data-access surface required by the dashboard service. The 3 term-resolver methods propagate sql.ErrNoRows directly (without mapping to apperrors.ErrNotFound) because the service treats "no row" as a control-flow signal -- step 1 falls through to step 2, etc. -- not as an end-user error.
type Repository interface {
// Term-resolution waterfall.
ResolveCurrentTermActive(ctx context.Context, viewerID pgtype.UUID) (string, error)
ResolveCurrentTermLastEnded(ctx context.Context, viewerID pgtype.UUID) (string, error)
ResolveCurrentTermLexLatest(ctx context.Context, viewerID pgtype.UUID) (string, error)
// Courses section.
ListEnrolledCoursesForTerm(ctx context.Context, arg db.ListEnrolledCoursesForTermParams) ([]db.ListEnrolledCoursesForTermRow, error)
// Study-guides section.
CountUserStudyGuides(ctx context.Context, viewerID pgtype.UUID) (int64, error)
ListRecentUserStudyGuides(ctx context.Context, arg db.ListRecentUserStudyGuidesParams) ([]db.ListRecentUserStudyGuidesRow, error)
// Practice section.
GetUserPracticeStats(ctx context.Context, viewerID pgtype.UUID) (db.GetUserPracticeStatsRow, error)
CountUserAnsweredQuestions(ctx context.Context, viewerID pgtype.UUID) (int64, error)
ListRecentUserSessions(ctx context.Context, arg db.ListRecentUserSessionsParams) ([]db.ListRecentUserSessionsRow, error)
// Files section.
GetUserFileStats(ctx context.Context, viewerID pgtype.UUID) (db.GetUserFileStatsRow, error)
ListRecentUserFiles(ctx context.Context, arg db.ListRecentUserFilesParams) ([]db.ListRecentUserFilesRow, error)
}
func NewSQLCRepository
func NewSQLCRepository(queries *db.Queries) Repository
NewSQLCRepository returns a Repository backed by sqlc-generated Postgres queries.
type Service
Service implements the GET /api/me/dashboard business logic.
type Service struct {
// contains filtered or unexported fields
}
func NewService
func NewService(repo Repository) *Service
NewService wires a dashboard Service over the given repository.
func (*Service) GetDashboard
func (s *Service) GetDashboard(ctx context.Context, p GetDashboardParams) (DashboardData, error)
GetDashboard assembles the full dashboard payload for the viewer (ASK-155).
Strategy:
- Resolve the current term via the 3-step waterfall (active -> last-ended -> lex-latest). When the user has no enrollments at all every step returns sql.ErrNoRows; the resolved term is nil and the courses section ships as {enrolled_count: 0, current_term: null, courses: []}.
- For each of the 4 sections fan out to per-section queries and assemble in-process. Sequential rather than goroutine fan-out: each query is sub-2ms on indexed reads at MVP scale, and the connection-pool churn from 10 concurrent queries would outweigh the parallelism win for what is already a single-client request.
- Any per-query error fails the entire request (per spec -- never return partial data). Wrapped with section context so a 500 logs identify the failing section.
Generated by gomarkdoc