23 Commits

Author SHA1 Message Date
506ac612bc Merge branch 'main' of ssh://gitea.bchen.dev:2222/brendan/project-inter-server
All checks were successful
Deploy to Coolify / deploy (push) Successful in 3m48s
2026-09-02 14:34:22 -07:00
30edc307af CLAUDE.md -> AGENTS.md 2026-09-02 14:34:16 -07:00
20adae3675 Delete .github/workflows/deploy.yml
Some checks failed
Deploy to Coolify / deploy (push) Has been cancelled
2026-09-02 21:21:42 +00:00
cd7d529b86 Rename the workflow
Some checks failed
Deploy to Coolify / deploy (push) Has been cancelled
2026-09-02 21:21:26 +00:00
15504ecba3 Add the build step to the Dockerfile
All checks were successful
Publish image / deploy (push) Successful in 3m47s
2026-09-02 13:49:27 -07:00
e98ef96b2e Update package.json so start no longer runs build
Some checks failed
Publish image / deploy (push) Has been cancelled
2026-09-02 20:40:40 +00:00
98eb1083eb Add image deployment workflow
Some checks failed
Publish image / deploy (push) Has been cancelled
2026-09-01 13:58:08 +00:00
3aa2a2583d Merge pull request #110 from brendan-ch/claude/apollo-router-migration-research-y8oVn
claude/apollo-router-migration-research-y8oVn
2026-03-27 18:35:47 -07:00
5b441b3698 Fix remaining npm audit issues 2026-03-27 18:15:33 -07:00
e45a94f260 Upgrade Apollo Server 4 to 5
@apollo/server v4 reached end-of-life on January 26, 2026. This bumps
to v5 (resolves to 5.5.0) and raises the graphql peer requirement to
^16.11.0 as required by AS5. No application code changes were needed
since the project already used the AS5-compatible patterns (separate
@as-integrations/express5 package, context in expressMiddleware options).

https://claude.ai/code/session_015DcGUmHQtHQRt2QUSefXga
2026-03-27 15:36:15 +00:00
c088f1003b Merge pull request #109 from brendan-ch/chore/remove-docker-host-in-deploy
chore/remove-docker-host-in-deploy
2026-03-24 16:13:42 -07:00
e0e4092218 Add Docker host and runtime directory exports 2026-03-24 16:12:28 -07:00
893274ca6a Remove DOCKER_HOST export in deploy.yml
Remove Docker host export from deployment script.
2026-03-24 16:08:12 -07:00
0601d9e6a0 Merge pull request #108 from brendan-ch/chore/remove-tailscale-for-deploy
chore/remove-tailscale-for-deploy
2026-03-24 16:01:33 -07:00
91f5f4ba7c Remove Tailscale connection from deploy workflow
Removed Tailscale connection step from deployment workflow.
2026-03-24 15:57:38 -07:00
4764ee6af0 Merge pull request #106 from brendan-ch/claude/add-initial-region-loading-jfNcI 2026-03-23 15:23:47 -07:00
44d2da45d1 Update Chapman region to wider campus bounds
https://claude.ai/code/session_0162emnCi1KxW5FkTNn2ZrvH
2026-03-23 22:08:57 +00:00
fe649c81eb Change region to use top-left and bottom-right coordinates
Replace center+delta region representation with a bounding box defined
by topLeft and bottomRight coordinate pairs, which maps more directly
to the visible map area.

https://claude.ai/code/session_0162emnCi1KxW5FkTNn2ZrvH
2026-03-23 22:05:57 +00:00
946f8dd342 Add initial region loading for supported systems
Expose an initialRegion field on the System GraphQL type so the app can
display the correct campus area on first load instead of a zoomed-out
default map view. Chapman University is configured with coordinates
centered on campus.

https://claude.ai/code/session_0162emnCi1KxW5FkTNn2ZrvH
2026-03-23 22:02:42 +00:00
690b2e1559 Merge pull request #105 from brendan-ch:chore/update-caddy-config
chore/update-caddy-config
2026-02-25 16:42:23 -08:00
78f20fa925 Remove the Caddyfile and expose port 4000 locally 2026-02-25 16:40:43 -08:00
90d993736d Merge pull request #104 from brendan-ch/chore/patch-deploy-workflow-1
chore/patch-deploy-workflow
2026-02-23 12:40:31 -08:00
4d79b842f5 Set Docker host for deployment workflow 2026-02-23 12:39:59 -08:00
16 changed files with 554 additions and 966 deletions

