Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing deployment and last seen #207

Merged
merged 1 commit into from
Jun 6, 2024
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
4 changes: 0 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ jobs:
aws_access_key_id = ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key = ${{ secrets.AWS_SECRET_ACCESS_KEY }}
EOF
cat >> ./devops/ec2-secrets.py << EOF
MBTA_V3_API_KEY = '${{ secrets.MBTA_V3_API_KEY }}'
LAST_SEEN_UPDATE = True
EOF
- name: Write SSH key
run: |
mkdir ~/.ssh
Expand Down
20 changes: 6 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ Dependencies:
Run:
- `$ npm install`
- `$ npm start`
- visit [http://localhost:5000/](http://localhost:5000/)
- visit [http://localhost:5173/](http://localhost:5173/)

To use an API key, put it in `server/secrets.py` or as an environment variable `MBTA_V3_API_KEY`
To use an API key, put it as an environment variable `MBTA_V3_API_KEY`

### Linting
To lint frontend and backend code, run `$ npm run lint` in the root directory
Expand All @@ -27,27 +27,19 @@ To lint just frontend code, run `$ npm run lint-frontend`

To lint just backend code, run `$ npm run lint-backend`

## Server Deployment
Additional requirements:
- nginx

Nginx serves as a reverse-proxy for the app running on localhost:5001.
The Flask app is run under gunicorn and controlled by systemd, which will restart after failure or reboot automatically.

## AWS Deployment
1. Make sure AWS CLI is set up and working — i.e. `aws cloudformation describe-stacks | wc -l` should work
2. Make sure these environment variables are set up in your shell (ask a Labs member for values if needed):
- `TM_NTT_CERT_ARN` (for production)
- `TM_LABS_WILDCARD_CERT_ARN` (for beta)
- `TM_LABS_WILDCARD_CERT_ARN` (for beta & production)
3. A key named `transitmatters-ntt` needs to be available in your AWS account and copied to `~/.ssh/transitmatters-ntt.pem`.
4. You will also need to create an `ec2-secrets.py` file in the `devops/` directory.
5. Run `cd devops && ./deploy.sh` (add `-p` for production) to deploy.
6. You're all set! Visit:
4. Run `./deploy.sh` (add `-p` for production) to deploy.
5. You're all set! Visit:
- https://ntt-beta.labs.transitmatters.org for beta
- https://traintracker.transitmatters.org for production

## Other Deployments
This project generally fits the "Flask app" mold. Contact us if you need help: labs@transitmatters.org
This project generally fits the "Chalice app" mold. Contact us if you need help: labs@transitmatters.org

## Support TransitMatters
If you've found this app helpful or interesting, please consider [donating](https://transitmatters.org/donate) to TransitMatters to help support our mission to provide data-driven advocacy for a more reliable, sustainable, and equitable transit system in Metropolitan Boston.
2 changes: 1 addition & 1 deletion deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ GIT_ABR_VERSION=`git describe --tags --abbrev=0`
echo "Deploying version $GIT_VERSION | $GIT_SHA"

# Adding some datadog tags to get better data
DD_TAGS="git.commit.sha:$GIT_SHA,git.repository_url:github.com/transitmatters/parking-explorer"
DD_TAGS="git.commit.sha:$GIT_SHA,git.repository_url:github.com/transitmatters/new-train-tracker"

npm run build

Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@
"@tanstack/react-query": "^5.40.1",
"bezier-js": "^2.5.1",
"classnames": "^2.5.1",
"dayjs": "^1.11.11",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-favicon": "2.0.5",
"react-loading-indicators": "^0.2.3",
"react-router-dom": "^6.23.1",
"react-spring": "^8.0.27",
"seamless-scroll-polyfill": "^2.3.4",
"timeago.js": "^4.0.2"
"seamless-scroll-polyfill": "^2.3.4"
},
"devDependencies": {
"@types/node": "^20.14.2",
Expand Down
10 changes: 10 additions & 0 deletions server/.chalice/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
],
"Effect": "Allow",
"Resource": "arn:*:logs:*:*:*"
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::traintracker.transitmatters.org",
"arn:aws:s3:::traintracker.transitmatters.org/*"
]
}
]
}
2 changes: 1 addition & 1 deletion server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def vehicles(trip_id, stop_id):

@app.schedule(Cron("0/10", "0-6,9-23", "*", "*", "?", "*"))
def update_last_seen(event):
asyncio.run(last_seen.update_recent_sightings())
last_seen.update_recent_sightings()


@app.route("/healthcheck", cors=cors_config)
Expand Down
9 changes: 0 additions & 9 deletions server/chalicelib/healthcheck.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import json
import os
import stat
import time
from chalice import Response
import chalicelib.secrets as secrets
from chalicelib.last_seen import JSON_PATH as LAST_SEEN_JSON_PATH


