diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7ecee2e..bd24ccb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/README.md b/README.md index f861693..d627a44 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/deploy.sh b/deploy.sh index be451ca..f81f572 100755 --- a/deploy.sh +++ b/deploy.sh @@ -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 diff --git a/package-lock.json b/package-lock.json index 8a1a5f1..8ec04b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,14 +14,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", @@ -1768,6 +1768,11 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4620,11 +4625,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/timeago.js": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", - "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" - }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", diff --git a/package.json b/package.json index 04c9e07..5c0c5c5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/.chalice/policy.json b/server/.chalice/policy.json index b2124e7..d27d6fb 100644 --- a/server/.chalice/policy.json +++ b/server/.chalice/policy.json @@ -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/*" + ] } ] } diff --git a/server/app.py b/server/app.py index d79eda9..9c0c2df 100644 --- a/server/app.py +++ b/server/app.py @@ -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) diff --git a/server/chalicelib/healthcheck.py b/server/chalicelib/healthcheck.py index e33737a..126a184 100644 --- a/server/chalicelib/healthcheck.py +++ b/server/chalicelib/healthcheck.py @@ -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)): diff --git a/server/chalicelib/last_seen.py b/server/chalicelib/last_seen.py index f769c3e..63111ea 100644 --- a/server/chalicelib/last_seen.py +++ b/server/chalicelib/last_seen.py @@ -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 @@ -10,9 +11,10 @@ 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: @@ -20,7 +22,7 @@ async def update_recent_sightings(): 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) @@ -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: diff --git a/server/chalicelib/s3.py b/server/chalicelib/s3.py index 57f13d6..0838947 100644 --- a/server/chalicelib/s3.py +++ b/server/chalicelib/s3.py @@ -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: diff --git a/server/chalicelib/secrets.py b/server/chalicelib/secrets.py index 1d14741..755e575 100644 --- a/server/chalicelib/secrets.py +++ b/server/chalicelib/secrets.py @@ -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` -""" diff --git a/server/cloudformation.json b/server/cloudformation.json index 10ddd85..4105e45 100644 --- a/server/cloudformation.json +++ b/server/cloudformation.json @@ -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": { diff --git a/src/components/Line.tsx b/src/components/Line.tsx index 7eabf1d..633a225 100644 --- a/src/components/Line.tsx +++ b/src/components/Line.tsx @@ -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'; @@ -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([ ['new_vehicles', ' new '], @@ -23,6 +24,8 @@ interface LineProps { age: VehiclesAge; } +dayjs.extend(relativeTime); + const abbreviateStationName = (station: string) => station .replace('Boston College', 'B.C.') @@ -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.`};