1 Commits

Author SHA1 Message Date
457722a79c Set Docker host for deployment workflow 2026-02-23 12:39:33 -08:00
16 changed files with 963 additions and 551 deletions

View File

@@ -1,47 +0,0 @@
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 }}'

60
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
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
View File

@@ -1,103 +0,0 @@
# 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 +1,93 @@
See AGENTS.md
# CLAUDE.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.

4
Caddyfile Normal file
View File

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

View File

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

View File

@@ -19,8 +19,6 @@ services:
build: .
command: npm run start
restart: unless-stopped
ports:
- "4000:4000"
depends_on:
- redis
environment:
@@ -36,6 +34,18 @@ services:
- ./redis-stack.conf:/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:
redis_data:
caddy_data:

1113
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": "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": "node ./dist/index.js",
"start": "npm run build && node ./dist/index.js",
"generate": "graphql-codegen --config codegen.ts",
"test": "npm run build:dev && jest --runInBand"
},
@@ -26,12 +26,12 @@
},
"private": true,
"dependencies": {
"@apollo/server": "^5.0.0",
"@apollo/server": "^4.11.2",
"@as-integrations/express5": "^1.1.2",
"express": "^5.1.0",
"express-rate-limit": "^8.0.1",
"express-slow-down": "^3.0.0",
"graphql": "^16.11.0",
"graphql": "^16.10.0",
"jsonwebtoken": "^9.0.2",
"redis": "^4.7.0"
},

View File

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

View File

@@ -19,7 +19,6 @@ import { InMemoryExternalSourceETARepository } from "../repositories/shuttle/eta
import { ETAGetterRepository } from "../repositories/shuttle/eta/ETAGetterRepository";
import { InMemorySelfUpdatingETARepository } from "../repositories/shuttle/eta/InMemorySelfUpdatingETARepository";
import { BaseInMemoryETARepository } from "../repositories/shuttle/eta/BaseInMemoryETARepository";
import { IRegion } from "./SharedEntities";
export interface InterchangeSystemBuilderArguments {
name: string;
@@ -50,12 +49,6 @@ export interface InterchangeSystemBuilderArguments {
* at a stop, in latitude/longitude degrees.
*/
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 {
@@ -69,7 +62,6 @@ export class InterchangeSystem {
public notificationRepository: NotificationRepository,
public parkingTimedDataLoader: TimedApiBasedRepositoryLoader | null,
public parkingRepository: ParkingGetterSetterRepository | null,
public initialRegion: IRegion | null,
) {
}
@@ -104,7 +96,6 @@ export class InterchangeSystem {
notificationRepository,
timedParkingLoader,
parkingRepository,
args.initialRegion ?? null,
);
}
@@ -214,7 +205,6 @@ export class InterchangeSystem {
notificationRepository,
timedParkingLoader,
parkingRepository,
args.initialRegion ?? null,
);
}

View File

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

View File

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

View File

@@ -83,14 +83,6 @@ export const SystemResolvers: Resolvers<ServerContext> = {
const shuttles = await system.shuttleRepository.getShuttles();
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) => {
const system = contextValue.findSystemById(parent.id);
if (!system) {

View File

@@ -35,37 +35,6 @@ 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", () => {
const query = `
query GetSystemRoutes($systemId: ID!) {

View File

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