Skip to content

Commit

Permalink
Fixing last seen data
Browse files Browse the repository at this point in the history
  • Loading branch information
devinmatte committed Jun 5, 2024
1 parent a7c31ee commit dd7e6de
Show file tree
Hide file tree
Showing 17 changed files with 291 additions and 385 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18
20
328 changes: 131 additions & 197 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,23 @@
},
"homepage": "https://github.com/transitmatters/new-train-tracker#readme",
"dependencies": {
"@ariakit/react": "^0.4.7",
"@tanstack/react-query": "^5.40.1",
"bezier-js": "^2.5.1",
"classnames": "^2.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-favicon": "1.0.1",
"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",
"reakit": "^1.3.11",
"seamless-scroll-polyfill": "^1.0.10",
"seamless-scroll-polyfill": "^2.3.4",
"timeago.js": "^4.0.2"
},
"devDependencies": {
"@types/node": "^20.14.1",
"@types/react": "^17.0.80",
"@types/react-dom": "^17.0.17",
"@types/node": "^20.14.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"@vitejs/plugin-react": "^4.3.0",
Expand All @@ -56,7 +57,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-react-hooks": "^4.6.2",
"prettier": "^3.3.0",
"prettier": "^3.3.1",
"typescript": "^5.4.5",
"vite": "^5.2.12"
}
Expand Down
15 changes: 7 additions & 8 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import json
import asyncio

from chalicelib import (background, last_seen, mbta_api)
from chalicelib import (last_seen, mbta_api)
import chalicelib.healthcheck
from datadog_lambda.wrapper import datadog_lambda_wrapper
from chalice import Chalice, CORSConfig, ConvertToMiddleware, Response
from chalice import Chalice, CORSConfig, ConvertToMiddleware, Response, Cron

app = Chalice(app_name="new-train-tracker")

Expand All @@ -24,12 +24,6 @@
cors_config = CORSConfig(allow_origin="*", max_age=3600)


# Start a background thread to run `schedule` (i.e. the package) jobs,
# which in our case is just the "last seen" update
background_thread = background.run_continuously()
last_seen.initialize()


# takes a comma-delimited string of route ids
@app.route("/trains/{route_ids_string}", cors=cors_config)
def trains(route_ids_string):
Expand Down Expand Up @@ -63,6 +57,11 @@ def vehicles(trip_id, stop_id):
return Response(json.dumps(departure), headers={"Content-Type": "application/json"})


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


@app.route("/healthcheck", cors=cors_config)
def healthcheck():
return chalicelib.healthcheck.run()
Expand Down
35 changes: 0 additions & 35 deletions server/chalicelib/background.py

This file was deleted.

50 changes: 12 additions & 38 deletions server/chalicelib/last_seen.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,43 @@
import asyncio
import datetime
import json
import schedule

import chalicelib.s3 as s3
import chalicelib.mbta_api as mbta_api
import chalicelib.secrets as secrets
from chalicelib.routes import get_line_for_route
from chalicelib.routes import DEFAULT_ROUTE_IDS
from chalicelib.routes import DEFAULT_ROUTE_IDS, get_line_for_route
from chalicelib.util import filter_new


JSON_PATH = "last_seen.json"
LAST_SEEN_TIMES = {}
ROUTES = DEFAULT_ROUTE_IDS


def update_recent_sightings_sync():
asyncio.run(update_recent_sightings())


def initialize():
global LAST_SEEN_TIMES

# Read from disk if there's a previous file there
try:
with open(JSON_PATH, "r", encoding="utf-8") as file:
LAST_SEEN_TIMES = json.load(file)
except FileNotFoundError:
print("Last seen file doesn't exist; starting fresh")

if not secrets.LAST_SEEN_UPDATE:
print("LAST_SEEN_UPDATE is false, so I'm not continuously updating last seen times for you.")
return
schedule.every().minute.do(update_recent_sightings_sync)


async 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()

all_vehicles = await mbta_api.vehicle_data_for_routes(ROUTES)
all_vehicles = asyncio.run(mbta_api.vehicle_data_for_routes(ROUTES))
new_vehicles = filter_new(all_vehicles)

for vehicle in new_vehicles:
line = get_line_for_route(vehicle["route"])
LAST_SEEN_TIMES[line] = {
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",
}

with open(JSON_PATH, "w", encoding="utf-8") as file:
json.dump(LAST_SEEN_TIMES, file, indent=4, sort_keys=True, default=str)
s3.upload(JSON_PATH, json.dumps(last_seen_times), compress=False)
except Exception as e:
print("Couldn't write last seen times to disk: ", e)
print("Couldn't write last seen times to s3: ", e)


# Get the last time that a new train was seen on each line
# This is the function that other modules use
def get_recent_sightings_for_lines():
return LAST_SEEN_TIMES


# For development/testing only!
if __name__ == "__main__":
initialize()
asyncio.run(update_recent_sightings())
return json.loads(s3.download(JSON_PATH, "utf8"))
14 changes: 12 additions & 2 deletions server/chalicelib/s3.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import os
import boto3
import zlib