47
.github/workflows/deploy-coolify.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: "Deploy to Coolify"
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Login to Gitea Container Registry
uses: docker/login-action@v4
with:
registry: gitea.bchen.dev
username: ${{ github.actor }}
password: ${{ secrets.PAT }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: gitea.bchen.dev/${{ github.actor }}/project-inter-server:latest
- name: Set up Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
use-cache: false
tailscaled-args: --tun=userspace-networking --socks5-server=localhost:1055 --outbound-http-proxy-listen=localhost:1055
- name: Deploy to Coolify
run: |
curl --request POST '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'

View File

@@ -1,60 +0,0 @@
name: "Deploy to birb co."
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
# See documentation: https://github.com/tailscale/github-action?tab=readme-ov-file
- name: Connect to Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
tags: tag:ci
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan ${{ vars.HOST }} >> ~/.ssh/known_hosts
- name: Remove directory from server
run: |
ssh -i ~/.ssh/id_ed25519 ${{ vars.USERNAME }}@${{ vars.HOST }} << 'EOF'
rm -rf ~/${{ vars.DIRECTORY_NAME }}
EOF
# Avoid needing to set up SSH access to GitHub for this user
- name: Transfer repository files to server
run: |
scp -i ~/.ssh/id_ed25519 -r ./* ${{ vars.USERNAME }}@${{ vars.HOST }}:~/${{ vars.DIRECTORY_NAME }}
- name: Deploy on server with Docker
run: |
ssh -i ~/.ssh/id_ed25519 ${{ vars.USERNAME }}@${{ vars.HOST }} << 'EOF'
cd ~/${{ vars.DIRECTORY_NAME }}
export APNS_IS_PRODUCTION=${{ secrets.APNS_IS_PRODUCTION }}
export APNS_BUNDLE_ID=${{ secrets.APNS_BUNDLE_ID }}
export APNS_TEAM_ID=${{ secrets.APNS_TEAM_ID }}
export APNS_KEY_ID=${{ secrets.APNS_KEY_ID }}
export APNS_PRIVATE_KEY=${{ secrets.APNS_PRIVATE_KEY }}
export PARKING_LOGGING_INTERVAL_MS=${{ secrets.PARKING_LOGGING_INTERVAL_MS }}
export RATE_LIMITS_DISABLED=${{ secrets.RATE_LIMITS_DISABLED }}
export RATE_LIMIT_WINDOW_MS=${{ secrets.RATE_LIMIT_WINDOW_MS }}
export RATE_LIMIT_DELAY_AFTER_REQUESTS=${{ secrets.RATE_LIMIT_DELAY_AFTER_REQUESTS }}
export RATE_LIMIT_DELAY_MULTIPLIER_MS=${{ secrets.RATE_LIMIT_DELAY_MULTIPLIER_MS }}
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker compose -f docker-compose.prod.yml down --remove-orphans
docker compose -f docker-compose.prod.yml up -d --build
EOF

103
AGENTS.md Normal file
View File

@@ -0,0 +1,103 @@
# CLAUDE.md
This file provides guidance to all agents when working with code in this repository.
## Development Commands
### Core Development
```bash
# Start development server with hot reloading
docker compose run dev
# Run comprehensive test suite
docker compose run test
# Generate GraphQL TypeScript types
npm run generate
# Build for development (install, codegen, tsc)
npm run build:dev
```
Only use Docker Compose for running tests, and only use `docker compose run test`
to run tests; don't try to run tests for individual files.
## Architecture Overview
Project Inter Server is a GraphQL-based backend for college transit tracking with real-time shuttle data, parking availability, and push notifications.
- InterchangeSystem: Central orchestrator for shuttles, parking, and notifications
- Use `InterchangeSystem.build()` in production
- Use `InterchangeSystem.buildForTesting()` in tests
- Repository Pattern (data access abstraction)
- Shuttle: `UnoptimizedInMemoryShuttleRepository`
- Parking: `InMemoryParkingRepository`
- Notifications: `RedisNotificationRepository` (prod) / `InMemoryNotificationRepository` (test)
- Data Loaders (external API integration)
- `ApiBasedShuttleRepositoryLoader` Passio GO! API
- `ChapmanApiBasedParkingRepositoryLoader` Parking data
- `TimedApiBasedRepositoryLoader` Periodic refresh wrapper
- Notification System
- `ETANotificationScheduler` Shuttle arrival notifications
- `AppleNotificationSender` APNS integration
## GraphQL
- Schema definition: `schema.graphqls`
- Generated types: `src/generated/`
- Resolvers: `src/resolvers/`
- Resolver merge: `src/MergedResolvers.ts`
## Directory Structure
- `src/entities/` Core business logic
- `src/repositories/` Data access layer
- `src/loaders/` External API integrations
- `src/notifications/` Push notification system
- `src/resolvers/` GraphQL resolvers and tests
- `testHelpers/` Test utilities and mock data
## Docker Services
- `dev` Development server with hot reload
- `test` Unit/integration tests
- `redis` Persistent Redis
- `redis-no-persistence` Ephemeral Redis for tests
## Testing Patterns
- Prefer `buildForTesting()` to construct systems in tests.
- Mock external APIs using JSON snapshots under `testHelpers/jsonSnapshots`.
- Use in-memory repositories for speed where possible.
- When adding features that affect API output, add focused resolver tests in the corresponding `src/resolvers/__tests__` file.
- Separate unit tests from integration tests where practical to keep feedback fast and failures well-scoped.
## Development Guidelines
### General Guidelines
- Use test-driven development where possible. Write tests before implementation and run them before and after changes.
- Use Docker Compose for tests. Run `docker compose run test` so you can see full output.
### Git Workflow
- Name pull requests after their branch name.
### Code Style
- Prefer arrow functions, especially within classes.
- Keep changes minimal and focused; avoid unrelated refactors. Mention incidental issues separately.
- Respect existing interfaces and types; use non-destructive edits.
### Agent Tips
- Write or update tests alongside changes; validate with `docker compose run test`.
- When touching GraphQL resolvers, co-locate new tests in `src/resolvers/__tests__`.
## Multi-tenant Support
Currently supports Chapman University (Passio System ID: `263`). Each university system uses isolated repositories and configuration. New systems should be added via `InterchangeSystem` configuration and appropriate loaders.

View File

@@ -1,93 +1 @@
# CLAUDE.md See AGENTS.md
This file provides guidance to Claude Code when working with code in this repository.
## Development Commands
### Core Development
```bash
# Start development server with hot reloading
docker compose run dev
# Run comprehensive test suite
docker compose run test
# Generate GraphQL TypeScript types
npm run generate
# Build for development (install, codegen, tsc)
npm run build:dev
```
Only use Docker Compose for running tests, and only use `docker compose run test`
to run tests; don't try to run tests for individual files.
## Architecture Overview
Project Inter Server is a GraphQL-based backend for college transit tracking with real-time shuttle data, parking availability, and push notifications.
- InterchangeSystem: Central orchestrator for shuttles, parking, and notifications
- Use `InterchangeSystem.build()` in production
- Use `InterchangeSystem.buildForTesting()` in tests
- Repository Pattern (data access abstraction)
- Shuttle: `UnoptimizedInMemoryShuttleRepository`
- Parking: `InMemoryParkingRepository`
- Notifications: `RedisNotificationRepository` (prod) / `InMemoryNotificationRepository` (test)
- Data Loaders (external API integration)
- `ApiBasedShuttleRepositoryLoader` Passio GO! API
- `ChapmanApiBasedParkingRepositoryLoader` Parking data
- `TimedApiBasedRepositoryLoader` Periodic refresh wrapper
- Notification System
- `ETANotificationScheduler` Shuttle arrival notifications
- `AppleNotificationSender` APNS integration
## GraphQL
- Schema definition: `schema.graphqls`
- Generated types: `src/generated/`
- Resolvers: `src/resolvers/`
- Resolver merge: `src/MergedResolvers.ts`
## Directory Structure
- `src/entities/` Core business logic
- `src/repositories/` Data access layer
- `src/loaders/` External API integrations
- `src/notifications/` Push notification system
- `src/resolvers/` GraphQL resolvers and tests
- `testHelpers/` Test utilities and mock data
## Docker Services
- `dev` Development server with hot reload
- `test` Unit/integration tests
- `redis` Persistent Redis
- `redis-no-persistence` Ephemeral Redis for tests
## Testing Patterns
- Prefer `buildForTesting()` to construct systems in tests.
- Mock external APIs using JSON snapshots under `testHelpers/jsonSnapshots`.
- Use in-memory repositories for speed where possible.
- When adding features that affect API output, add focused resolver tests in the corresponding `src/resolvers/__tests__` file.
- Separate unit tests from integration tests where practical to keep feedback fast and failures well-scoped.
## Development Guidelines
### General Guidelines
- Use test-driven development where possible. Write tests before implementation and run them before and after changes.
- Use Docker Compose for tests. Run `docker compose run test` so you can see full output.
### Git Workflow
- Name pull requests after their branch name.
### Code Style
- Prefer arrow functions, especially within classes.
- Keep changes minimal and focused; avoid unrelated refactors. Mention incidental issues separately.
- Respect existing interfaces and types; use non-destructive edits.
### Agent Tips
- Write or update tests alongside changes; validate with `docker compose run test`.
- When touching GraphQL resolvers, co-locate new tests in `src/resolvers/__tests__`.
## Multi-tenant Support
Currently supports Chapman University (Passio System ID: `263`). Each university system uses isolated repositories and configuration. New systems should be added via `InterchangeSystem` configuration and appropriate loaders.

View File

@@ -1,4 +0,0 @@
interchange-api.bchen.dev {
reverse_proxy app:4000
}

View File

@@ -1,4 +1,7 @@
FROM node:22-alpine FROM node:22-alpine
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY . . COPY . .
RUN npm run build
EXPOSE 4000 EXPOSE 4000
CMD ["npm", "run", "start"]

View File

@@ -19,6 +19,8 @@ services:
build: . build: .
command: npm run start command: npm run start
restart: unless-stopped restart: unless-stopped
ports:
- "4000:4000"
depends_on: depends_on:
- redis - redis
environment: environment:
@@ -34,18 +36,6 @@ services:
- ./redis-stack.conf:/redis-stack.conf - ./redis-stack.conf:/redis-stack.conf
command: redis-stack-server /redis-stack.conf command: redis-stack-server /redis-stack.conf
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes: volumes:
redis_data: redis_data:
caddy_data: caddy_data:

1123
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"build:dev": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json", "build:dev": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json",
"build": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json && npm prune --omit=dev", "build": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json && npm prune --omit=dev",
"start:dev": "npm run build:dev && node ./dist/index.js", "start:dev": "npm run build:dev && node ./dist/index.js",
"start": "npm run build && node ./dist/index.js", "start": "node ./dist/index.js",
"generate": "graphql-codegen --config codegen.ts", "generate": "graphql-codegen --config codegen.ts",
"test": "npm run build:dev && jest --runInBand" "test": "npm run build:dev && jest --runInBand"
}, },
@@ -26,12 +26,12 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/server": "^4.11.2", "@apollo/server": "^5.0.0",
"@as-integrations/express5": "^1.1.2", "@as-integrations/express5": "^1.1.2",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^8.0.1", "express-rate-limit": "^8.0.1",
"express-slow-down": "^3.0.0", "express-slow-down": "^3.0.0",
"graphql": "^16.10.0", "graphql": "^16.11.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"redis": "^4.7.0" "redis": "^4.7.0"
}, },

View File

@@ -6,6 +6,7 @@ scalar DateTime
type System { type System {
id: ID! id: ID!
name: String! name: String!
initialRegion: Region
routes: [Route!] routes: [Route!]
route(id: ID): Route route(id: ID): Route
stops: [Stop!] stops: [Stop!]
@@ -16,6 +17,11 @@ type System {
parkingSystem: ParkingSystem parkingSystem: ParkingSystem
} }
type Region {
topLeft: Coordinates!
bottomRight: Coordinates!
}
type ParkingSystem { type ParkingSystem {
systemId: ID! systemId: ID!
parkingStructures: [ParkingStructure!] parkingStructures: [ParkingStructure!]

View File

@@ -19,6 +19,7 @@ import { InMemoryExternalSourceETARepository } from "../repositories/shuttle/eta
import { ETAGetterRepository } from "../repositories/shuttle/eta/ETAGetterRepository"; import { ETAGetterRepository } from "../repositories/shuttle/eta/ETAGetterRepository";
import { InMemorySelfUpdatingETARepository } from "../repositories/shuttle/eta/InMemorySelfUpdatingETARepository"; import { InMemorySelfUpdatingETARepository } from "../repositories/shuttle/eta/InMemorySelfUpdatingETARepository";
import { BaseInMemoryETARepository } from "../repositories/shuttle/eta/BaseInMemoryETARepository"; import { BaseInMemoryETARepository } from "../repositories/shuttle/eta/BaseInMemoryETARepository";
import { IRegion } from "./SharedEntities";
export interface InterchangeSystemBuilderArguments { export interface InterchangeSystemBuilderArguments {
name: string; name: string;
@@ -49,6 +50,12 @@ export interface InterchangeSystemBuilderArguments {
* at a stop, in latitude/longitude degrees. * at a stop, in latitude/longitude degrees.
*/ */
shuttleStopArrivalDegreeDelta: number; shuttleStopArrivalDegreeDelta: number;
/**
* The initial map region to display when the app first loads
* this system. Represents a center coordinate and span.
*/
initialRegion?: IRegion;
} }
export class InterchangeSystem { export class InterchangeSystem {
@@ -62,6 +69,7 @@ export class InterchangeSystem {
public notificationRepository: NotificationRepository, public notificationRepository: NotificationRepository,
public parkingTimedDataLoader: TimedApiBasedRepositoryLoader | null, public parkingTimedDataLoader: TimedApiBasedRepositoryLoader | null,
public parkingRepository: ParkingGetterSetterRepository | null, public parkingRepository: ParkingGetterSetterRepository | null,
public initialRegion: IRegion | null,
) { ) {
} }
@@ -96,6 +104,7 @@ export class InterchangeSystem {
notificationRepository, notificationRepository,
timedParkingLoader, timedParkingLoader,
parkingRepository, parkingRepository,
args.initialRegion ?? null,
); );
} }
@@ -205,6 +214,7 @@ export class InterchangeSystem {
notificationRepository, notificationRepository,
timedParkingLoader, timedParkingLoader,
parkingRepository, parkingRepository,
args.initialRegion ?? null,
); );
} }

