Restructure into a frontend and a backend

The frontend was basically static files, and there was no convenient means of
testing it locally. Now, the API backend is served off of
http://sky-banana-party.appspot.com/, while the frontend is a bunch of static
files deployed to http://skybanana.party/. This feels cleaner and, vitally,
makes it possible to test frontend-only changes far more conveniently.
This commit is contained in:
Peter Williams 2019-01-23 12:34:09 -05:00
Родитель b611e77b69
Коммит 51625c5c44
18 изменённых файлов: 217 добавлений и 43 удалений

Просмотреть файл

@ -16,16 +16,17 @@ anticipated!
## Technical Overview
The app runs in [Google App Engine](https://cloud.google.com/appengine/),
simply because its a system with which the author was familiar. The live app
backend, implemented in `app/main.py`, is actually relatively simple, and just
provides a small API for retrieving data about LIGO events and their
localizations.
The frontend of the website as seen by users is simple static content thats
stored in `frontend/`. Its very quick to create a WWT control, and then
theres some custom JavaScript to download the LIGO event data and render them
in the WWT framework. The frontend is served off of Google Cloud Storage.
The website as seen by users is mostly simple static content, stored in
`app/static/`. Its very quick to create a WWT control, and then theres some
custom JavaScript to download the LIGO event data and render them in the WWT
framework.
The frontend makes API calls against a backend that provides programmatic
access to a small database. The backend runs in
[Google App Engine](https://cloud.google.com/appengine/), simply because its
a system with which the author was familiar. The main implementation is in
`backend/main.py`. Its relatively simple, and just provides a small API for
retrieving data about LIGO events and their localizations.
The idea is that when LIGO is running, the app will show events as they occur.
But in the meantime, we have to show something. The directory `bootstrap/` has

Просмотреть файл

@ -1,6 +0,0 @@
# The Google App Engine app
This is the python app that has the server-side smarts.
Update the deployed app with `gcloud app deploy`. Update datastore indexes
with `gcloud datastore indexes create index.yaml`.

Просмотреть файл

@ -1,19 +0,0 @@
runtime: python37
# TESTING ONLY: keep static files fresh
default_expiration: "30s"
handlers:
- url: /admin/.*
script: auto
login: admin
- url: /api/.*
script: auto
- url: /static
static_dir: static
- url: /
static_files: static/index.html
upload: static/index.html
- url: /favicon.ico
static_files: static/favicon.ico
upload: static/favicon.ico

Просмотреть файл

0
app/.gitignore → backend/.gitignore поставляемый
Просмотреть файл

9
backend/README.md Normal file
Просмотреть файл

@ -0,0 +1,9 @@
# The Google App Engine backend
This is the backend Python app that has the server-side smarts. It is served
at <http://sky-banana-party.appspot.com/>. The user-facing frontend,
<http://skybanana.party/>, makes various API calls to this backend from
JavaScript.
Update the deployed app with `gcloud app deploy`. Update datastore indexes
with `gcloud datastore indexes create index.yaml`.

8
backend/app.yaml Normal file
Просмотреть файл

@ -0,0 +1,8 @@
runtime: python37
handlers:
- url: /admin/.*
script: auto
login: admin
- url: /api/.*
script: auto

Просмотреть файл

Просмотреть файл

@ -7,7 +7,7 @@ from google.cloud import datastore
import json
import os
from flask import Flask
from flask import Flask, Response
app = Flask(__name__)
@ -100,7 +100,12 @@ def working_set():
)
# TODO: filter by recency
results = list(q.fetch())
return json.dumps(results)
# We need to enable CORS since we're serving from appspot.com to skybanana.party.
resp = Response(json.dumps(results))
resp.headers['Content-Type'] = 'application/json'
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
@app.route('/api/events/<string:ident>/regions')
@ -115,7 +120,12 @@ def regions(ident):
client = datastore.Client()
key = client.key('event', ident)
ent = client.get(key)
return ent['regionjson']
# We need to enable CORS since we're serving from appspot.com to skybanana.party.
resp = Response(ent['regionjson'])
resp.headers['Content-Type'] = 'application/json'
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
if __name__ == '__main__':

Просмотреть файл

Просмотреть файл

@ -155,7 +155,7 @@ def get_events():
def main():
app_dir = bpath(os.pardir, 'app')
backend_dir = bpath(os.pardir, 'backend')
bootstrap_data = {}
for ident, flitem, geojson in get_events():
@ -200,7 +200,7 @@ def main():
result['regions'] = regions
with io.open(os.path.join(app_dir, 'bootstrap_data.json'), 'wt') as f:
with io.open(os.path.join(backend_dir, 'bootstrap_data.json'), 'wt') as f:
json.dump(bootstrap_data, f)

22
frontend/README.md Normal file
Просмотреть файл

@ -0,0 +1,22 @@
# The app frontend
This is the frontend of the app, served as static files off of <http://skybanana.party>.
## Local testing
The recommendend means for local testing is to use Node.js:
```
npx httpserver 8080
```
This server is a bit more sophisticated about MIME types and whatnot than some
other one-liners.
Note that the app hardcodes a reference to the backend URL on
<http://sky-banana-party.appspot.com/>, so it's not super convenient to test
integrated changes to both the frontend and backend.
## Deployment
Documentation TBD.

71
frontend/check-config.py Executable file
Просмотреть файл

@ -0,0 +1,71 @@
#! /usr/bin/env python
# Copyright 2019 Peter Williams
# Licensed under the MIT License
"""Validate the `config.json` file.
This script should be run locally before uploading a new version of the config
file -- it's just a safety check to make sure that we don't accidentally
upload something busted.
"""
# note: Python 2.x compat not tested
from __future__ import absolute_import, division, print_function
import io
import json
import os.path
import time
_frontend_dir = os.path.dirname(__file__)
def frontend_path(*args):
return os.path.join(_frontend_dir, *args)
def die(fmt, *args):
import sys
if len(args):
text = fmt % args
else:
text = str(fmt)
print('error:', text, file=sys.stderr)
sys.exit(1)
def main():
# Maybe the biggest test: is it valid JSON?
with io.open(frontend_path('config.json')) as f:
config = json.load(f)
ligo_mode = config.get('ligo_mode')
if not isinstance(ligo_mode, str):
die('ligo_mode should be a str')
if ligo_mode == "off":
ligo_turn_on_unix_ms = config.get('ligo_turn_on_unix_ms')
if not isinstance(ligo_turn_on_unix_ms, int):
die('if ligo_mode==off, ligo_turn_on_unix_ms should be an int')
if ligo_turn_on_unix_ms < 1548258899000: # 2019 Jan 23 in Unix-time millisecs
die('ligo_turn_on_unix_ms suspiciously small')
if ligo_turn_on_unix_ms > 3294825889900: # 2119 Jan 23 or so
die('ligo_turn_on_unix_ms suspiciously large')
ligo_turn_on_html_frag = config.get('ligo_turn_on_html_frag')
if not isinstance(ligo_turn_on_html_frag, str):
die('if not ligo_running, ligo_turn_on_html_frag should be a str')
else:
die('unexpected value for ligo_mode: %r', ligo_mode)
# If we made it here ...
print('config looks OK')
if __name__ == '__main__':
main()

5
frontend/config.json Normal file
Просмотреть файл

@ -0,0 +1,5 @@
{
"ligo_mode": "off",
"ligo_turn_on_unix_ms": 1551416400000,
"ligo_turn_on_html_frag": "<a href=\"https://www.ligo.caltech.edu/page/observatory-status\">ER14</a>"
}

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 8.8 KiB

После

Ширина:  |  Высота:  |  Размер: 8.8 KiB

Просмотреть файл

@ -5,13 +5,17 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Sky Banana Party!</title>
<link rel="stylesheet" href="static/style.css">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
<script src="https://WorldWideTelescope.github.io/pywwt/wwtsdk.js"></script>
<script src="https://code.jquery.com/jquery-1.8.3.min.js"></script>
<!--[if IE]> <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
</head>
<body>
<div id="wwtcanvas"></div>
<script src="static/script.js"></script>
<div id="maininfo">
<p id="ligostatus"></p>
</div>
<script src="script.js"></script>
</body>
</html>

Просмотреть файл

@ -1,4 +1,6 @@
(function() {
const backend_base = "http://sky-banana-party.appspot.com/api/";
function size_content() {
const new_width = ($("html").width()) + "px";
$("#wwtcanvas").css("width", new_width);
@ -18,6 +20,8 @@
// `wwtlib.WWTControl.scriptInterface`.
var wwt_si = wwtlib.WWTControl.initControlParam("wwtcanvas", true); // "true" => WebGL enabled
wwt_si.add_ready(wwt_ready);
apply_config();
}
$(document).ready(initialize);
@ -64,13 +68,14 @@
//wwt.settings.set_solarSystemPlanets(true);
function setup_bananas(wwt_ctl, wwt_si) {
const wsurl = new URL("api/events/workingset", window.location.href).href;
const wsurl = backend_base + "events/workingset";
$.getJSON(wsurl, function(data) {
var index = 0;
for (const event_info of data) {
const key = event_info.ident;
const regurl = new URL("api/events/" + key + "/regions", window.location.href).href;
const regurl = backend_base + "events/" + key + "/regions";
event_info.index = index;
$.getJSON(regurl, function(regions) { setup_one_event(wwt_ctl, wwt_si, event_info, regions) });
index++;
@ -125,6 +130,37 @@
}
}
function apply_config() {
// Note: at the moment, this is called before the WWT control is
// necessarily set up; would be straightforward to restructure if we
// end up needing access to it.
$.getJSON("config.json").done(function(data) {
var ligo_status_html;
var seconds_until_start;
if (data.ligo_mode == "off") {
seconds_until_start = 0.001 * (data.ligo_turn_on_unix_ms - Date.now());
if (seconds_until_start < 1) {
// The JSON must be stale ...
data.ligo_mode = "on";
// ... handle this more
}
}
if (data.ligo_mode == "off") {
ligo_status_html = "<a href=\"https://www.ligo.caltech.edu/\">LIGO</a> is currently <b>off</b>" +
" — " + data.ligo_turn_on_html_frag + " will begin in " + wait_time_to_text(seconds_until_start) +
"<br>Events from earlier observing runs are shown.";
} else {
ligo_status_html = "<a href=\"https://www.ligo.caltech.edu/\">LIGO</a>s status is <b>unknown</b>";
}
$("#ligostatus").html(ligo_status_html);
});
}
function setup_controls(wwt_ctl, wwt_si) {
// TODO: this code is from pywwt and was designed for use in Jupyter;
// we might be able to do something simpler here.
@ -240,4 +276,20 @@
}
})(true));
}
function wait_time_to_text(seconds) {
if (seconds > 5184000) { // 86400 * 30 * 2
const months = seconds / 2592000;
return months.toFixed(0) + " months";
} else if (seconds > 259200) { // 86400 * 3
const days = seconds / 86400;
return days.toFixed(0) + " days";
} else if (seconds > 7200) { // 2 hours
const hours = seconds / 3600;
return "about " + hours.toFixed(0) + " hours";
} else {
// With timezones, etc., I don't think we'll get more precise than this.
return "just a few hours!";
}
}
})();

Просмотреть файл

@ -3,6 +3,8 @@ html {
margin: 0px;
padding: 0px;
background-color: #000000;
font-size: 1.375em;
font-family: 'Roboto', sans-serif;
}
body {
@ -17,3 +19,18 @@ body {
border-style: none;
border-width: 0px;
}
#maininfo {
position: absolute;
left: 1rem;
top: 1rem;
color: #FFF;
}
p {
margin: 0px 0px 0.1rem 0px;
}
a, a:hover, a:visited {
color: #FFF;
}