A social network for students to track course grades, share grade calculation templates, and manage course prerequisites, created by our group for CMPT 370 at the University of Saskatchewan. Built using Svelte, complete with a GraphQL API and Grafana Dashboard.

Watch the Overview
Check out our video overview - one of the deliverables required for the class. In it, we discuss project architecture and design decisions as well as core features.
- Ruel Nathaniel Alarcon
- Nina Sproule
- Parsa Djavaheri
- Miguel Espino
- Sydney Williamson
- A+Plus
The easiest way to run the application is with Docker Compose:
-
Make sure you have Docker and Docker Compose installed on your system.
-
Create a
.envfile with the required environment variables:NODE_ENV=development # options: production, development, test # Note: don't use NODE_ENV=production unless deploying on a site with valid SSL certificates, etc. PORT=3000 SESSION_SECRET=your-secret-key-here GRAFANA_ADMIN_USER=admin GRAFANA_ADMIN_PASSWORD=your-grafana-password-hereAn example .env file is given (env.example)
Note: The NODE_ENV variable determines which database file is used:
- development: Uses dev.db
- production: Uses prod.db
- test: Uses test.db
-
Build and start the complete stack (app, Prometheus, and Grafana):
docker-compose up
or in detached mode:
docker-compose up -d
-
Access the application at http://localhost:3000
-
Access Prometheus at http://localhost:9090
-
Access Grafana at http://localhost:3456
- Default credentials are set in your
.envfile
- Default credentials are set in your
-
Shut down docker containers using:
docker-compose down
To run only the monitoring tools (Prometheus and Grafana) without the app:
docker-compose -f docker-compose.metrics.yml up -dIf you want to work on the code directly without Docker:
-
Install dependencies:
npm install
-
Create a
.envfile with the same variables as in the Docker setup. -
Start development server:
npm run dev
This will build the frontend and start the server.
-
Access the app at http://localhost:3000
-
See self-documented API sandbox at http://localhost:3000/graphql
-
For monitoring, you can start just the monitoring stack:
docker-compose -f docker-compose.metrics.yml up -d
The application is set up to export Prometheus metrics that can be visualized in Grafana. The metrics cover HTTP requests, GraphQL operations, active sessions, and system resources.
- HTTP Request Metrics: Request counts, durations, and status codes
- GraphQL Operation Metrics: Operation counts by type and name
- Active User Metrics: Users who have been active in the last 5 minutes
- System Metrics: CPU and memory usage
Note: The
/metricsendpoint does not require authentication. The standard for securing this endpoint in production is via adding authentication to it through your reverse proxy of choice.
The preconfigured Grafana dashboard includes panels for:
- HTTP request rates and latencies
- GraphQL operation counts
- Active users (based on activity within the last 5 minutes)
- Node.js CPU and memory usage
You can add custom metrics to the application by creating new metrics in server/server.js using the prom-client library.
Note: Running development tests requires building through the development setup, not just through the Docker quickstart.
The application includes a comprehensive test suite utilizing Jest unit tests and Cypress E2E integration tests. The tests operate a separate test database to avoid affecting production data.
-
Run the Jest tests:
npm test -
Run the Cypress tests:
npm run cypress:run
-
Optionally, you can view the Cypress tests run yourself:
npm run cypress:open
Then, open E2E test specs, letting you watch each test spec run
-
You're also able to run the project locally but using the test database:
npm run dev:test
The test database clears all data each run so it's easy to test scenarios. Additionally, when on the test database you can go to
/resetto reset the database, and/seedto seed the database with some dummy data.
server/
__tests__/ # Backend test files
auth.test.js # Authentication endpoint tests
calculators.test.js # Calculator endpoint tests
courses.test.js # Course endpoint tests
templates.test.js # Template endpoint tests
users.test.js # User endpoint tests
testHelpers.js # Helper functions for tests
setup.js # Setup for individual test files
frontend/
__tests__/ # Frontend test files
utils/
gradeCalculations.test.js # Tests for grade calculation utilities
courseSorting.test.js # Tests for course sorting utilities
cypress/
e2e/ # Cypress E2E integration test specs
auth.cy.js # Tests the authentication flow
calculators.cy.js # Tests all user stories related to calculators
commandPalette.cy.js # Tests all user stories related to the command palette
courses.cy.js # Tests all user stories related to the course tracker
templates.cy.js # Tests all user stories related to shared templates
jest.config.js # Jest configuration
jest.setup.js # Global test setup and teardown
cypress.config.js # Cypress configuration
babel.config.js # Babel configuration for testing
The test suite covers both backend API endpoints and frontend utility functions.
- Create personal grade calculators with custom assessments
- Calculate weighted grade averages
- Save and update grades as they come in
- Create calculators from shared templates
- Share grade calculation structures with other students
- Include course name, term, year, and institution
- Upvote/downvote system for community curation
- Comment system for discussions
- Search templates by name, term, year, or institution
- Track courses and their prerequisites
- Mark courses as completed
- Track credit hours for each course
- Visualize course dependencies in levels
- Prevent circular prerequisites
It's important that course tracking is a separate feature from grade calculators as users may join the platform partially through university and it would be tedious to retroactively create grade calculators for classes you have already completed just to get accurate credit tracking
App.svelte: Main router and authentication flowAppShell.svelte: Main layout component that wraps the applicationCalculatorCard.svelte: Calculator item display componentCourseCard.svelte: Course card display componentTemplateCard.svelte: Template display componentVoteButtons.svelte: Template voting interfaceComments.svelte: Template comment systemCommentCard.svelte: Individual comment display componentCommentsSheet.svelte: Slide-out comment panel componentCommandPalette.svelte: Command interface for quick navigation and actions
Index.svelte: Landing page componentLogin.svelte: User login pageRegister.svelte: User registration pageCalculator.svelte: Calculator editing/viewing pageCalculators.svelte: List of user's calculatorsCourses.svelte: Course management and visualizationSearch.svelte: Template search interfaceTemplatePreview.svelte: Template details viewUser.svelte: User profile page
- Unauthenticated users can only access /, /login, /register, and /template/:id
- Template preview redirects to register to capture potential new users
- After authentication, users are redirected to their intended destination
- Authenticated users are redirected from auth pages to dashboard
Users and Authentication:
- users: id, username, password (hashed)
Grade Calculators:
- calculators: id, user_id, name, template_id, min_desired_grade (decimal), created_at
- assessments: id, calculator_id, name, weight, grade
Templates:
- calculator_templates: id, user_id, name, term, year, institution, vote_count, deleted
- template_assessments: id, template_id, name, weight
- template_votes: template_id, user_id, vote (-1 or 1)
- template_comments: id, template_id, user_id, content, created_at, updated_at
Course Tracking:
- courses: id, user_id, name, credits, completed, created_at
- course_prerequisites: course_id, prerequisite_id
The application uses GraphQL for its API, providing a single endpoint for all operations:
POST /graphql- All data operations are performed through this endpoint
- Accepts GraphQL queries and mutations in the request body
- Authentication is managed through sessions
GraphQL Schema includes:
Queries:
me: Get the current logged-in user's ID and usernameuser(id: ID!): Get a specific user by ID, returns user detailscalculator(id: ID!): Get a specific calculator with its assessments, min_desired_grade, and associated templatetemplate(id: ID!): Get a specific template with assessments, creator details, and voting informationallTemplates(query: String, term: String, year: Int, institution: String, page: Int, limit: Int): Search and filter templates with paginationcourse(id: ID!): Get a specific course with its name, credits, completion status, and prerequisitestemplateComments(templateId: ID!): Get all comments for a specific template with author informationhealth: Simple health check endpoint
Mutations:
- Authentication:
register(username: String!, password: String!): Register a new user, returns user detailslogin(username: String!, password: String!): Log in a user, returns user detailslogout: Log out the current user, returns success boolean
- Calculators:
createCalculator(name: String!, min_desired_grade: Float): Create a calculator with optional minimum gradeupdateCalculator(id: ID!, name: String, min_desired_grade: Float, assessments: [AssessmentInput!]): Update calculator details and assessmentsdeleteCalculator(id: ID!): Delete a calculator, returns success boolean
- Templates:
createTemplate(name: String!, term: String!, year: Int!, institution: String!, assessments: [TemplateAssessmentInput!]!): Create a shareable calculator templatedeleteTemplate(id: ID!): Soft delete a template (only owner can delete)useTemplate(templateId: ID!): Create a personal calculator from a templatevoteTemplate(templateId: ID!, vote: Int!): Vote on a template (1 for upvote, -1 for downvote)removeTemplateVote(templateId: ID!): Remove vote from a template
- Courses:
createCourse(name: String!, credits: Float!, prerequisiteIds: [ID!]): Create a course with optional prerequisitesupdateCourse(id: ID!, name: String, credits: Float, completed: Boolean, prerequisiteIds: [ID!]): Update course details and prerequisitesdeleteCourse(id: ID!): Delete a course, returns success boolean
- Comments:
addTemplateComment(templateId: ID!, content: String!): Add a comment to a templateupdateTemplateComment(commentId: ID!, content: String!): Update a comment (only author can update)deleteTemplateComment(commentId: ID!): Delete a comment (only author can delete)
This project relies on several external dependencies:
- @apollo/server: GraphQL server implementation
- bcrypt: Password hashing and verification
- better-sqlite3: SQLite database driver
- dotenv: Environment variable management
- express: Web application framework
- express-session: Session middleware for authentication
- graphql: GraphQL implementation
- lodash: Utility library
- svelte-routing: Client-side routing
- winston: Logging library
- prom-client: Prometheus client for Node.js
- express-prom-bundle: Express middleware for Prometheus metrics
- @sveltejs/vite-plugin-svelte: Vite plugin for Svelte
- tailwindcss and plugins: CSS framework
- jest: Testing framework
- cypress: End-to-end testing framework for integration tests
- supertest: HTTP assertion library
- start-server-and-test: Allows the server to start before running tests
- svelte: Component framework
- svelte-sonner: Toast notifications
- lucide-svelte: UI icons
- vite: Build tool and development server
- Additional utilities: bits-ui, clsx, cmdx-sk, tailwind-merge, tailwind-variants (used for shadcn-svelte)
- Users can't vote on their own templates
- Creator's templates start with automatic upvote
- Vote changes update template's total vote count
- Votes can be removed (except creator's upvote)
Templates on the search page are not simply filtered (meaning, once you create a template you should never have an empty search), they always "show" all the templates paginated, it's just that they are ordered according to the search parameters via the following classifications:
- Number of matching fields (name, term, year, institution)
- Institution match priority
- Name match priority
- Term match priority
- Vote count (descending)
- Creation date (most recent first)
- Courses are displayed in levels based on dependencies
- A course appears after all its prerequisites
- Credit hours are tracked for each course