View File

@@ -11,3 +11,8 @@ export interface ICoordinates {
longitude: number; longitude: number;
} }
export interface IRegion {
topLeft: ICoordinates;
bottomRight: ICoordinates;
}

View File

@@ -25,6 +25,10 @@ const supportedSystems: InterchangeSystemBuilderArguments[] = [
name: "Chapman University", name: "Chapman University",
useSelfUpdatingEtas: true, useSelfUpdatingEtas: true,
shuttleStopArrivalDegreeDelta: 0.001, shuttleStopArrivalDegreeDelta: 0.001,
initialRegion: {
topLeft: { latitude: 33.85733, longitude: -117.89553 },
bottomRight: { latitude: 33.73970, longitude: -117.81878 },
},
} }
] ]

View File

@@ -83,6 +83,14 @@ export const SystemResolvers: Resolvers<ServerContext> = {
const shuttles = await system.shuttleRepository.getShuttles(); const shuttles = await system.shuttleRepository.getShuttles();
return shuttles.slice().sort((a, b) => a.name.localeCompare(b.name)); return shuttles.slice().sort((a, b) => a.name.localeCompare(b.name));
}, },
initialRegion: async (parent, _args, contextValue, _info) => {
const system = contextValue.findSystemById(parent.id);
if (!system) {
return null;
}
return system.initialRegion;
},
parkingSystem: async (parent, _args, contextValue, _info) => { parkingSystem: async (parent, _args, contextValue, _info) => {
const system = contextValue.findSystemById(parent.id); const system = contextValue.findSystemById(parent.id);
if (!system) { if (!system) {

View File

@@ -35,6 +35,37 @@ describe("SystemResolvers", () => {
}); });
} }
describe("initialRegion", () => {
const query = `
query GetSystemInitialRegion($systemId: ID!) {
system(id: $systemId) {
initialRegion {
topLeft {
latitude
longitude
}
bottomRight {
latitude
longitude
}
}
}
}
`;
it("returns the initial region for the system", async () => {
const response = await getResponseFromQueryNeedingSystemId(query);
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
const initialRegion = (response.body.singleResult.data as any).system.initialRegion;
expect(initialRegion).toEqual({
topLeft: { latitude: 33.85733, longitude: -117.89553 },
bottomRight: { latitude: 33.7397, longitude: -117.81878 },
});
});
});
describe("routes", () => { describe("routes", () => {
const query = ` const query = `
query GetSystemRoutes($systemId: ID!) { query GetSystemRoutes($systemId: ID!) {

View File

@@ -26,6 +26,10 @@ const systemInfoForTesting: InterchangeSystemBuilderArguments = {
parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id,
useSelfUpdatingEtas: false, useSelfUpdatingEtas: false,
shuttleStopArrivalDegreeDelta: 0.001, shuttleStopArrivalDegreeDelta: 0.001,
initialRegion: {
topLeft: { latitude: 33.85733, longitude: -117.89553 },
bottomRight: { latitude: 33.73970, longitude: -117.81878 },
},
}; };
export function buildSystemForTesting() { export function buildSystemForTesting() {