Compare commits

...

4 Commits

Author SHA1 Message Date
sam 45472e055f Finalising tweaks before deployment. 2026-06-10 15:40:48 +01:00
sam bd2d0aeee6 Tweaks to colors and padding. 2026-06-10 08:58:38 +01:00
sam 26510d93c7 Improvements to UX, introduced TOC filtering. 2026-06-10 08:50:04 +01:00
sam d84f80a998 Major tweaks, still no API protection, insecure for live. 2026-06-09 17:16:09 +01:00
3 changed files with 224 additions and 21 deletions
+183 -12
View File
@@ -8,6 +8,92 @@
<link rel="icon" type="image/x-icon" href="https://cdn.ballast-data.co.uk/Icon.svg"> <link rel="icon" type="image/x-icon" href="https://cdn.ballast-data.co.uk/Icon.svg">
<style> <style>
html {
display: flex;
min-height: 100vh;
background-color: var(--A1);
}
body {
display: flex;
min-height: 100%;
width: 100%;"
background-color: var(--A2);
padding-left: 18px;
padding-right: 18px;
padding-top: 0px;
padding-bottom: 0px;
margin: 0px;
}
a {
color: var(--A0);
}
.wrapper {
display: flex;
flex-direction: column;
min-height: 100%;
width: 100%;
background-color: var(--A0);
padding: 0px;
margin: 0px;
}
.header {
position: sticky;
top: 0px;
width: fit-content;
min-width: 35em;
color: var(--A0);
background-color: var(--C2);
border: 6px solid var(--C1);
border-radius: 0 0 9px 9px;
padding: 12px;
margin-left: auto;
margin-right: auto;
margin-top: 0px;
}
.main {
width: fit-content;
background-color: white;
border: 9px solid var(--B2);
border-radius: 9px;
padding: 12px;
margin-left: auto;
margin-right: auto;
margin-bottom: 1em;
margin-top: 1em;
}
.branding {
width: fit-content;
border-bottom: 9px solid var(--C1);
margin-left: auto;
margin-right: auto;
}
.query {
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.footer {
display: flex;
width: fit-content;
font-size: 0.8em;
color: var(--A0);
background-color: var(--D2);
border: 6px solid var(--D1);
border-radius: 9px 9px 0 0;
padding: 12px;
margin-left: auto;
margin-right: auto;
margin-top: auto;
}
.level0 { .level0 {
text-align: right; text-align: right;
} }
@@ -16,24 +102,59 @@
.col2 { .col2 {
text-align: left; text-align: left;
} }
input {
font-family: "Merriweather Sans";
font-size: 1em;
border: 3px solid var(--A1);
}
input:hover {
background-color: var(--C2);
}
</style> </style>
<script>
function querySt(attribute) {
url = window.location.search.substring(1);
substrings = url.split('&');
for (i=0;i<substrings.length;i++) {
ft = substrings[i].split('=');
if (ft[0] == attribute) {
return ft[1];
}
}
}
function prefill(attribute) {
var value = querySt(attribute);
value = decodeURIComponent(value);
if(value !== 'undefined'){
document.getElementById(attribute).value = value;
} else {
document.getElementById(attribute).value = '';
}
}
</script>
</head> </head>
<body style="padding: 0px; margin: 0px;"> <body>
<div style="width: 100%; padding: 0px; margin: 0px; background-color: var(--A0);"> <div class="wrapper">
<div style="margin-left: auto; margin-right: auto; width: fit-content; background-color: var(--C2); color: var(--A0); margin-top: 0px; padding: 12px; border-radius: 0 0 9px 9px; border: 6px solid var(--C1)"> <div class="header">
<table> <table>
<tr> <tr>
<td> <td>
<img src="https://cdn.ballast-data.co.uk/Icon-NB.svg"> <a href="/fares"><img src="https://cdn.ballast-data.co.uk/Icon-NB.svg"></a>
</td> </td>
<td> <td>
Ballast-Data Product | Ballast-Data Product <br>
Contact: Non-Doxxing Email Lorum Ipsum <br> <a href="/">Home</a> |
Some Header Content <a href="/fares">Fares</a> |
<a href="/stations">Stations</a> |
<a href="/about">About Us</a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -41,25 +162,75 @@
<br> <br>
<div style="width: fit-content; margin-left: auto; margin-right: auto; border: 9px solid var(--B2); border-radius: 3px; padding: 12px; background-color: white; border-radius: 9px;"> <div class="main">
<div style="width: fit-content; margin-left: auto; margin-right: auto; border-bottom: 9px solid var(--C1);"> <div class="branding">
<br> <br>
<img height=80px src="https://cdn.ballast-data.co.uk/Logo.svg"> <a href="/fares">
<img height=80px src="https://cdn.ballast-data.co.uk/Logo.svg">
</a>
<b style="font-size: 4em; line-height: 1; font-weight: 600;"> Fares </b> <br> <b style="font-size: 4em; line-height: 1; font-weight: 600;"> Fares </b> <br>
</div> </div>
<br> <br>
<div class="query">
<form method="get" action="/fares">
<input
name="origin"
id="origin"
required
autocomplete="on"
placeholder="Origin*"
style="width: 10em;"
/>
<script>prefill('origin')</script>
<input
name="destination"
id="destination"
required
autocomplete="on"
placeholder="Destination*"
style="width: 10em;"
/>
<script>prefill('destination')</script>
<input
name="toc"
id="toc"
autocomplete="on"
placeholder="TOC"
style="width: 5em;"
/>
<script>prefill('toc')</script>
<input type="submit" value="Go!" style="width: 5em;">
</form>
</div>
<br>
<!-- Content --> <!-- Content -->
</div> </div>
<br> <br>
<div style="margin-left: auto; margin-right: auto; width: fit-content; background-color: var(--D2); color: var(--A0); margin-top: 0px; padding: 12px; border-radius: 9px 9px 0 0; border: 6px solid var(--D1)"> <div class="footer">
Open Rail Data Disclaimer Etc. | No Guarantees or Whatever | We do freelance and dont want to be poor. <span>
Powered by National Rail Enquiries. This site is not accredited by National Rail. <br>
We use information from
<a href="https://opendata.nationalrail.co.uk/" target="_blank">National Rail Open Data</a>
and
<a href="https://raildata.org.uk/" target="_blank">Rail Data Marketplace</a>. <br>
This service is not to be used for safety critical purposes. <br>
</span>
</div> </div>
</div> </div>
</body> </body>
</html> </html>
+10 -2
View File
@@ -12,12 +12,20 @@ import json
# Functions # Functions
def fares_query(origin: str, destination: str) -> list[dict[str, dict[str, str]]]: def fares_query(
origin: str,
destination: str,
toc: str | None = None,
) -> list[dict[str, dict[str, str]]]:
url = f"https://fares.ballast-data.co.uk/fares?origin={origin}&destination={destination}"
url += f"&toc={toc.upper()}" if toc is not None else ""
response = requests.get( response = requests.get(
url=f"https://fares.ballast-data.co.uk/fares?origin={origin}&destination={destination}", url=url,
auth=( auth=(
environ.get("BD_FARES_USER", ""), environ.get("BD_FARES_USER", ""),
environ.get("BD_FARES_PASS", ""), environ.get("BD_FARES_PASS", ""),
), ),
) )
if response.status_code != 200:
return []
return json.loads(response.content.decode()) # pyright: ignore[reportAny] return json.loads(response.content.decode()) # pyright: ignore[reportAny]
+31 -7
View File
@@ -35,6 +35,8 @@ COLUMN_RENAMES = {
"restriction_code": "Restriction", "restriction_code": "Restriction",
} }
VALID_QUERY_TERMS = ["origin", "destination", "toc"]
DEFAULT_TEMPLATE_PATH = Path(__file__).parents[2] / "media/template.html" DEFAULT_TEMPLATE_PATH = Path(__file__).parents[2] / "media/template.html"
logger: Logger = Logger(name=__name__, level=INFO) logger: Logger = Logger(name=__name__, level=INFO)
@@ -42,23 +44,42 @@ logger: Logger = Logger(name=__name__, level=INFO)
# Classes # Classes
class FaresHandler(BaseHTTPRequestHandler): class FaresHandler(BaseHTTPRequestHandler):
def parse_url_query(self) -> dict[str, str]: def parse_url_query(self) -> dict[str, str | None]:
text = self.requestline.split(" ")[1].split("?") text = self.requestline.split(" ")[1].split("?")
if len(text) == 1: if len(text) == 1:
return {} return {}
text = text[1] return {
return {s.split("=")[0]: s.split("=")[-1] for s in text.split("&")} _s[0]: _s[-1] if _s[-1] != "" else None
for s in text[1].split("&")
if (_s := s.split("="))[0] in VALID_QUERY_TERMS
}
def content_of_GET(self, template_path: Path = DEFAULT_TEMPLATE_PATH) -> str: def content_of_GET(self, template_path: Path = DEFAULT_TEMPLATE_PATH) -> str:
""" """
Generates the content of a GET request. Generates the content of a GET request.
Responsible for receiving the requests' details and constructing the output. Responsible for receiving the requests' details and constructing the output.
""" """
query_terms = self.parse_url_query()
origin = query_terms.get("origin")
destination = query_terms.get("destination")
if origin is None or destination is None:
text = "Search above including at least Origin and Destination."
with open(template_path, "r") as rf:
return rf.read().replace("<!-- Content -->", text)
assert origin is not None
assert destination is not None
data = fares_query( data = fares_query(
origin=(_d := self.parse_url_query()).get("origin", "NCL"), origin=origin,
destination=_d.get("destination", "NCL"), destination=destination,
toc=query_terms.get("toc"),
) )
text = "<table>" if len(data) == 0:
text = "No Fares Found."
with open(template_path, "r") as rf:
return rf.read().replace("<!-- Content -->", text)
text = '<table style="margin-left: auto; margin-right: auto;">'
text += ( text += (
'<tr style="font-size: 1.6em;"> <th> Flow </th> <th> Fares </th> </tr>\n' '<tr style="font-size: 1.6em;"> <th> Flow </th> <th> Fares </th> </tr>\n'
) )
@@ -101,9 +122,11 @@ class FaresHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
_ = self.wfile.write(content.encode()) _ = self.wfile.write(content.encode())
return None return None
except ConnectionError as ex: except ConnectionError as ex:
logger.warning(f"{type(ex)} | {ex}") logger.warning(f"{type(ex)} | {ex}")
self.send_response(503, "Upstream API refused to respond.") self.send_response(503, "Our upstream API refused to respond.")
except Exception as ex: except Exception as ex:
logger.critical(f"{type(ex)} | {ex}") logger.critical(f"{type(ex)} | {ex}")
self.send_response( self.send_response(
@@ -111,6 +134,7 @@ class FaresHandler(BaseHTTPRequestHandler):
"Something abnormal occured, apologies. It has been logged with importance.", "Something abnormal occured, apologies. It has been logged with importance.",
) )
raise ex raise ex
self.end_headers() self.end_headers()