def file_age_s(pathname):
return time.time() - os.stat(pathname)[stat.ST_MTIME]


def run():
checks = [
lambda: len(secrets.MBTA_V3_API_KEY) > 0,
lambda: file_age_s(LAST_SEEN_JSON_PATH) < 1800, # allow up to 30 minutes of outdated last_seen.json
]

for i in range(0, len(checks)):
Expand Down
9 changes: 5 additions & 4 deletions server/chalicelib/last_seen.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import datetime
import json
from zoneinfo import ZoneInfo

import chalicelib.s3 as s3
import chalicelib.mbta_api as mbta_api
Expand All @@ -10,17 +11,18 @@

JSON_PATH = "last_seen.json"
ROUTES = DEFAULT_ROUTE_IDS
EASTERN_TIME = ZoneInfo("US/Eastern")


async def update_recent_sightings():
def update_recent_sightings():
try:
last_seen_times = json.loads(s3.download(JSON_PATH, "utf8", compressed=False))
except Exception as e:
print("Couldn't read last seen times from s3: ", e)
last_seen_times = {}
try:
print("Updating recent sightings...")
now = datetime.datetime.utcnow()
now = datetime.datetime.now(EASTERN_TIME)

all_vehicles = asyncio.run(mbta_api.vehicle_data_for_routes(ROUTES))
new_vehicles = filter_new(all_vehicles)
Expand All @@ -29,8 +31,7 @@ async def update_recent_sightings():
line = get_line_for_route(vehicle["route"])
last_seen_times[line] = {
"car": vehicle["label"],
# Python isoformat() doesn't include TZ, but we know this is UTC because we used utcnow() above
"time": now.isoformat()[:-3] + "Z",
"time": now.isoformat(),
}
s3.upload(JSON_PATH, json.dumps(last_seen_times), compress=False)
except Exception as e:
Expand Down
1 change: 0 additions & 1 deletion server/chalicelib/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@


def download(key, encoding="utf8", compressed=True):
print(BUCKET)
obj = s3.get_object(Bucket=BUCKET, Key=key)
s3_data = obj["Body"].read()
if not compressed:
Expand Down
17 changes: 0 additions & 17 deletions server/chalicelib/secrets.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
import os


MBTA_V3_API_KEY = os.environ.get("MBTA_V3_API_KEY", "")
# False by default, because debug Flask spawns two processes, and two last seen updaters will trample each other!
LAST_SEEN_UPDATE = False
"""
If you put your api key here, you may want to run
`git update-index --assume-unchanged server/secrets.py`
so that it doesn't pollute your `git status`

To make that more convenient, you may want to add these aliases to ~/.gitconfig

[alias]
hide = update-index --assume-unchanged
unhide = update-index --no-assume-unchanged
hidden = !git ls-files -v | grep "^[[:lower:]]"

Then run `git hide server/secrets.py`
"""
15 changes: 15 additions & 0 deletions server/cloudformation.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@
}
}
},
"UpdateLastSeen": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Description": "Update the last seen time for new trains on a schedule",
"Environment": {
"Variables": {
"MBTA_V3_API_KEY": { "Ref": "MbtaV3ApiKey" },
"DD_API_KEY": { "Ref": "DDApiKey" },
"DD_VERSION": { "Ref": "GitVersion" },
"DD_TAGS": { "Ref": "DDTags" }
}
}
}

},
"FrontendBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
Expand Down
7 changes: 5 additions & 2 deletions src/components/Line.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useMemo, useState, useLayoutEffect, useEffect } from 'react';
import classNames from 'classnames';
import * as timeago from 'timeago.js';

import { prerenderLine } from '../prerender';
import { renderTextTrainlabel } from '../labels';
Expand All @@ -10,6 +9,8 @@ import { PopoverContainerContext, getTrainRoutePairsForLine, setCssVariable } fr
import { Line as TLine, Pair, StationPositions, VehiclesAge } from '../types';
import { MBTAApi } from '../hooks/useMbtaApi';
import { useLastSightingByLine } from '../hooks/useLastSighting';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';

const AGE_WORD_MAP = new Map<VehiclesAge, string>([
['new_vehicles', ' new '],
Expand All @@ -23,6 +24,8 @@ interface LineProps {
age: VehiclesAge;
}

dayjs.extend(relativeTime);

const abbreviateStationName = (station: string) =>
station
.replace('Boston College', 'B.C.')
Expand Down Expand Up @@ -58,7 +61,7 @@ const EmptyNoticeForLine: React.FC<{ line: string; age: VehiclesAge }> = ({ line
// What to show when new is selected
if (sightingForLine) {
const { car, time } = sightingForLine;
const ago = timeago.format(time);
const ago = dayjs(time).fromNow();
return <>{`A new ${line} Line train (#${car}) was last seen ${ago}.`}</>;
}
return <>{`No new trains on the ${line} Line right now.`}</>;
Expand Down
Loading