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
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e
export PATH="/usr/local/go/bin:$HOME/go/bin:$HOME/.local/share/pnpm:$PATH"
export PATH="$HOME/.local/bin:$HOME/.local/share/pnpm/bin:$HOME/go/bin:$PATH"
cd ~/schedule-optimizer

# Pull latest changes
Expand All @@ -44,11 +44,11 @@ jobs:
# Build backend (frontend already compiled to backend/internal/static/dist)
echo "Building Go binary..."
cd backend
go build -o bin/server ./cmd/server
CGO_ENABLED=1 go build -o bin/server ./cmd/server

# Run tests
echo "Running tests..."
go test ./...
CGO_ENABLED=1 go test ./...
cd ..

# Restart service
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Schedule Optimizer is a course scheduling tool for WWU students. The main branch
Install these on a fresh server:

```bash
# Go 1.24+
# Go 1.25+
# https://go.dev/doc/install

# SQLite3
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A course scheduling tool for Western Washington University students. Add your courses, set preferences, and generate optimized, conflict-free schedules instantly.

**[Try it live](https://cwooper.me/schedule-optimizer)**
**[Try it live](https://schedule-optimizer.cwooper.me)**

> **Disclaimer:** This project is not affiliated with Western Washington University. It is an independent initiative developed for educational and personal use. All data is for informational purposes only and should not be considered official or binding.

Expand Down Expand Up @@ -45,7 +45,7 @@ Under the hood, schedule generation uses a **bitmask-based conflict detection**

| Layer | Technology |
|-------|-----------|
| Backend | Go 1.24, Gin, SQLite (WAL mode) |
| Backend | Go 1.25, Gin, SQLite (WAL mode) |
| SQL | sqlc (type-safe codegen), golang-migrate |
| Frontend | React 19, TypeScript, Vite |
| Styling | Tailwind CSS v4, shadcn/ui |
Expand Down Expand Up @@ -115,7 +115,7 @@ frontend/src/

### Prerequisites

- Go 1.24+
- Go 1.25+
- Node.js 20+ and pnpm
- SQLite 3
- [sqlc](https://sqlc.dev/) — `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest`
Expand Down
2 changes: 1 addition & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ JOBS_PAST_TERM_YEARS=5 # Years of past terms to scrape
# Production Settings
# ENVIRONMENT=production
# DATABASE_PATH=/var/lib/schedule-optimizer/schedule.db
# CORS_ALLOWED_ORIGINS=https://cwooper.me/schedule-optimizer
# CORS_ALLOWED_ORIGINS=https://schedule-optimizer.cwooper.me
4 changes: 2 additions & 2 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Go backend for WWU Schedule Optimizer using Gin, SQLite, and sqlc.
### Required Software

```bash
# Go 1.24+
# Go 1.25+
# See https://go.dev/doc/install
go version # should show go1.24 or later
go version # should show go1.25 or later

# SQLite3
sudo apt install sqlite3 # Debian/Ubuntu
Expand Down
16 changes: 8 additions & 8 deletions backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module schedule-optimizer

go 1.24.4
go 1.25.0

