Skip to content

feat(library-traffic): Library Traffic Averaging Endpoints#413

Open
estao1 wants to merge 26 commits into
mainfrom
268-reduce-library-traffic-scrape-frequency
Open

feat(library-traffic): Library Traffic Averaging Endpoints#413
estao1 wants to merge 26 commits into
mainfrom
268-reduce-library-traffic-scrape-frequency

Conversation

@estao1
Copy link
Copy Markdown
Contributor

@estao1 estao1 commented May 20, 2026

Description

Adds three new REST + GraphQL endpoints under /v2/rest/libraryTraffic for accessing historical occupancy data, alongside small consistency improvements to the existing snapshot endpoint.

New endpoints

Endpoint Purpose
GET /history Raw, cursor-paginated occupancy rows. Filter by location, date range, or academic term.
GET /history/aggregated Time-bucketed averages (hour / day / week / month). Scope with (startDate + endDate) or (year + quarter). Range cap per granularity to bound response size.
GET /history/pattern Recurring-slot pattern averages — e.g. "typical 2pm", "typical Monday". Granularity controls the cycle (24 hour-of-day buckets, 7 day-of-week, 10 week-of-term, 12 month).

Equivalent GraphQL queries (libraryTrafficHistory, libraryTrafficHistoryAggregated, libraryTrafficHistoryPattern) are exposed with matching shapes and @cacheControl(maxAge: 1800).

Implementation notes

  • Calendar-term joins. year + quarter + period are convenience filters that resolve via calendarTerm. Raw and aggregated overwrite the date range from the term; pattern joins on timestamp BETWEEN periodStart AND periodEnd so each row gets its term context.
  • Week-of-term. The pattern endpoint computes week buckets relative to the term's period start (floor((ts - periodStart)/604800) + 1), so week numbers are 1–10 (term-relative) rather than ISO weeks of the year. When granularity=week with an explicit quarter filter, rows are separated per-term and include year / quarter in the response; otherwise data is combined across terms.
  • Composite cursor. Raw pagination uses the row's UUID as the cursor with a (timestamp > cursor.ts) OR (timestamp = cursor.ts AND id > cursor.id) predicate to keep pagination stable across duplicate timestamps (multiple locations scraped at the same second).
  • Production cache. REST routes register productionCache({ maxAge: 1800 }), matching the 30-minute scraper cadence and the GraphQL @cacheControl TTLs.
  • Error mapping. Invalid cursors and missing calendar terms throw HTTPException(400) instead of Error, so users get proper status codes instead of 500s.
  • Empty results. Cursor pagination's natural end state is an empty page — the three new history endpoints return 200 with an empty array/items, matching the codebase convention (courses, instructors, enrollment-history). The pre-existing snapshot endpoint keeps its 400-on-empty behavior.

Related Issue

Closes #243

Motivation and Context

The existing snapshot endpoint only exposes current occupancy. Consumers (study-spot recommendations, dashboards, etc.) need historical views — both raw records for export/analysis and aggregated/pattern views for charts.

How Has This Been Tested?

Manually verified each endpoint via curl against a local dev server with backfilled history:

  • GET /libraryTraffic — snapshot regression
  • GET /libraryTraffic/history — pagination terminates correctly; composite cursor handles duplicate timestamps
  • GET /libraryTraffic/history/aggregated with (startDate + endDate), (year + quarter), and missing both (422)
  • GET /libraryTraffic/history/pattern with granularity=hour|day|week|month, with and without year/quarter filters, verifying week-of-term buckets are 1–10 and labels are clean ("2pm", "Monday", "Week 5")
  • GraphQL queries via Yoga playground for the same matrix
  • pnpm check:types and pnpm check:biome clean for apps/api

Screenshots (if appropriate):

image image image

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code involves a change to the database schema.
  • My code requires a change to the documentation.

estao1 added 20 commits April 21, 2026 18:52
…ror handling and update period to bucketStart
@estao1 estao1 marked this pull request as ready for review May 22, 2026 01:32
@laggycomputer laggycomputer requested a review from HwijungK May 22, 2026 02:13
@estao1 estao1 changed the title 268 reduce library traffic scrape frequency Library Traffic Averaging Endpoints May 22, 2026
@estao1 estao1 changed the title Library Traffic Averaging Endpoints feat(library-traffic): Library Traffic Averaging Endpoints May 22, 2026
Comment on lines +235 to +236
? sql`EXISTS (SELECT 1 FROM calendar_term ct WHERE ${libraryTrafficHistory.timestamp} BETWEEN ct.finals_start AND ct.finals_end${yearClause}${quarterClause})`
: sql`EXISTS (SELECT 1 FROM calendar_term ct WHERE ${libraryTrafficHistory.timestamp} BETWEEN ct.instruction_start AND ct.instruction_end${yearClause}${quarterClause})`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use any less raw SQL for this?

Comment on lines +6 to +22
const la = new Intl.DateTimeFormat("sv-SE", {
timeZone: "America/Los_Angeles",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(utcDate);
const localStr = la.replace(" ", "T");
const offsetMin = (Date.parse(`${localStr}Z`) - utcDate.getTime()) / 60000;
const sign = offsetMin >= 0 ? "+" : "-";
const abs = Math.abs(offsetMin);
const offset = `${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
return `${localStr}${offset}`;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspected this is duplicated with study rooms code. Make sure this is new or consolidate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Library Traffic Data Averaging Using Historical Data

2 participants