Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ When Codex substantially authors a Git commit in this repository:

## Project

Gapwise is a privacy-first web application for students at 11 supported Canadian universities, served from `https://gapwise.ca` and dedicated university subdomains. Timetable identity and source-backed building maps cover all supported editions. Pedestrian routing and entrance coverage vary by campus; UTM-specific places and ACORN import remain scoped to the University of Toronto edition.
Gapwise is a privacy-first web application for students at 12 supported Canadian universities, served from `https://gapwise.ca` and dedicated university subdomains. Timetable identity and source-backed building maps cover all supported editions. Pedestrian routing and entrance coverage vary by campus; UTM-specific places and ACORN import remain scoped to the University of Toronto edition.

The current product:

Expand Down
2 changes: 1 addition & 1 deletion docs/CAMPUS_DATA.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Campus data and documentation boundaries

Canonical public multi-university campus facts, geometry, provenance, and data-maintenance guidance live in [`GapwiseHQ/data`](https://github.com/GapwiseHQ/data). The dataset covers 11 universities (University of Toronto, Carleton University, Toronto Metropolitan University, Queen's University, Wilfrid Laurier University, York University, McMaster University, Western University, University of Guelph, University of Ottawa, and Brock University) across 13 campus models.
Canonical public multi-university campus facts, geometry, provenance, and data-maintenance guidance live in [`GapwiseHQ/data`](https://github.com/GapwiseHQ/data). The dataset covers 12 universities (University of Toronto, Carleton University, Toronto Metropolitan University, Queen's University, Wilfrid Laurier University, York University, McMaster University, Western University, University of Guelph, University of Ottawa, Brock University, and the University of British Columbia) across 14 campus models.

Public human-readable data documentation lives at **https://docs.gapwise.ca/data/**. The currently published raw distributions live at **https://data.gapwise.ca/datasets/**.

Expand Down
2 changes: 1 addition & 1 deletion docs/ECOSYSTEM_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Gapwise is one product ecosystem implemented across seven product repositories a

| Repository | Owns | Consumes |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `data` | Canonical multi-university campus facts and geometry across 11 universities; routing graphs and reviewed entrances | Reviewed public/field evidence; core consumer tests |
| `data` | Canonical multi-university campus facts and geometry across 12 universities; routing graphs and reviewed entrances | Reviewed public/field evidence; core consumer tests |
| `gapwise` | Web/PWA across 11 university editions, timetable/gap/routing semantics, encrypted private state, API v1, OpenAPI and both SDKs | Validated build-time Data mirror; explicit AI delegation |
| `android` | Kotlin/Compose UX, Keystore persistence, native import and encrypted-sync adapter (focuses on U of T) | Core HTTP/crypto contracts and public campus APIs |
| `ios` | SwiftUI UX and portable local timetable foundation (focuses on U of T) | Campus-scoped identity; broader native campus/account integrations remain pending |
Expand Down
4 changes: 2 additions & 2 deletions docs/INFRASTRUCTURE_COST_AND_SCALING_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

Gapwise operates on an intensely disciplined **local-first, zero-unnecessary-cloud architecture**. Course schedules, `.ics` files, calendar parsing, timetable rendering, walking route graphs, gap detection, and campus building spatial indexes execute 100% client-side in the student's browser (WebAssembly and Web Crypto).

Because of this architectural posture, **Gapwise's current operational infrastructure cost is $0.00/month across all 11 supported universities**.
Because of this architectural posture, **Gapwise's current operational infrastructure cost is $0.00/month across all 12 supported universities**.

This document models the resource consumption, provider limits, financial projections, and explicit upgrade triggers across four growth stages:

Expand Down Expand Up @@ -43,7 +43,7 @@ This document models the resource consumption, provider limits, financial projec
### A. Bandwidth (Vercel & CDN)

- **Initial App Route Payload**: 467.7 KiB JS + 37.7 KiB CSS (gzipped) = ~505 KiB per new visitor.
- **Service Worker Precache**: On first load, the PWA precaches 261 entries (~18 MB uncompressed) containing the static offline campus graph for all 11 universities.
- **Service Worker Precache**: The production build precaches static application and campus assets so supported editions remain resilient after their assets have been cached.
- **Repeat Visits**: 0 bytes network bandwidth for core app; 304 Not Modified or Cache-Control immutable hits.
- **Average monthly bandwidth per active student**: Estimated at 2.5 MB/student/month (accounting for occasional app updates).

Expand Down
83 changes: 83 additions & 0 deletions e2e/ubc-university.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { watchForAppFailures } from "./helpers";

const workdayFixture = readFileSync(
new URL("../tests/fixtures/ubc-workday-sanitized.tsv", import.meta.url),
"utf8",
);

test("UBC Workday paste import preserves the Vancouver campus through timetable and routing", async ({
page,
baseURL,
}) => {
test.skip(test.info().project.name !== "chromium");
if (!baseURL) throw new Error("Playwright baseURL is required");
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?university=ubc&campus=ubc-vancouver");
await expect(page.getByText("For University of British Columbia", { exact: true })).toBeVisible();
await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC");

await page.getByRole("button", { name: "Paste Workday View My Courses rows" }).click();
await page
.getByRole("textbox", { name: "Paste schedule text from Workday View My Courses" })
.fill(workdayFixture);
await page.getByRole("button", { name: "Import pasted schedule" }).click();

await expect(page).toHaveURL(/\/today$/);
await page
.getByRole("group", { name: "View mode" })
.getByRole("button", { name: "Weekly timetable" })
.click();
await expect(page).toHaveURL(/\/timetable/);
await expect(page.getByText("CPSC 110").first()).toBeVisible();
await expect(page.getByText("ENGL 110").first()).toBeVisible();
await expect(page.getByText(/6 meetings in Fall/)).toBeVisible();
await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC");

await page
.getByRole("group", { name: "View mode" })
.getByRole("button", { name: "Day route" })
.click();
await page
.getByRole("group", { name: "Route weekday" })
.getByRole("button", { name: "Monday" })
.click();
await expect(page.getByRole("searchbox", { name: "Search UBC buildings" })).toBeVisible();
await expect(page.locator(".map-time-marker")).toHaveCount(2);

const search = page.getByRole("searchbox", { name: "Search UBC buildings" });
await search.fill("ICCS");
await expect(page.getByTestId("building-search-result").first()).toContainText(
"Computer Science",
);

await page.reload();
await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC");
await expect(page.getByRole("searchbox", { name: "Search UBC buildings" })).toBeVisible();

await page.goto("/route?university=ubc&campus=ubc-vancouver");
await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC");
await expect(page.getByRole("searchbox", { name: "Search UBC buildings" })).toBeVisible();
failures.assertClean();
});

test("UBC has its own demo timetable and usable mobile campus map", async ({ page, baseURL }) => {
test.skip(test.info().project.name !== "mobile-chromium");
if (!baseURL) throw new Error("Playwright baseURL is required");
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?university=ubc&campus=ubc-vancouver");
await expect(page.getByText("For University of British Columbia", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Try a demo" }).click();
await expect(page).toHaveURL(/\/timetable/);
await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click();
await expect(page.getByText("CPSC 110").first()).toBeVisible();
await expect(page.getByText("ENGL 110").first()).toBeVisible();

await page.getByRole("navigation", { name: "Main" }).getByRole("link", { name: "Map" }).click();
await expect(page.getByRole("searchbox", { name: "Search UBC buildings" })).toBeVisible();
await expect(page.getByRole("button", { name: "Options" })).toBeVisible();
failures.assertClean();
});
2 changes: 1 addition & 1 deletion public/institutional-brief.html
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ <h1 class="brand-title">
bridges the gap between official course schedules and physical campus navigation. It
connects class times to campus geometry—computing true usable break times, verified walking
paths, step-free navigation, and study destinations across
<strong>11 Canadian universities and 13 campus models</strong> with strict
<strong>12 Canadian universities and 14 campus models</strong> with strict
privacy-by-design.
</div>

Expand Down
Binary file added public/universities/ubc/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/universities/ubc/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/universities/ubc/favicon-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/universities/ubc/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/universities/ubc/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/universities/ubc/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions public/universities/ubc/logo-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/universities/ubc/og-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions public/universities/ubc/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Gapwise — University of British Columbia",
"short_name": "Gapwise",
"icons": [
{
"src": "/universities/ubc/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/universities/ubc/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#0d1117",
"background_color": "#0d1117",
"display": "standalone",
"start_url": "/"
}
10 changes: 8 additions & 2 deletions scripts/audit-residence-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
campusResidenceBuildings,
campusBuildingConfigurations,
CAMPUS_LABELS,
ensureCampusCatalog,
type GapwiseCampusId,
} from "../src/data/campuses/index.js";

Expand Down Expand Up @@ -46,7 +47,12 @@ export interface ResidenceCoverageMatrix {
campuses: Record<string, CampusResidenceCoverage>;
}

export function generateResidenceCoverageMatrix(): ResidenceCoverageMatrix {
export async function generateResidenceCoverageMatrix(): Promise<ResidenceCoverageMatrix> {
await Promise.all(
manifest.universities.flatMap((university) =>
university.campuses.map((campusId) => ensureCampusCatalog(campusId)),
),
);
const campusCoverages: Record<string, CampusResidenceCoverage> = {};
let totalBuildings = 0;
let totalResidences = 0;
Expand Down Expand Up @@ -112,7 +118,7 @@ export function generateResidenceCoverageMatrix(): ResidenceCoverageMatrix {
const args = process.argv.slice(2);
const checkOnly = args.includes("--check");

const matrix = generateResidenceCoverageMatrix();
const matrix = await generateResidenceCoverageMatrix();

console.log("\n=======================================================");
console.log("GAPWISE UNIVERSITY RESIDENCE COVERAGE MATRIX AUDIT");
Expand Down
4 changes: 2 additions & 2 deletions scripts/build-seo-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const PAGES: readonly SeoPage[] = [
path: "/developers",
title: "Gapwise API & SDKs — Developers",
description:
"Build with the Gapwise public campus building, place, routing, and deterministic gap-planning API, OpenAPI contract, and official SDKs across all 11 supported Canadian universities.",
"Build with the Gapwise public campus building, place, routing, and deterministic gap-planning API, OpenAPI contract, and official SDKs across all 12 supported Canadian universities.",
heading: "Deterministic campus intelligence for developers.",
detail:
"Gapwise publishes a bounded public API for campus buildings, places, routing, and gap planning across supported universities, with OpenAPI plus JavaScript/TypeScript and Python SDK documentation.",
Expand Down Expand Up @@ -338,7 +338,7 @@ function homepageStructuredData(page: SeoPage, uniContext?: UniversityContext) {
: [
"Browser-local timetable import (ACORN .ics, Carleton Central, TMU, Queen's, Laurier, York, McMaster, Western, Guelph, uOttawa, Brock schedule formats)",
"Mississauga, St. George, Scarborough, Carleton, TMU, Queen's, Laurier, York, McMaster, Western, Guelph, uOttawa, and Brock campus timetable identity",
"Source-backed building maps for all 11 supported universities",
"Source-backed building maps for all 12 supported universities",
"Source-backed UTM pedestrian routing",
"Optional encrypted private sync",
],
Expand Down
55 changes: 14 additions & 41 deletions scripts/check-seo-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,46 +442,19 @@ for (const uniId of UNIVERSITY_IDS) {

// ── Per-university social preview metadata and OG card regression checks ───────

const ALL_UNIVERSITY_IDS = [
"uoft",
"carleton",
"tmu",
"queens",
"laurier",
"york",
"mcmaster",
"western",
"guelph",
"uottawa",
"brock",
] as const;
type AnyUniversityId = (typeof ALL_UNIVERSITY_IDS)[number];
const ALL_UNIVERSITY_NAMES: Record<AnyUniversityId, string> = {
uoft: "University of Toronto",
carleton: "Carleton University",
tmu: "Toronto Metropolitan University",
queens: "Queen's University",
laurier: "Wilfrid Laurier University",
york: "York University",
mcmaster: "McMaster University",
western: "Western University",
guelph: "University of Guelph",
uottawa: "University of Ottawa",
brock: "Brock University",
};
const ALL_UNIVERSITY_ORIGINS: Record<AnyUniversityId, string> = {
uoft: "https://gapwise.ca",
carleton: "https://carleton.gapwise.ca",
tmu: "https://tmu.gapwise.ca",
queens: "https://queens.gapwise.ca",
laurier: "https://laurier.gapwise.ca",
york: "https://york.gapwise.ca",
mcmaster: "https://mcmaster.gapwise.ca",
western: "https://western.gapwise.ca",
guelph: "https://guelph.gapwise.ca",
uottawa: "https://uottawa.gapwise.ca",
brock: "https://brock.gapwise.ca",
const universityManifest = JSON.parse(await readFile("universities.json", "utf8")) as {
universities: Array<{ id: string; name: string; hosts: string[]; status: string }>;
};
const supportedUniversityEntries = universityManifest.universities.filter(
(university) => university.status === "supported",
);
const ALL_UNIVERSITY_IDS = supportedUniversityEntries.map((university) => university.id);
const ALL_UNIVERSITY_NAMES = Object.fromEntries(
supportedUniversityEntries.map((university) => [university.id, university.name]),
) as Record<string, string>;
const ALL_UNIVERSITY_ORIGINS = Object.fromEntries(
supportedUniversityEntries.map((university) => [university.id, `https://${university.hosts[0]}`]),
) as Record<string, string>;

function pngDimensions(bytes: Buffer) {
if (bytes.subarray(1, 4).toString() !== "PNG") {
Expand All @@ -491,7 +464,7 @@ function pngDimensions(bytes: Buffer) {
}

for (const uniId of ALL_UNIVERSITY_IDS) {
const name = ALL_UNIVERSITY_NAMES[uniId];
const name = ALL_UNIVERSITY_NAMES[uniId]!;
const origin = ALL_UNIVERSITY_ORIGINS[uniId];
const cardPath = `dist/universities/${uniId}/og-card.png`;
const cardBytes = await readFile(cardPath);
Expand Down Expand Up @@ -570,7 +543,7 @@ for (const uniId of ALL_UNIVERSITY_IDS) {
// Inspect every public edition page, not only the homepage. One university must never
// appear in another university edition's title, description, social tags, or fallback body.
for (const uniId of UNIVERSITY_IDS) {
const ownName = ALL_UNIVERSITY_NAMES[uniId];
const ownName = ALL_UNIVERSITY_NAMES[uniId]!;
const pages: Array<[string, string]> = [
["/", `dist/_universities/${uniId}/index.html`],
...COMMON_SEO_PATHS.map((path): [string, string] => [
Expand Down
1 change: 1 addition & 0 deletions scripts/generate-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,5 +244,6 @@ await Promise.all([
render("favicon-32x32.png", 32),
render("apple-touch-icon.png", 180),
render("icon-192.png", 192),
render("favicon-192x192.png", 192),
render("icon-512.png", 512),
]);
8 changes: 8 additions & 0 deletions scripts/generate-social-cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ const CARDS: CardConfig[] = [
tagline: "Make the time between classes count.",
detail: "Brock University",
},
{
outputPath: "public/universities/ubc/og-card.png",
accentColor: "#002145",
textColor: "#93c5fd",
title: "Gapwise",
tagline: "Make the time between classes count.",
detail: "University of British Columbia",
},
// ── Root fallbacks (U of T default) ──────────────────────
{
outputPath: "public/og-card.png",
Expand Down
1 change: 1 addition & 0 deletions src/components/MarketingLandingImpl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const INSTITUTION_MARKETING_METRICS: Record<
guelph: { count: 15, sampleCodes: ["UC", "LIB", "ROZH", "MCKN"] },
uottawa: { count: 17, sampleCodes: ["UCU", "TBT", "MRT", "DMS"] },
brock: { count: 12, sampleCodes: ["ST", "TH", "MC", "GSB"] },
ubc: { count: 236, sampleCodes: ["ICCS", "BUCH", "PHRM", "LIFE"] },
};

export function MarketingLandingImpl({
Expand Down
Loading
Loading