require (
github.com/gin-contrib/cors v1.7.6
Expand All @@ -9,7 +9,8 @@ require (
github.com/joho/godotenv v1.5.1
github.com/lmittmann/tint v1.1.2
github.com/mattn/go-sqlite3 v1.14.33
golang.org/x/sync v0.19.0
github.com/xuri/excelize/v2 v2.10.0
golang.org/x/sync v0.20.0
)

require (
Expand All @@ -32,19 +33,18 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/excelize/v2 v2.10.0 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
28 changes: 14 additions & 14 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
Expand Down Expand Up @@ -91,19 +91,19 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
12 changes: 1 addition & 11 deletions backend/internal/static/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import (
"strings"
)

// basePath is stripped from incoming requests to match nginx rewrite behavior.
const basePath = "/schedule-optimizer"

// Handler returns an http.Handler that serves static files from the embedded filesystem.
// It serves pre-compressed .gz files when the client accepts gzip encoding.
func Handler() http.Handler {
Expand All @@ -35,14 +32,8 @@ func Handler() http.Handler {
})

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Strip base path prefix (matches nginx rewrite behavior)
urlPath := strings.TrimPrefix(r.URL.Path, basePath)
if urlPath == "" {
urlPath = "/"
}

// Determine the logical file path (for gzip lookup)
filePath := strings.TrimPrefix(urlPath, "/")
filePath := strings.TrimPrefix(r.URL.Path, "/")
if filePath == "" {
filePath = "index.html"
}
Comment on lines 35 to 39
Expand All @@ -61,7 +52,6 @@ func Handler() http.Handler {
}

// Fall back to http.FileServer for non-gzip requests
r.URL.Path = urlPath
fileServer.ServeHTTP(w, r)
})
}
Expand Down
19 changes: 7 additions & 12 deletions backend/internal/static/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,13 @@ func TestHandler(t *testing.T) {
wantTypePrefix string
wantGzip bool
}{
// With /schedule-optimizer prefix (production path)
{"root with prefix", "/schedule-optimizer/", false, http.StatusOK, "text/html", false},
{"root with prefix gzip", "/schedule-optimizer/", true, http.StatusOK, "text/html", true},
{"index explicit redirects", "/schedule-optimizer/index.html", false, http.StatusMovedPermanently, "", false},
{"svg logo", "/schedule-optimizer/schopt-logo-dark.svg", false, http.StatusOK, "image/svg+xml", false},

// Without prefix (direct local access)
{"root no prefix", "/", false, http.StatusOK, "text/html", false},
{"root no prefix gzip", "/", true, http.StatusOK, "text/html", true},
{"root", "/", false, http.StatusOK, "text/html", false},
{"root gzip", "/", true, http.StatusOK, "text/html", true},
{"index explicit redirects", "/index.html", false, http.StatusMovedPermanently, "", false},
{"svg logo", "/schopt-logo-dark.svg", false, http.StatusOK, "image/svg+xml", false},

// Unknown paths should 404
{"unknown path", "/schedule-optimizer/nonexistent.txt", false, http.StatusNotFound, "", false},
{"unknown path", "/nonexistent.txt", false, http.StatusNotFound, "", false},
}

for _, tt := range tests {
Expand Down Expand Up @@ -64,7 +59,7 @@ func TestHandlerVaryHeader(t *testing.T) {
handler := Handler()

// Gzip responses should include Vary header for caching
req := httptest.NewRequest("GET", "/schedule-optimizer/", nil)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
Expand Down Expand Up @@ -92,7 +87,7 @@ func TestAcceptsGzipEdgeCases(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/schedule-optimizer/", nil)
req := httptest.NewRequest("GET", "/", nil)
if tt.acceptEncoding != "" {
req.Header.Set("Accept-Encoding", tt.acceptEncoding)
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

<meta name="description" content="Free schedule builder for Western Washington University students. Create conflict-free class schedules, compare options, and plan your quarter." />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://cwooper.me/schedule-optimizer" />
<link rel="canonical" href="https://schedule-optimizer.cwooper.me/" />

<meta property="og:type" content="website" />
<meta property="og:url" content="https://cwooper.me/schedule-optimizer" />
<meta property="og:url" content="https://schedule-optimizer.cwooper.me/" />
<meta property="og:title" content="WWU Schedule Optimizer" />
<meta property="og:description" content="Free schedule builder for Western Washington University students. Create conflict-free class schedules, compare options, and plan your quarter." />
<!-- TODO: Add og:image when created -->
Expand Down
21 changes: 19 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,25 @@
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1",
"vite": "^7.3.2",
"vite-plugin-compression": "^0.5.1",
"vitest": "^4.0.18"
"vitest": "^4.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
],
"overrides": {
"minimatch@3": "^3.1.5",
"minimatch@9": "^9.0.9",
"minimatch@10": "^10.2.5",
"picomatch@4": "^4.0.4",
"flatted@3": "^3.4.2",
"rollup@4": "^4.61.1",
"ajv@6": "^6.15.0",
"brace-expansion@1": "^1.1.15",
"brace-expansion@2": "^2.1.1",
"postcss@8": "^8.5.15"
}
}
}
Loading