s3 = boto3.client("s3")
BUCKET = os.environ.get("TM_CORS_HOST", "ntt-beta.labs.transitmatters.org")


def download(bucket, key, encoding="utf8", compressed=True):
obj = s3.get_object(Bucket=bucket, Key=key)
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:
return s3_data.decode(encoding)
# 32 should detect zlib vs gzip
decompressed = zlib.decompress(s3_data, zlib.MAX_WBITS | 32).decode(encoding)
return decompressed


def upload(key, bytes, compress=True):
if compress:
bytes = zlib.compress(bytes)
s3.put_object(Bucket=BUCKET, Key=key, Body=bytes)
53 changes: 27 additions & 26 deletions src/components/AgeTabPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRef, useLayoutEffect } from 'react';
import { TabList, Tab } from 'reakit';
import { TabList, Tab, TabProvider } from '@ariakit/react';
import { VehiclesAge } from '../types';
import { useAgeSearchParam } from '../hooks/searchParams';

Expand Down Expand Up @@ -36,32 +36,33 @@ export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabColor }) => {
}, [tabColor, ageSearchParam]);

return (
<TabList className="tab-picker" aria-label="Select train age" ref={wrapperRef}>
<div className="selected-indicator" ref={selectedIndicatorRef} />
<TabProvider>
<TabList className="tab-picker" aria-label="Select train age" ref={wrapperRef}>
<div className="selected-indicator" ref={selectedIndicatorRef} />

{trainTypes.map((trainType) => {
return (
<Tab
id={trainType.key}
className="tab"
key={trainType.key}
as="div"
data-color={tabColor}
onClick={() => {
setAgeSearchParam(trainType.key);
}}
>
<div
aria-label={trainType.key}
className="icon age"
style={{ backgroundColor: tabColor }}
{trainTypes.map((trainType) => {
return (
<Tab
id={trainType.key}
className="tab"
key={trainType.key}
data-color={tabColor}
onClick={() => {
setAgeSearchParam(trainType.key);
}}
>
{trainType.label.toUpperCase()}
</div>
<div className="label">trains</div>
</Tab>
);
})}
</TabList>
<div
aria-label={trainType.key}
className="icon age"
style={{ backgroundColor: tabColor }}
>
{trainType.label.toUpperCase()}
</div>
<div className="label">trains</div>
</Tab>
);
})}
</TabList>
</TabProvider>
);
};
2 changes: 1 addition & 1 deletion src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const Footer: React.FC<{ version: string }> = ({ version }) => {
export const Footer: React.FC<{ version?: string }> = ({ version }) => {
return (
<div className="footer">
<p>
Expand Down
18 changes: 10 additions & 8 deletions src/components/Line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { renderTextTrainlabel } from '../labels';

import { Train } from './Train';
import { PopoverContainerContext, getTrainRoutePairsForLine, setCssVariable } from './util';
import { getInitialDataByKey } from '../initialData';
import { Line as TLine, Pair, StationPositions, VehiclesAge } from '../types';
import { MBTAApi } from '../hooks/useMbtaApi';
import { useLastSightingByLine } from '../hooks/useLastSighting';

const AGE_WORD_MAP = new Map<VehiclesAge, string>([
['new_vehicles', ' new '],
Expand Down Expand Up @@ -46,22 +46,22 @@ const sortTrainRoutePairsByDistance = (pairs: Pair[], stationPositions: StationP
return pairs.sort((a, b) => distanceMap.get(a) - distanceMap.get(b));
};

const renderEmptyNoticeForLine = (line, age) => {
const EmptyNoticeForLine: React.FC<{ line: string; age: VehiclesAge }> = ({ line, age }) => {
const sightingForLine = useLastSightingByLine(line);

const ageWord = AGE_WORD_MAP.get(age);
// What to show when old or all is selected
if (age !== 'new_vehicles') {
return `No${ageWord}trains on the ${line} Line right now.`;
return <>{`No${ageWord}trains on the ${line} Line right now.`}</>;
}

// What to show when new is selected
const sightings = getInitialDataByKey('sightings');
const sightingForLine = sightings && sightings[line];
if (sightingForLine) {
const { car, time } = sightingForLine;
const ago = timeago.format(time);
return `A new ${line} Line train (#${car}) was last seen ${ago}.`;
return <>{`A new ${line} Line train (#${car}) was last seen ${ago}.`}</>;
}
return `No new trains on the ${line} Line right now.`;
return <>{`No new trains on the ${line} Line right now.`}</>;
};

const getRouteColor = (colors, routeId, focusedRouteId) => {
Expand Down Expand Up @@ -224,7 +224,9 @@ export const Line: React.FC<LineProps> = ({ api, line, age }) => {
if (trainRoutePairs.length === 0) {
return (
<div className="line-pane empty">
<div className="empty-notice">{renderEmptyNoticeForLine(line.name, age)}</div>
<div className="empty-notice">
<EmptyNoticeForLine line={line.name} age={age} />
</div>
</div>
);
}
Expand Down
Loading

0 comments on commit dd7e6de

Please sign in to comment.