Initial Push (v0.0.2)
Signed-off-by: Fred Boniface <fred@fjla.uk>
11
.dockerignore
Normal file
@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
db-manager
|
||||
run.sh
|
||||
LICENSE
|
||||
*.md
|
||||
static
|
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
# REVEALS CREDENTIALS
|
||||
run.sh
|
||||
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
|
1
.test-tools/MULTIPLE-NRCC-MSG_API-EG-cdf.json
Normal file
1
.test-tools/NO-SERVICES_API-EG-pil.json
Normal file
@ -0,0 +1 @@
|
||||
{"GetStationBoardResult":{"generatedAt":"2023-01-14T11:23:12.6558466+00:00","locationName":"Pilning","crs":"PIL","nrccMessages":{"message":"\nPoor weather affecting services in Wales due to flooding on the railway More details can be found in <a href=\"https://t.co/uBU966PUmX\">Latest Travel News</a>."},"platformAvailable":"true"}}
|
1
.test-tools/all.json
Normal file
1
.test-tools/clean.json
Normal file
15
.test-tools/compose.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: mongo:6.0-focal
|
||||
restart: always
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: owl
|
||||
MONGO_INITDB_ROOT_PASSWORD: twittwoo
|
||||
volumes:
|
||||
- mongo-test:/data/db
|
||||
ports:
|
||||
- 27017:27017
|
||||
volumes:
|
||||
mongo-test:
|
33
.test-tools/ferry-vc.json
Normal file
@ -0,0 +1,33 @@
|
||||
{"service":
|
||||
[
|
||||
{"sta":"16:07",
|
||||
"eta":"On time",
|
||||
"operator":"South Western Railway",
|
||||
"operatorCode":"SW",
|
||||
"serviceType":"ferry",
|
||||
"serviceID":"37782PHBR____",
|
||||
"origin":
|
||||
{"location":
|
||||
{"locationName":
|
||||
"Ryde Pier Head","crs":"RYP"
|
||||
}
|
||||
},
|
||||
"destination":
|
||||
{"location":
|
||||
{"locationName":"Portsmouth Harbour",
|
||||
"crs":"PMH"
|
||||
}
|
||||
},
|
||||
"previousCallingPoints":
|
||||
{"callingPointList":
|
||||
{"callingPoint":
|
||||
{"locationName":"Ryde Pier Head",
|
||||
"crs":"RYP",
|
||||
"st":"15:45",
|
||||
"et":"On time"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{"std":"16:15","etd":"On time","operator":"South Western Railway","operatorCode":"SW","serviceType":"ferry","serviceID":"37746PHBR____","origin":{"location":{"locationName":"Portsmouth Harbour","crs":"PMH"}},"destination":{"location":{"locationName":"Ryde Pier Head","crs":"RYP"}},"subsequentCallingPoints":{"callingPointList":{"callingPoint":
|
||||
{"locationName":"Ryde Pier Head","crs":"RYP","st":"16:37","et":"On time"}}}}]}
|
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"git.autofetch": "all",
|
||||
"git.alwaysSignOff": true,
|
||||
"git.enableCommitSigning": false,
|
||||
"git.fetchOnPull": true,
|
||||
"git.pullBeforeCheckout": true
|
||||
}
|
12
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: start",
|
||||
"detail": "node app.js"
|
||||
}
|
||||
]
|
||||
}
|
7
Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM node:19
|
||||
EXPOSE 8460
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
CMD [ "node", "app.js" ]
|
105
README.md
@ -1,3 +1,104 @@
|
||||
# server
|
||||
# OwlBoard
|
||||
|
||||
The OwlBoard Node.JS Server
|
||||
OwlBoard is both a backend API, and a frontend Arrival/Departure board webapp.
|
||||
|
||||
Powered by Node.JS and using the ldbs-json module, the OwlBoard API provides up to date train departure information for any station in the UK.
|
||||
|
||||
Whilst the application is open source, the webservice (owlboard.fb-infra.uk) is not openly available. National Rail Enquiries have limits on API access so to use this software yourself, you'll need to run your own instance after obtaining your own API key.
|
||||
|
||||
The webservice (owlboard.fb-infra.uk) may contain ads to support the running of the service,
|
||||
if ads are implemented, I intend to avoid 'dubious' advertisers that target and track users.
|
||||
|
||||
Currently only the public API is available as I am currently unable to request a key for the staff version.
|
||||
|
||||
## Requirements:
|
||||
|
||||
To run this server you will need:
|
||||
- Docker or Kubernetes
|
||||
|
||||
## WebApp Colours:
|
||||
|
||||
- See CSS Variables
|
||||
|
||||
## API Endpoints:
|
||||
- /api/v1:
|
||||
|
||||
- /list:
|
||||
- /stations:
|
||||
- GET: Get list of stations
|
||||
- Authenticated: No
|
||||
- Returns JSON: `{"STATION NAME":{"CRS":"code","TIPLOC":"code"}}`
|
||||
|
||||
- /corpus:
|
||||
- GET: Get full CORPUS Data
|
||||
- Authenticated: No
|
||||
- Returns JSON in original CORPUS format minus any blank values.
|
||||
|
||||
- /ldb:
|
||||
- /{crs}:
|
||||
- GET: Get arrival/departure board for {crs}
|
||||
- Authenticated: No
|
||||
- Returns JSON: Formatted as per ldbs-json module.
|
||||
|
||||
- /gitea:
|
||||
- POST: Post issue to Gitea Repo
|
||||
- Authenticated: Yes
|
||||
- Not yet implemented, submit issues at https://git.fjla.uk/fred.boniface/owlboard
|
||||
|
||||
- /kube:
|
||||
- /alive:
|
||||
- GET: Check alive
|
||||
- Authenticated: No
|
||||
- Returns JSON: `{"status":"alive"}`
|
||||
|
||||
- /ready:
|
||||
- GET: Check ready
|
||||
- Authenticated: No
|
||||
- Returns JSON: `{"state":""}` ready or not_ready.
|
||||
|
||||
## Stack:
|
||||
- app.js -> Launches server, Entry Point, defines routers and middlewares.
|
||||
- routes -> Routers - Directs requests to controllers.
|
||||
- controllers -> Checks auth, sends response. Request doesn't pass further.
|
||||
- services -> Provide data and do tasks, uses other services and utils.
|
||||
|
||||
- utils -> Provide utility functions that can be called by services.
|
||||
- configs -> Provide configuration details for other files.
|
||||
- static -> Holds files for static service, should be hosted behind a caching proxy.
|
||||
|
||||
## Configuration:
|
||||
The app is designed to be run within Kubernetes or within a Docker container, as such configuration is provided with environment variables. See the variable name and default options below. If a required configuration is not present the program will exit when that feature is initialised.
|
||||
|
||||
|VAR|DEFAULT|REQUIRED|PURPOSE|
|
||||
|:-:|:-----:|:------:|:-----:|
|
||||
|OWL_SRV_PORT|8460|NO|Web Server Port|
|
||||
|OWL_SRV_LISTEN|0.0.0.0|NO|Web Server Listen Address|
|
||||
|OWL_DB_USER|owl|NO|Database Username|
|
||||
|OWL_DB_PASS|twittwoo|NO|Database Password - Do not leave as default in production|
|
||||
|OWL_DB_NAME|owlboard|NO|Database Name|
|
||||
|OWL_DB_PORT|27017|NO|Database Server Port|
|
||||
|OWL_DB_HOST|localhost|NO|Database Server Host|
|
||||
|OWL_LDB_KEY||YES|National Rail LDBWS API Key|
|
||||
|OWL_LDB_SVKEY||NO|National Rail LDBSVWS API Key|
|
||||
|OWL_LDB_CORPUSUSER||YES|Network Rail CORPUS API Username|
|
||||
|OWL_LDB_CORPUSPASS||YES|Network Rail CORPUS API Password|
|
||||
|OWL_GIT_ISSUEBOT||NO|Gitea API Key for issue reporting|
|
||||
|OWL_GIT_APIENDPOINT||NO|Gitea API Endpoint|
|
||||
|
||||
In the case that OWL_LDB_SVKEY is not available, staff versions of departure board, etc. will not be available.
|
||||
|
||||
In the case that OWL_GIT_ISSUEBOT is not available, the 'Report Issue' page will not be able to POST data.
|
||||
|
||||
## Database Layout
|
||||
|
||||
The OwlBoard application will build the database if required at startup. All it needs is authentication details for a MongoDB server.
|
||||
|
||||
### Collections
|
||||
|
||||
|Collection|Contents|Purpose|
|
||||
|:--------:|:------:|:-----:|
|
||||
|corpus|Raw CORPUS data with blank keys removed|Code lookups|
|
||||
|stations|Cleaned CORPUS Data, any objects with blank 3ALPHA & STANOX fields are removed|Validation before fetching Arr/Dep boards|
|
||||
|meta|Lists the update time of corpus and station data|Will be used to update after a predetermined time period|
|
||||
|
||||
Note that even after removing all objects from the CORPUS with a blank 3ALPHA & STANOX, many items remain which are not stations and will not have a board available. Going forwards methods to remove non-stations from this data will be introduced.
|
40
UpNext.md
Normal file
@ -0,0 +1,40 @@
|
||||
# What to do next:
|
||||
|
||||
## Frontend:
|
||||
|
||||
* Enable text search for `locationName` on find-code page.
|
||||
* Add security headers - maybe on ingress controller?
|
||||
- see: https://webera.blog/improving-your-website-security-with-http-headers-in-nginx-ingress-369e8f3302cc
|
||||
* Replace close and menu icons with SVG
|
||||
* Service detail page needs style adjustments, the lines overflow on small screens
|
||||
|
||||
### In Progress:
|
||||
|
||||
|
||||
|
||||
### Completed - Testing:
|
||||
|
||||
* Write service worker for full PWA experience.
|
||||
* Implement error pages.
|
||||
* Issue page: Submit using API.
|
||||
* Issue page: Collect diagnostics such as browser features etc.
|
||||
* Add sanitizing to Gitea Issue API, currently considered to be unsafe.
|
||||
* Add Gitea Issue API
|
||||
* Issue page: Check for success and then redirect to /.
|
||||
* Add success test for Gitea Issue API and send the result onto the client.
|
||||
* DB Indexes:
|
||||
- "stations": 3ALPHA, STANOX, TIPLOC
|
||||
- "corpus": 3ALPHA, NLC
|
||||
* DB Indexes:.
|
||||
- "corpus": NLCDESC(TEXT)
|
||||
* Build metrics page
|
||||
* Responsive text sizes for boards.
|
||||
* Undo changed to make everything an array - frontend code to handle this.
|
||||
* Explore compression of API Responses
|
||||
|
||||
## Backend:
|
||||
|
||||
* Rewrite sanitizing functions to remove external dependancy.
|
||||
* DB: Count document creation, should only add date if doesn't already exist.
|
||||
- Then the count doesn't need clearing at each start.
|
||||
- Currently commented out the upsert of the date. This will only work on existing databases.
|
75
app.js
Normal file
@ -0,0 +1,75 @@
|
||||
// OwlBoard - © Fred Boniface 2022 - Licensed under GPLv3 (or later)
|
||||
|
||||
// Please see the included LICENSE file. Statically served fonts are
|
||||
// licensed separately, each folder contains a license file where a
|
||||
// different license applies.
|
||||
|
||||
// While the Node app can serve static files, in production a separate
|
||||
// container should be used for this. See the dockerfile under /static
|
||||
// for this.
|
||||
|
||||
// External Requires
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const compression = require('compression')
|
||||
|
||||
// Internal Requires
|
||||
const log = require('./src/utils/log.utils'); // Log Helper
|
||||
const version = require('./src/configs/version.configs'); // Version Strings
|
||||
const listRtr = require('./src/routes/list.routes'); // /list endpoints
|
||||
const ldbRtr = require('./src/routes/ldb.routes'); // /ldb endpoints
|
||||
const kubeRtr = require('./src/routes/kube.routes'); // /kube endpoints
|
||||
const findRtr = require('./src/routes/find.routes'); // /find endpoints
|
||||
const issueRtr = require('./src/routes/issue.routes') // /issue endpoints
|
||||
const statRtr = require('./src/routes/stats.routes'); // /stat endpoints
|
||||
const initDb = require('./src/utils/dbinit.utils'); // DB Init Utility
|
||||
|
||||
// Set Server Configurations
|
||||
const srvListen = process.env.OWL_SRV_LISTEN || "0.0.0.0"
|
||||
const srvPort = process.env.OWL_SRV_PORT || 8460
|
||||
|
||||
// Print version number:
|
||||
log.out(`app: Starting OwlBoard - Backend Version: ${version.app} - API versions: ${version.api}`);
|
||||
|
||||
// Test for required vars:
|
||||
// const varTest = require('./src/utils/varTest.utils');
|
||||
// var startTest = await varTest.varTest();
|
||||
//console.log("Required Vars Missing:", startTest.missing_required);
|
||||
//console.log("Desired Vars Missing:", startTest.missing_desired);
|
||||
// if startTest.pass == false
|
||||
// console.log("Unable to start, missing required vars")
|
||||
// exit app
|
||||
|
||||
// DB Init
|
||||
initDb.init();
|
||||
|
||||
// Express Error Handling:
|
||||
app.use((err, req, res, next) => {
|
||||
const statusCode = err.statuscode || 500;
|
||||
console.error(err.message, err.stack);
|
||||
res.status(statusCode).json({'message': err.message});
|
||||
return;
|
||||
});
|
||||
|
||||
// Express Submodules:
|
||||
app.use(express.json()); //JSON Parsing for POST Requests
|
||||
app.use(express.static('static')); //Serve static content from /static
|
||||
app.use(compression())
|
||||
|
||||
// Express Routes
|
||||
app.use('/api/v1/list', listRtr);
|
||||
app.use('/api/v1/ldb', ldbRtr);
|
||||
app.use('/api/v1/kube', kubeRtr);
|
||||
app.use('/api/v1/find', findRtr);
|
||||
app.use('/api/v1/issue', issueRtr);
|
||||
app.use('/api/v1/stats', statRtr)
|
||||
|
||||
// Start Express
|
||||
app.listen(srvPort, srvListen, (error) =>{
|
||||
if(!error) {
|
||||
log.out(`app.listen: Listening on http://${srvListen}:${srvPort}`);
|
||||
log.out("app.listen: State - alive")
|
||||
} else {
|
||||
log.out("app.listen: Error occurred, server can't start", error);
|
||||
}
|
||||
});
|
3778
package-lock.json
generated
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.2.1",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"ldbs-json": "^1.2.1",
|
||||
"mongodb": "^4.13.0",
|
||||
"node-gzip": "^1.1.2",
|
||||
"string-sanitizer-fix": "^2.0.1"
|
||||
},
|
||||
"name": "owlboard",
|
||||
"description": "OwlBoard is an API and PWA for live rail departure board in the UK.",
|
||||
"version": "0.0.1",
|
||||
"main": "express.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node app.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.fjla.uk/fred.boniface/owlboard.git"
|
||||
},
|
||||
"author": "Fred Boniface",
|
||||
"license": "GPL-3.0-or-later"
|
||||
}
|
10
src/configs/domains.configs.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = valid
|
||||
|
||||
const valid = [
|
||||
"owlboard.co.uk",
|
||||
"fjla.uk",
|
||||
"gwr.com",
|
||||
"swrailway.com",
|
||||
"firstrail.com",
|
||||
"networkrail.co.uk"
|
||||
]
|
6
src/configs/version.configs.js
Normal file
@ -0,0 +1,6 @@
|
||||
const version = {
|
||||
api: ["/api/v1/",],
|
||||
app: "0.0.2"
|
||||
};
|
||||
|
||||
module.exports = version;
|
58
src/controllers/find.controllers.js
Normal file
@ -0,0 +1,58 @@
|
||||
const find = require('../services/find.services');
|
||||
|
||||
async function findName(req, res, next){
|
||||
try {
|
||||
var id = req.params.id
|
||||
res.json(await find.name(id))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function findCrs(req, res, next){
|
||||
try {
|
||||
var id = req.params.id
|
||||
res.json(await find.crs(id))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function findNlc(req, res, next){
|
||||
try {
|
||||
var id = req.params.id
|
||||
res.json(await find.nlc(id))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function findTiploc(req, res, next){
|
||||
try {
|
||||
var id = req.params.id
|
||||
res.json(await find.tiploc(id))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function findStanox(req, res, next){
|
||||
try {
|
||||
var id = req.params.id
|
||||
res.json(await find.stanox(id))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
module.exports = {
|
||||
findName,
|
||||
findCrs,
|
||||
findNlc,
|
||||
findTiploc,
|
||||
findStanox
|
||||
}
|
14
src/controllers/issue.controllers.js
Normal file
@ -0,0 +1,14 @@
|
||||
const issue = require('../services/issue.services');
|
||||
|
||||
async function post(req, res, next){
|
||||
try {
|
||||
res.json(await issue.processor(req.body))
|
||||
} catch (err) {
|
||||
console.error(`Controller Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
post
|
||||
}
|
34
src/controllers/kube.controllers.js
Normal file
@ -0,0 +1,34 @@
|
||||
const kube = require('../services/kube.services');
|
||||
|
||||
async function getAlive(req, res, next){
|
||||
try {
|
||||
var state = kube.getAlive()
|
||||
res.status((await state).code).send((await state).state)
|
||||
} catch (err) {
|
||||
res.status("503").send({state: "error"})
|
||||
}
|
||||
}
|
||||
|
||||
async function getReady(req, res, next){
|
||||
try {
|
||||
res.json(await kube.getReady(req.body))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getTime(req, res, next){
|
||||
try {
|
||||
res.json(await kube.getTime(req.body))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAlive,
|
||||
getReady,
|
||||
getTime
|
||||
}
|
15
src/controllers/ldb.controllers.js
Normal file
@ -0,0 +1,15 @@
|
||||
const ldb = require('../services/ldb.services');
|
||||
|
||||
async function get(req, res, next){
|
||||
try {
|
||||
var id = req.params.id
|
||||
res.json(await ldb.get(req.body, id))
|
||||
} catch (err) {
|
||||
console.error(`Unknown Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get
|
||||
}
|
34
src/controllers/list.controllers.js
Normal file
@ -0,0 +1,34 @@
|
||||
const list = require('../services/list.services');
|
||||
|
||||
async function getStations(req, res, next){
|
||||
try {
|
||||
res.json(await list.getStations(req.body))
|
||||
} catch (err) {
|
||||
console.error(`Controller Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCorpus(req, res, next){
|
||||
try {
|
||||
res.json(await list.getCorpus(req.body))
|
||||
} catch (err) {
|
||||
console.error(`Controller Error`, err.message);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function hits(req, res, next) {
|
||||
try {
|
||||
res.json(await list.hits())
|
||||
} catch (err) {
|
||||
console.error(`Controller Error`, err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getStations,
|
||||
getCorpus,
|
||||
hits
|
||||
}
|
13
src/controllers/stats.controllers.js
Normal file
@ -0,0 +1,13 @@
|
||||
const stat = require('../services/stats.services');
|
||||
|
||||
async function get(req, res, next) {
|
||||
try {
|
||||
res.json(await stat.hits())
|
||||
} catch (err) {
|
||||
console.error(`Controller Error`, err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get}
|
23
src/routes/find.routes.js
Normal file
@ -0,0 +1,23 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const findController = require('../controllers/find.controllers');
|
||||
|
||||
/* GET programming languages. */
|
||||
//router.get('/', programmingLanguagesController.get);
|
||||
|
||||
/* POST programming language */
|
||||
//router.post('/', programmingLanguagesController.create);
|
||||
|
||||
/* PUT programming language */
|
||||
//router.put('/:id', programmingLanguagesController.update);
|
||||
|
||||
/* DELETE programming language */
|
||||
//router.delete('/:id', programmingLanguagesController.remove);
|
||||
|
||||
router.get('/name/:id', findController.findName);
|
||||
router.get('/crs/:id', findController.findCrs);
|
||||
router.get('/nlc/:id', findController.findNlc);
|
||||
router.get('/tiploc/:id', findController.findTiploc);
|
||||
router.get('/stanox/:id', findController.findStanox);
|
||||
|
||||
module.exports = router;
|
7
src/routes/issue.routes.js
Normal file
@ -0,0 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const issueController = require('../controllers/issue.controllers');
|
||||
|
||||
router.post('/', issueController.post);
|
||||
|
||||
module.exports = router;
|
9
src/routes/kube.routes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const kubeController = require('../controllers/kube.controllers');
|
||||
|
||||
router.get('/alive', kubeController.getAlive);
|
||||
router.get('/ready', kubeController.getReady);
|
||||
router.get('/time', kubeController.getTime);
|
||||
|
||||
module.exports = router
|
19
src/routes/ldb.routes.js
Normal file
@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ldbController = require('../controllers/ldb.controllers');
|
||||
|
||||
/* GET programming languages. */
|
||||
//router.get('/', programmingLanguagesController.get);
|
||||
|
||||
/* POST programming language */
|
||||
//router.post('/', programmingLanguagesController.create);
|
||||
|
||||
/* PUT programming language */
|
||||
//router.put('/:id', programmingLanguagesController.update);
|
||||
|
||||
/* DELETE programming language */
|
||||
//router.delete('/:id', programmingLanguagesController.remove);
|
||||
|
||||
router.get('/:id', ldbController.get);
|
||||
|
||||
module.exports = router;
|
20
src/routes/list.routes.js
Normal file
@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const listController = require('../controllers/list.controllers');
|
||||
|
||||
/* GET programming languages. */
|
||||
//router.get('/', programmingLanguagesController.get);
|
||||
|
||||
/* POST programming language */
|
||||
//router.post('/', programmingLanguagesController.create);
|
||||
|
||||
/* PUT programming language */
|
||||
//router.put('/:id', programmingLanguagesController.update);
|
||||
|
||||
/* DELETE programming language */
|
||||
//router.delete('/:id', programmingLanguagesController.remove);
|
||||
|
||||
router.get('/stations', listController.getStations);
|
||||
router.get('/corpus', listController.getCorpus);
|
||||
|
||||
module.exports = router;
|
8
src/routes/stats.routes.js
Normal file
@ -0,0 +1,8 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const statsController = require('../controllers/stats.controllers');
|
||||
|
||||
|
||||
router.get('/', statsController.get);
|
||||
|
||||
module.exports = router;
|
73
src/services/corpus.services.js
Normal file
@ -0,0 +1,73 @@
|
||||
// Get CORPUS data from Network Rail and format the data for OwlBoard
|
||||
|
||||
// Network Rail Datafeed user and pass must be stored in `/srv/keys/owlboard/keys.config.js`
|
||||
|
||||
// FUNCTIONS/
|
||||
// initSubset() : Exported: Uses the internal functions to return a clean CORPUS object.
|
||||
// initAll() : Exported: Uses the internal functions to return a full CORPUS object.
|
||||
// get() : Get the CORPUS data from Network Rail as a gzip file.
|
||||
// extract() : Extract the CORPUS JSON file from the GZIP file.
|
||||
// clean() : Cleans the CORPUS data, removing unneccesary non-stations from the data.
|
||||
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
|
||||
const axios = require('axios')
|
||||
const gz = require('node-gzip')
|
||||
|
||||
const corpusUser = process.env.OWL_LDB_CORPUSUSER
|
||||
const corpusPass = process.env.OWL_LDB_CORPUSPASS
|
||||
|
||||
async function subset(allCorpus) {
|
||||
return (await clean(allCorpus))
|
||||
}
|
||||
|
||||
async function get() {
|
||||
var gzipData = await fetch()
|
||||
return (await extract(gzipData))
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
log.out("corpus.fetch: Fetching CORPUS Data from Network Rail")
|
||||
authHead = Buffer.from(`${corpusUser}:${corpusPass}`).toString('base64')
|
||||
const url = 'https://publicdatafeeds.networkrail.co.uk/ntrod/SupportingFileAuthenticate?type=CORPUS'
|
||||
const options = {
|
||||
method: 'get',
|
||||
timeout: 20000,
|
||||
headers: {'Authorization': `Basic ${authHead}`},
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
try {
|
||||
var { data } = await axios.get(url, options)
|
||||
log.out("corpus.fetch: CORPUS Data fetched")
|
||||
} catch (error) {
|
||||
log.out("corpus.fetch: Error fetching CORPUS")
|
||||
log.out(error)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
async function extract(input) {
|
||||
log.out(`corpus.extract: Extracting CORPUS archive`)
|
||||
var raw = await gz.ungzip(input)
|
||||
var obj = await JSON.parse(raw)
|
||||
return (obj.TIPLOCDATA)
|
||||
}
|
||||
|
||||
async function clean(input) {
|
||||
log.out(`corpus.clean: Removing non-stations from CORPUS data`)
|
||||
let clean = [];
|
||||
for (const element of input) {
|
||||
if (element.STANOX != ' ' && element['3ALPHA'] != ' '){
|
||||
delete(element.UIC);
|
||||
delete(element.NLCDESC16);
|
||||
delete(element.NLC);
|
||||
clean.push(element);
|
||||
}
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get,
|
||||
subset
|
||||
}
|
131
src/services/dbAccess.services.js
Normal file
@ -0,0 +1,131 @@
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
|
||||
const dbUser = process.env.OWL_DB_USER || "owl"
|
||||
const dbPass = process.env.OWL_DB_PASS || "twittwoo"
|
||||
const dbName = process.env.OWL_DB_NAME || "owlboard"
|
||||
const dbPort = process.env.OWL_DB_PORT || 27017
|
||||
const dbHost = process.env.OWL_DB_HOST || "localhost"
|
||||
const uri = `mongodb://${dbUser}:${dbPass}@${dbHost}:${dbPort}`;
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
const client = new MongoClient(uri);
|
||||
const db = client.db(dbName);
|
||||
|
||||
async function dropCollection(coll){
|
||||
await client.connect();
|
||||
|
||||
// check if collection contains any documents, if it doesn't, it is either empty or non-existent - it doesn't need dropping.
|
||||
var collection = db.collection(coll);
|
||||
var count = await collection.countDocuments();
|
||||
log.out(`DbAccess.dropCollection: Collection '${coll}' contains ${count} documents`)
|
||||
if (count == 0) {
|
||||
log.out(`DbAccess.dropCollection: Collection '${coll}' is empty. Do not need to drop`)
|
||||
} else {
|
||||
log.out(`DbAccess.dropCollection: dropping collection: '${coll}'`)
|
||||
db.dropCollection(coll);
|
||||
log.out(`DbAccess.dropCollection: dropped collection: '${coll}'`)
|
||||
}
|
||||
}
|
||||
|
||||
async function putCorpus(data){
|
||||
log.out("DbAccess.putCorpus: Uploading CORPUS data to database")
|
||||
await client.connect();
|
||||
try {
|
||||
var coll = db.collection("corpus");
|
||||
await coll.insertMany(data);
|
||||
} catch (error) {
|
||||
log.out("DbAccess.putCorpus: Error uploading Corpus data to database")
|
||||
log.out(error)
|
||||
}
|
||||
};
|
||||
|
||||
async function putStations(data){
|
||||
log.out("DbAccess.putStations: Uploading Stations data to database")
|
||||
await client.connect();
|
||||
try {
|
||||
var coll = db.collection("stations");
|
||||
coll.insertMany(data);
|
||||
} catch (error) {
|
||||
log.out("DbAccess.putStations: Error uploading Stations data to database")
|
||||
log.out(error)
|
||||
}
|
||||
};
|
||||
|
||||
async function updateMeta(type, target, unixTime){
|
||||
await client.connect();
|
||||
var coll = db.collection("meta");
|
||||
var filter = {type: type, target: target};
|
||||
var update = {$set:{updated: unixTime}};
|
||||
var options = {upsert: true}; // If document isn't present will insert.
|
||||
try {
|
||||
var result = await coll.updateOne(filter,update,options)
|
||||
log.out(`dbAccessServices.updateMeta: ${JSON.stringify(result)}`)
|
||||
log.out(`dbAccessServices.updateMeta: meta for '${target}' updated`)
|
||||
} catch (err) {
|
||||
log.out(`dbAccessServices.updateMeta: Unable to update meta for '${target}'`)
|
||||
log.out(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function query(collection, query){
|
||||
await client.connect();
|
||||
log.out(`dbAccess.query: Connecting to collection: '${collection}'`)
|
||||
var qcoll = db.collection(collection);
|
||||
var qcursor = qcoll.find(query)
|
||||
qcursor.project({_id: 0})
|
||||
log.out(`dbAccess.query: Running Query: ${JSON.stringify(query)}`)
|
||||
increment(collection)
|
||||
return (await qcursor.toArray());
|
||||
}
|
||||
|
||||
async function ensureIndex(col, field, text) {
|
||||
await client.connect();
|
||||
if (!text) {
|
||||
log.out(`dbAccess.ensureIndex: Creating index in collection ${col} for field ${field}`)
|
||||
db.createIndex(col, field);
|
||||
} else {
|
||||
log.out(`dbAccess.ensureIndex: Creating text index in collection ${col} for field ${field}`)
|
||||
let idx = {}
|
||||
idx[field] = "text";
|
||||
db.createIndex(col, idx);
|
||||
}
|
||||
log.out(`dbAccess.ensureIndex: Index created`);
|
||||
return;
|
||||
}
|
||||
|
||||
async function increment(target) {
|
||||
await client.connect();
|
||||
let col = db.collection("meta");
|
||||
let update = {}
|
||||
update[target] = 1
|
||||
col.updateOne({target: "counters"}, {$inc:update})
|
||||
return;
|
||||
}
|
||||
|
||||
async function createCount() {
|
||||
await client.connect();
|
||||
let col = db.collection("meta");
|
||||
var filter = {type: "count", target: "counters"};
|
||||
var update = {$set:{/*since: new Date,*/ type: "count", target: "counters"}};
|
||||
var options = {upsert: true}; // If document isn't present will insert.
|
||||
try {
|
||||
var result = await col.updateOne(filter,update,options)
|
||||
log.out(`dbAccessServices.updateMeta: ${JSON.stringify(result)}`)
|
||||
log.out(`dbAccessServices.updateMeta: count meta added updated`)
|
||||
} catch (err) {
|
||||
log.out(`dbAccessServices.updateMeta: Unable to add count`)
|
||||
log.out(err)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
putCorpus,
|
||||
putStations,
|
||||
dropCollection,
|
||||
updateMeta,
|
||||
query,
|
||||
ensureIndex,
|
||||
increment,
|
||||
createCount
|
||||
}
|
59
src/services/find.services.js
Normal file
@ -0,0 +1,59 @@
|
||||
// Parse and return a find request
|
||||
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
const db = require('../services/dbAccess.services');
|
||||
const san = require('../utils/sanitizer.utils')
|
||||
|
||||
// DB Query: query(collection, query)
|
||||
|
||||
// Define collection as all queries are for the "corpus" collection.
|
||||
const col = "corpus"
|
||||
|
||||
async function name(id){
|
||||
log.out(`findServices.name: Finding station name: ${id}`)
|
||||
var name = san.cleanApiEndpointTxt(id.toUpperCase())
|
||||
query = {NLCDESC: name}
|
||||
var data = await db.query(col,query)
|
||||
return data
|
||||
}
|
||||
|
||||
async function crs(id){
|
||||
log.out(`findServices.crs: Finding crs: ${id}`)
|
||||
var crs = san.cleanApiEndpointTxt(id.toUpperCase())
|
||||
query = {'3ALPHA': crs}
|
||||
var data = await db.query(col,query)
|
||||
return data
|
||||
}
|
||||
|
||||
async function nlc(id){
|
||||
log.out(`findServices.nlc: Finding nlc: ${id}`)
|
||||
var nlc = san.cleanApiEndpointNum(id)
|
||||
query = {NLC: parseInt(nlc)}
|
||||
log.out(`findServices.nlc: NLC Converted to int: ${query}`)
|
||||
var data = await db.query(col,query)
|
||||
return data
|
||||
}
|
||||
|
||||
async function tiploc(id){
|
||||
log.out(`findServices.tiploc: Finding tiploc: ${id}`)
|
||||
var tiploc = san.cleanApiEndpointTxt(id.toUpperCase())
|
||||
query = {TIPLOC: tiploc}
|
||||
var data = await db.query(col,query)
|
||||
return data
|
||||
}
|
||||
|
||||
async function stanox(id){
|
||||
log.out(`findServices.stanox: Finding stanox: ${id}`)
|
||||
var stanox = san.cleanApiEndpointNum(id)
|
||||
query = {STANOX: String(stanox)}
|
||||
var data = await db.query(col,query)
|
||||
return data
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name,
|
||||
crs,
|
||||
nlc,
|
||||
tiploc,
|
||||
stanox
|
||||
}
|
33
src/services/issue.services.js
Normal file
@ -0,0 +1,33 @@
|
||||
const axios = require('axios')
|
||||
const log = require('../utils/log.utils')
|
||||
|
||||
async function processor(data) {
|
||||
log.out(`issueService.processor: Issue received`)
|
||||
let out = {}
|
||||
out.title = data.subject.replace(/<[^>]+>|[\*\$]/g, '');
|
||||
out.body = data.msg.replace(/<[^>]+>|[\*\$]/g, '')
|
||||
sendToGitea(out);
|
||||
}
|
||||
|
||||
async function sendToGitea(body) {
|
||||
let key = process.env.OWL_GIT_ISSUEBOT
|
||||
let url = process.env.OWL_GIT_APIENDPOINT
|
||||
let opts = {
|
||||
headers: {
|
||||
Authorization: key
|
||||
}
|
||||
}
|
||||
var res = await axios.post(url, body, opts)
|
||||
// Need to read the output from the POST and pass the result upwards to the client.
|
||||
if (res.status == 201) {
|
||||
log.out("issueService.sendToGitea: Issue sent to Gitea")
|
||||
return {status: res.status,message:"issue created"}
|
||||
} else {
|
||||
log.out("issueService.sendToGitea: Failed to send issue to Gitea")
|
||||
return {status: res.status,message:"issue not created"}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processor
|
||||
}
|
20
src/services/kube.services.js
Normal file
@ -0,0 +1,20 @@
|
||||
async function getAlive(){
|
||||
log.out(`kubeServices.getAlive: alive hook checked`)
|
||||
return {code: 200, state: {state: "alive",noise: "twit-twoo"}}
|
||||
}
|
||||
|
||||
async function getReady(){
|
||||
log.out(`kubeServices.getReady: ready hook checked`)
|
||||
return "not_implemented";
|
||||
};
|
||||
|
||||
async function getTime(){
|
||||
var now = new Date()
|
||||
return {responseGenerated: now}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAlive,
|
||||
getReady,
|
||||
getTime
|
||||
}
|
52
src/services/ldb.services.js
Normal file
@ -0,0 +1,52 @@
|
||||
// Parse and return an LDB Request
|
||||
|
||||
// FUNCTIONS
|
||||
// post(body, id): Exported:
|
||||
// body: [req.body from controller]
|
||||
// id : [req.params.id from controller - this is expected to be CRS or TIPLOC]
|
||||
|
||||
// convertTiploc(TIPLOC) : Exported: Looks up CRS, Name & STANOX for Tiploc
|
||||
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
const ldb = require('ldbs-json')
|
||||
const util = require('../utils/ldb.utils')
|
||||
const san = require('../utils/sanitizer.utils')
|
||||
const db = require('../services/dbAccess.services')
|
||||
|
||||
const ldbKey = process.env.OWL_LDB_KEY
|
||||
const ldbsvKey = process.env.OWL_LDB_SVKEY
|
||||
|
||||
async function get(body, id){
|
||||
var cleanId = san.cleanApiEndpointTxt(id);
|
||||
var obj = await util.checkCrs(cleanId);
|
||||
try {
|
||||
var crs = obj[0]['3ALPHA'];
|
||||
log.out(`ldbService.get: Determined CRS for lookup to be: ${crs}`);
|
||||
var data = await arrDepBoard(crs);
|
||||
db.increment("ldbws") // Need to add creation of this document to the database. >> {type:"count",counting:"api_hit",target:"ldbws",since:"DATE"}
|
||||
} catch (err) {
|
||||
log.out(`ldbService.get: Error, Unable to find CRS: ${err}`)
|
||||
var data = {ERROR:'NOT_FOUND',description:'The entered station was not found. Please check and try again.'};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function arrDepBoard(CRS){
|
||||
log.out(`ldbService.arrDepBoard: Trying to fetch ArrDep Board for ${CRS}`)
|
||||
try {
|
||||
var options = {
|
||||
numRows: 10,
|
||||
crs: CRS.toUpperCase()
|
||||
}
|
||||
var api = new ldb(ldbKey,false)
|
||||
var reply = await api.call("GetArrDepBoardWithDetails",options)
|
||||
return reply
|
||||
} catch (err) {
|
||||
log.out(`ldbService.arrDepBoard: Lookup Failed for: ${CRS}`)
|
||||
return {GetStationBoardResult: "not available", Reason: `The CRS code ${CRS} is not valid`, Why: `Sometimes a station will have more than one CRS - for example Filton Abbey Wood has FIT and FAW however schedules are only available when looking up with FIT - this is how the National Rail Enquiries systems work.`};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
get
|
||||
}
|
20
src/services/list.services.js
Normal file
@ -0,0 +1,20 @@
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
const db = require('../services/dbAccess.services')
|
||||
const os = require('os')
|
||||
|
||||
async function getStations(){
|
||||
var out = await db.query("stations")
|
||||
log.out(`listServices.getStations: fetched stations list`)
|
||||
return out;
|
||||
}
|
||||
|
||||
async function getCorpus(){
|
||||
var out = await db.query("corpus")
|
||||
log.out(`listServices.getCorpus: fetched CORPUS list`)
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getStations,
|
||||
getCorpus
|
||||
}
|
16
src/services/stats.services.js
Normal file
@ -0,0 +1,16 @@
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
const db = require('../services/dbAccess.services')
|
||||
const os = require('os')
|
||||
|
||||
async function hits(){
|
||||
var dat = await db.query("meta", {target: "counters"});
|
||||
log.out(`listServices.meta: fetched server meta`)
|
||||
let out = {}
|
||||
out.host = os.hostname()
|
||||
out.dat = dat
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hits
|
||||
}
|
0
src/utils/checkauth.utils.js
Normal file
90
src/utils/dbinit.utils.js
Normal file
@ -0,0 +1,90 @@
|
||||
// FUNCTIONS
|
||||
// init() : Exported: Uses the internal functions to initialise databases.
|
||||
// check() : Checks data presence and age.
|
||||
// build() : Builds/Rebuilds collections.
|
||||
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
const time = require('../utils/timeConvert.utils'); // Time Helper
|
||||
const corpus = require('../services/corpus.services');
|
||||
const dbAccess = require('../services/dbAccess.services');
|
||||
|
||||
async function init(){
|
||||
var status = await check('corpus');
|
||||
if (status == "not_ready") {
|
||||
try {
|
||||
await build("corpus")
|
||||
} catch (err) {
|
||||
log.out("dbInitUtils.init: Error building corpus database")
|
||||
log.out(err)
|
||||
}
|
||||
}
|
||||
|
||||
var status = await check('stations')
|
||||
if (status == "not_ready") {
|
||||
try {
|
||||
await build("stations")
|
||||
} catch (err) {
|
||||
log.out("dbInitUtils.init: Error building stations database")
|
||||
log.out(err)
|
||||
}
|
||||
}
|
||||
indexes();
|
||||
dbAccess.createCount();
|
||||
}
|
||||
|
||||
async function check(coll){
|
||||
log.out(`dbInitUtils.check: Checking collection '${coll}'`)
|
||||
try {
|
||||
var queryStr = {'type':'collection','target': coll};
|
||||
var res = await dbAccess.query('meta',queryStr);
|
||||
log.out(`dbInitUtils.check: Last update of ${coll}: ${time.unixLocal(res['0']['updated'])}`)
|
||||
var now = time.jsUnix(Date.now())
|
||||
var delta = now - res['0']['updated']
|
||||
} catch (err) {
|
||||
log.out(`dbInitUtils.check: Unable to find out data age. Presume stale. Error Message:`)
|
||||
log.out(err)
|
||||
var delta = 12096000 // Extra zero to ensure data is updated.
|
||||
}
|
||||
|
||||
var maxAge = 1209600 // 14 Days
|
||||
if (delta > maxAge) {
|
||||
log.out(`dbInitUtils.check: '${coll}' data older than max age ${maxAge} seconds. Update pending`)
|
||||
return "not_ready"
|
||||
} else {
|
||||
log.out(`dbInitUtils.check: '${coll}' data newer than max age ${maxAge} seconds. Update not required`)
|
||||
return "ready"
|
||||
}
|
||||
}
|
||||
|
||||
async function build(db){ // `db` must be one of: `corpus`, `stations`, `all`.
|
||||
log.out("dbInitUtils.build: Building database structure")
|
||||
var corpusAll = await corpus.get();
|
||||
if (db === "corpus") {
|
||||
await dbAccess.dropCollection("corpus");
|
||||
dbAccess.putCorpus(corpusAll);
|
||||
|
||||
log.out(`dbInitUtils.build: Updating corpus meta`);
|
||||
dbAccess.updateMeta("collection", "corpus", time.jsUnix(Date.now()));
|
||||
}
|
||||
if (db === "stations") {
|
||||
await dbAccess.dropCollection("stations");
|
||||
var corpusSubset = await corpus.subset(corpusAll);
|
||||
dbAccess.putStations(corpusSubset);
|
||||
|
||||
log.out(`dbInitUtils.build: Updating stations meta`);
|
||||
dbAccess.updateMeta("collection", "stations", time.jsUnix(Date.now()));
|
||||
}
|
||||
}
|
||||
|
||||
async function indexes() {
|
||||
dbAccess.ensureIndex("corpus", "NLC");
|
||||
dbAccess.ensureIndex("corpus", "3ALPHA");
|
||||
dbAccess.ensureIndex("stations", "3ALPHA");
|
||||
dbAccess.ensureIndex("stations", "STANOX");
|
||||
dbAccess.ensureIndex("stations", "TIPLOC");
|
||||
dbAccess.ensureIndex("corpus", "NLCDESC", "text")
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init
|
||||
}
|
43
src/utils/ldb.utils.js
Normal file
@ -0,0 +1,43 @@
|
||||
const log = require('../utils/log.utils'); // Log Helper
|
||||
const db = require('../services/dbAccess.services') // DB Access
|
||||
const san = require('../utils/sanitizer.utils') // Sanitiser
|
||||
|
||||
async function checkCrs(input){
|
||||
var INPUT = input.toUpperCase()
|
||||
log.out(`ldbUtils.checkCrs: Building database query to find: '${INPUT}'`)
|
||||
var query = {'$or':[{'3ALPHA':INPUT},{'TIPLOC':INPUT},{'STANOX':INPUT}]};
|
||||
var result = await db.query("stations", query)
|
||||
log.out(`ldbUtils.checkCrs: Query results: ${JSON.stringify(result)}`)
|
||||
return result
|
||||
}
|
||||
|
||||
async function cleanMessages(input){
|
||||
var out = []
|
||||
if (typeof input.message == "string") {
|
||||
out.push(await san.cleanNrcc(input.message))
|
||||
} else if (typeof input.message == "object") {
|
||||
for(var i = 0; i < input.message.length; i++) {
|
||||
out.push(await san.cleanNrcc(input.message[i]))
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Accepts an object but not an Array and returns it wrapped in an array.
|
||||
async function cleanServices(input){
|
||||
var out = []
|
||||
if (!Array.isArray(input)) {
|
||||
log.out(`ldbUtils.cleanServices: Transforming input: ${input}`)
|
||||
out.push(input)
|
||||
log.out(`ldbUtils.cleanServices: Returning output: ${out}`)
|
||||
return out;
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkCrs,
|
||||
cleanMessages,
|
||||
cleanServices
|
||||
}
|
8
src/utils/log.utils.js
Normal file
@ -0,0 +1,8 @@
|
||||
function out(msg) {
|
||||
var time = new Date().toISOString();
|
||||
console.log(`${time} - ${msg}`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
out
|
||||
}
|
45
src/utils/sanitizer.utils.js
Normal file
@ -0,0 +1,45 @@
|
||||
const clean = require('string-sanitizer-fix');
|
||||
const log = require('../utils/log.utils');
|
||||
|
||||
/*
|
||||
string.sanitize("a.bc@d efg#h"); // abcdefgh
|
||||
string.sanitize.keepSpace("a.bc@d efg#h"); // abcd efgh
|
||||
string.sanitize.keepUnicode("a.bc@d efg#hক"); // abcd efghক
|
||||
string.sanitize.addFullstop("a.bc@d efg#h"); // abcd.efgh
|
||||
string.sanitize.addUnderscore("a.bc@d efg#h"); // abcd_efgh
|
||||
string.sanitize.addDash("a.bc@d efg#h"); // abcd-efgh
|
||||
string.sanitize.removeNumber("@abcd efgh123"); // abcdefgh
|
||||
string.sanitize.keepNumber("@abcd efgh123"); // abcdefgh123
|
||||
string.addFullstop("abcd efgh"); // abcd.efgh
|
||||
string.addUnderscore("@abcd efgh"); // @abcd_efgh
|
||||
string.addDash("@abcd efgh"); // @abcd-efgh
|
||||
string.removeSpace("@abcd efgh"); // @abcdefgh
|
||||
*/
|
||||
|
||||
function cleanApiEndpointTxt(input) {
|
||||
var output = clean.sanitize.keepSpace(input)
|
||||
if (output != input){
|
||||
log.out(`sanitizerUtils.cleanApiEndpoint: WARN: Sanitizing changed string. Input = ${input}`);
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
function cleanApiEndpointNum(input) {
|
||||
var output = clean.sanitize.keepNumber(input)
|
||||
if (output != input){
|
||||
log.out(`sanitizerUtils.cleanApiEndpointNum: WARN: Sanitizing changed string. Input = ${input}`);
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
function cleanNrcc(input) {
|
||||
var rmNewline = input.replace(/[\n\r]/g, ""); // Remove newlines
|
||||
var rmPara = rmNewline.replace(/<\/?p[^>]*>/g, ""); // Remove <p> & </p>
|
||||
return rmPara;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cleanApiEndpointTxt,
|
||||
cleanApiEndpointNum,
|
||||
cleanNrcc
|
||||
}
|
15
src/utils/timeConvert.utils.js
Normal file
@ -0,0 +1,15 @@
|
||||
function unixLocal(unix) {
|
||||
var jsTime = unix*1000
|
||||
var dt = new Date(jsTime)
|
||||
return dt.toLocaleString()
|
||||
}
|
||||
|
||||
function jsUnix(js) {
|
||||
var preRound = js / 1000
|
||||
return Math.round(preRound)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
unixLocal,
|
||||
jsUnix,
|
||||
}
|
27
src/utils/varTest.utils.js
Normal file
@ -0,0 +1,27 @@
|
||||
// Checks that all required environment variables are present.
|
||||
// Returns True or False and offers an object detailing what is missing.
|
||||
|
||||
async function varTest(){
|
||||
var required = {
|
||||
OWL_LDB_KEY: process.env.OWL_LDB_KEY,
|
||||
OWL_LDB_CORPUSUSER: process.env.OWL_LDB_CORPUSUSER,
|
||||
OWL_LDB_CORPUSPASS: process.env.OWL_LDB_CORPUSPASS,
|
||||
OWL_NOT_USED: process.env.OWL_NOT_USED
|
||||
}
|
||||
var desired = {
|
||||
OWL_DB_PASS: process.env.OWL_DB_PASS
|
||||
}
|
||||
// DO NOT LOG CREDENTIALS!!!
|
||||
|
||||
// Test that each of required is NOT undefined.
|
||||
// var pass = true if all okay, false if not.
|
||||
// Append any missing values to missing_required = []
|
||||
// Test that each of desired is NOT undefined.
|
||||
// Append any missing values to missing_desired = []
|
||||
|
||||
// Return : {pass: $pass, missong_required = $missing_required, missing_desired = $missing_desired}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
varTest
|
||||
}
|
4
static/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
*.xcf
|
||||
*.inkscape.svg
|
34
static/404.html
Normal file
@ -0,0 +1,34 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<!-- NO SCRIPTS LOADED - NOT REQUIRED AT PRESENT -->
|
||||
<title>OwlBoard - Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="top_button" class="hide_micro">
|
||||
<picture aria-label="Back" class="sidebar_control" onclick="history.back()">
|
||||
<source srcset="/images/nav/back.svg" type="image/svg+xml">
|
||||
<img src="back-40.png" alt="Back">
|
||||
</picture>
|
||||
</div>
|
||||
<picture>
|
||||
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml">
|
||||
<source media="(max-height: 739px)" srcset="/images/logo/logo-full-200.png" type="image/png">
|
||||
<source srcset="/images/logo/logo-full-250.png" type="image/png">
|
||||
<img class="titleimg" src="/images/logo/logo-full-250.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<h2>Oh no!</h2>
|
||||
<p>That page cannot be found</p>
|
||||
<p>Try going to the <a href="/">homepage</a></p>
|
||||
<p>Error number: 404</p>
|
||||
</body>
|
||||
</html>
|
12
static/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM fedora:latest as compressor
|
||||
RUN dnf install brotli nodejs npm jq -y
|
||||
RUN npm i uglifyjs-folder uglifycss html-minifier-terser -g
|
||||
COPY . /data/in
|
||||
RUN bash /data/in/conf/deploy.sh
|
||||
|
||||
FROM fholzer/nginx-brotli:latest
|
||||
RUN rm /etc/nginx/nginx.conf
|
||||
RUN apk update
|
||||
RUN apk add --upgrade libxml2 libxslt
|
||||
COPY ./conf/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=compressor /data/out/ /site-static/
|
129
static/board.html
Normal file
@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<title>OwlBoard - Loading</title>
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="./styles/boards.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<script src="./js/lib.main.js" defer></script>
|
||||
<script src="./js/lib.board.js" defer></script>
|
||||
<script src="./js/simple-board.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<div class="spinner">
|
||||
</div>
|
||||
<p id="loading_desc">\nLoading</p>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div id="header">
|
||||
<div id="station_name">
|
||||
<h1 id="stn_name" class="header-large"></h1>
|
||||
</div>
|
||||
<div id="header-right">
|
||||
<p class="header-small">Data from:</p>
|
||||
<p id="fetch_time" class="header-small">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alerts" onclick="">
|
||||
<div id="alerts_bar" onclick="inflateAlerts()">
|
||||
<picture>
|
||||
<source srcset="./images/nav/alert_icon.svg" type="image/svg+xml">
|
||||
<img id="alert_icon" src="./images/nav/alert_icon.svg" alt="">
|
||||
</picture>
|
||||
<p id="alert_bar_note"></p>
|
||||
<button id="alert_expand_arrow">⋁</button>
|
||||
<div id="alerts_msg" onclick="NULL">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="output">
|
||||
<table>
|
||||
<caption>Train Services</caption>
|
||||
<tr>
|
||||
<th class="name">Origin</th>
|
||||
<th class="name">Dest.</th>
|
||||
<th class="plat">Plat.</th>
|
||||
<th class="time">Sch Arr.</th>
|
||||
<th class="time">Exp Arr.</th>
|
||||
<th class="time">Sch Dep.</th>
|
||||
<th class="time">Exp Dep.</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="no_services" class="main-notice hidden-whille-loading">
|
||||
<p>There are no scheduled train services from this station</p>
|
||||
</div>
|
||||
|
||||
<div id="ferry" class="hide-when-loading secondary-table">
|
||||
<table>
|
||||
<caption>Ferry Services</caption>
|
||||
<tr>
|
||||
<th class="name">Origin</th>
|
||||
<th class="name">Dest.</th>
|
||||
<th class="plat"></th>
|
||||
<th class="time">Sch Arr.</th>
|
||||
<th class="time">Exp Arr.</th>
|
||||
<th class="time">Sch Dep.</th>
|
||||
<th class="time">Exp Dep.</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="bus" class="hide-when-loading secondary-table">
|
||||
<table>
|
||||
<caption>Bus Services</caption>
|
||||
<tr>
|
||||
<th class="name">Origin</th>
|
||||
<th class="name">Dest.</th>
|
||||
<th class="plat"></th>
|
||||
<th class="time">Sch Arr.</th>
|
||||
<th class="time">Exp Arr.</th>
|
||||
<th class="time">Sch Dep.</th>
|
||||
<th class="time">Exp Dep.</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="error_notice" class="main-notice hide-when-loading">
|
||||
<h1 class="error">Oops</h1>
|
||||
<p class="error">There was an error with your request</p>
|
||||
<p id="err_not_found" class="notices-hidden">The station you are searching for cannot be found</p>
|
||||
<p id="err_no_data" class="notices-hidden">The station has no data. It may not be in operation yet/anymore.</p>
|
||||
<p id="err_conn" class="notices-hidden">Connection Error, check your data connection. Retrying.</p>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<a href="https://nationalrail.co.uk" target="_blank" rel="nofollow external noreferrer noopener">
|
||||
<picture id="nre_logo">
|
||||
<source srcset="./images/nre/nre-powered_400w.jxl" type="image/jxl">
|
||||
<source srcset="./images/nre/nre-powered_400w.webp" type="image/webp">
|
||||
<img src="./images/nre/nre-powered_400w.png" alt="Powered by National Rail Enquiries">
|
||||
</picture>
|
||||
</a>
|
||||
<a href="/">
|
||||
<picture id="owlboard_logo">
|
||||
<source srcset="./images/logo/mono-logo.svg" type="image/svg+xml">
|
||||
<img src="./images/logo/mono-logo-33.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<picture id="home_icon">
|
||||
<source srcset="./images/nav/home_icon.svg" type="image/svg+xml">
|
||||
<img src="./images/nav/home_icon-40.png" alt="Home">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
38
static/conf/deploy.sh
Normal file
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOTIN="/data/in"
|
||||
ROOTOUT="/data/out"
|
||||
|
||||
echo "Running UglifyJS on /data/in folder"
|
||||
uglifyjs-folder "$ROOTIN" -x ".js" -eo "$ROOTOUT"
|
||||
|
||||
echo "Running UglifyCSS"
|
||||
CSSIN="/data/in/styles/"
|
||||
CSSOUT="/data/out/styles"
|
||||
|
||||
cd $CSSIN
|
||||
echo "Changed directory"
|
||||
pwd
|
||||
for f in *
|
||||
do
|
||||
if [ -f "$f" ]; then
|
||||
uglifycss "$f" --output "$f";
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Moving 'styles' to 'out'"
|
||||
cp -r /data/in/styles /data/out/styles
|
||||
|
||||
echo "Running html-minifier-terser on /folder"
|
||||
HTMLIN="/data/in/"
|
||||
HTMLOUT="/data/out"
|
||||
html-minifier-terser --collapse-whitespace --remove-comments --file-ext html --input-dir /data/in/ --output-dir /data/out/
|
||||
|
||||
echo "Moving JSON Manifest file from root to output"
|
||||
cat /data/in/manifest.json | jq -c > /data/out/manifest.json
|
||||
|
||||
echo "Moving images folder from in/ to out/"
|
||||
cp -r /data/in/images /data/out/images
|
||||
|
||||
echo "Running GZIP & Brotli on all HTML, JS, CSS, JSON & SVG files"
|
||||
find /data/out -type f -name \*.html -or -name \*.js -or -name \*.css -or -name \*.json -or -name \*.svg -or -name \*.ttf | while read file; do gzip -k -9 $file; brotli -k -q 11 $file; done
|
60
static/conf/nginx.conf
Normal file
@ -0,0 +1,60 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
proxy_cache_path /var/cache/nginx keys_zone=owl_cache:20m inactive=24h;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
proxy_cache owl_cache;
|
||||
|
||||
add_header Content-Security-Policy "default-src 'self'";
|
||||
|
||||
location / {
|
||||
root /site-static/;
|
||||
index index.html;
|
||||
gzip_static on;
|
||||
brotli_static on;
|
||||
error_page 404 /404.html;
|
||||
expires 3600;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8460;
|
||||
proxy_cache_key $scheme://$host$uri$is_args$query_string;
|
||||
proxy_ignore_headers Cache-Control;
|
||||
proxy_cache_valid 200 2m; # Evaluate whether 2m or 1m is more appropriate
|
||||
expires 2m;
|
||||
add_header Cache-Control "private, no-transform";
|
||||
}
|
||||
|
||||
location /api/v1/list/ {
|
||||
proxy_pass http://localhost:8460;
|
||||
proxy_cache_key $scheme://$host$uri$is_args$query_string;
|
||||
proxy_ignore_headers Cache-Control;
|
||||
proxy_cache_valid 200 10080m;
|
||||
expires 3d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
}
|
||||
}
|
36
static/conn-err.html
Normal file
@ -0,0 +1,36 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<!-- NO SCRIPTS LOADED - NOT REQUIRED AT PRESENT -->
|
||||
<title>OwlBoard - Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="top_button" class="hide_micro">
|
||||
<picture aria-label="Close Menu" class="sidebar_control" onclick="history.back()">
|
||||
<source srcset="/images/nav/back.svg" type="image/svg+xml">
|
||||
<img src="back-40.png" alt="Close menu">
|
||||
</picture>
|
||||
</div>
|
||||
<picture>
|
||||
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml">
|
||||
<source media="(max-height: 739px)" srcset="/images/logo/logo-full-200.png" type="image/png">
|
||||
<source srcset="/images/logo/logo-full-250.png" type="image/png">
|
||||
<img class="titleimg" src="/images/logo/logo-full-250.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<h2>Oh no!</h2>
|
||||
<p>OwlBoard has encountered a Connection Error</p>
|
||||
<p>Check your data connection and try again</p>
|
||||
<p>Go to the <a href="/">homepage</a></p>
|
||||
<br>
|
||||
<p>Error Code: CERR</p>
|
||||
</body>
|
||||
</html>
|
64
static/find-code.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="./styles/find-code.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<title>OwlBoard - Code Lookup</title>
|
||||
<script src="./js/lib.main.js" defer></script>
|
||||
<script src="./js/find-code.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="top_button" class="hide_micro">
|
||||
<a href="/">
|
||||
<picture aria-label="Home" class="sidebar_control">
|
||||
<source srcset="/images/nav/back.svg" type="image/svg+xml">
|
||||
<img src="back-40.png" alt="Home">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<picture>
|
||||
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml">
|
||||
<source media="(max-height: 739px)" srcset="/images/logo/logo-full-200.png" type="image/png">
|
||||
<source srcset="/images/logo/logo-full-250.png" type="image/png">
|
||||
<img class="titleimg" src="/images/logo/logo-full-250.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<h2>Code Lookup</h2>
|
||||
<p>Enter one known code in the relevant box below and hit submit.
|
||||
Where they exist, the other code types will be filled in.</p>
|
||||
<p>You cannot yet lookup by location name as the values are not unique.</p>
|
||||
<p>Location name search will be added in the future.</p>
|
||||
|
||||
<div id="loading">
|
||||
<div class="spinner">
|
||||
</div>
|
||||
<p id="loading_desc">Searching</p>
|
||||
</div>
|
||||
|
||||
<label for="name">Location name:</label><br>
|
||||
<input type="text" class="small-lookup-box" id="name" name="name" readonly=""><br>
|
||||
<label for="3alpha">CRS/3ALPHA:</label><br>
|
||||
<input type="text" class="small-lookup-box" id="3alpha" name="3alpha" maxlength="3"><br>
|
||||
<label for="nlc">NLC:</label><br>
|
||||
<input type="number" class="small-lookup-box" id="nlc" name="nlc" min="100000" max="999999"><br>
|
||||
<label for="tiploc">TIPLOC:</label><br>
|
||||
<input type="text" class="small-lookup-box" id="tiploc" name="tiploc" maxlength="7"><br>
|
||||
<label for="stanox">STANOX:</label><br>
|
||||
<input type="number" class="small-lookup-box" id="stanox" name="stanox"><br>
|
||||
<label for="stanme" hidden>STANME:</label><br>
|
||||
<input type="test" class="small-lookup-box" id="stanme" name="stanme" readonly="" hidden><br>
|
||||
<input type="submit" value="Find" class="lookup-button" onclick="fetchEntry()">
|
||||
<input type="submit" value="Clear" class="lookup-button" onclick="clearForm()">
|
||||
</body>
|
||||
</html>
|
93
static/help.html
Normal file
@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="./styles/help.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<!-- NO SCRIPTS LOADED - NOT REQUIRED AT PRESENT -->
|
||||
<title>OwlBoard</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="top_button" class="hide_micro">
|
||||
<a href="/">
|
||||
<picture aria-label="Home" class="sidebar_control">
|
||||
<source srcset="/images/nav/back.svg" type="image/svg+xml">
|
||||
<img src="back-40.png" alt="Home">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<picture>
|
||||
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml">
|
||||
<source media="(max-height: 739px)" srcset="/images/logo/logo-full-200.png" type="image/png">
|
||||
<source srcset="/images/logo/logo-full-250.png" type="image/png">
|
||||
<img class="titleimg" src="/images/logo/logo-full-250.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<h2>Help</h2>
|
||||
<p>OwlBoard gives you quick and easy access to departure boards for
|
||||
all National Rail stations in the UK.</p>
|
||||
<p>Just type a CRS, TIPLOC or STANOX into the textbox on the homepage and tap
|
||||
enter on the screen or your keypad. You can also select a differnt board type,
|
||||
more details on your choices below.</p>
|
||||
<p>For example, Portway Park &
|
||||
Ride's CRS is 'PRI', and its TIPLOC is 'PTWYPR'; Portsmouth Harbour's
|
||||
CRS is 'PMH', and its TIPLOC is 'PHBR'.</p>
|
||||
<p>A CRS is always three letters,
|
||||
a TIPLOC can be between 4-7 letters.</p>
|
||||
<br>
|
||||
<h3>Don't know the CRS or TIPLOC?</h3>
|
||||
<p>Sorry, you can't search by name but you can use our <a href="find-code.html">
|
||||
Code Lookup</a> page to help.</p>
|
||||
<h3>Board Types</h3>
|
||||
<h4>Basic Board - Default</h4>
|
||||
<p>The basic board shows the next 10 train arrival and departures, as well as
|
||||
bus and ferry departures where available.</p>
|
||||
<p>You can tap on a trains origin or destination to see service details.</p>
|
||||
<br>
|
||||
<h3>Glossary</h3>
|
||||
<p>Some of the terms may be new to you or different from those commonly used.</p>
|
||||
<table id="table">
|
||||
<tr>
|
||||
<th>Term</th>
|
||||
<th>Definition</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAN</td>
|
||||
<td>Cancelled</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CRS</td>
|
||||
<td>Computer Reservation System Code - correctly termed as '3ALPHA'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NLC</td>
|
||||
<td>National Location Code - Used for finance & accounting</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RT</td>
|
||||
<td>Right rime (On time)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>STANOX</td>
|
||||
<td>Station Number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TIPLOC</td>
|
||||
<td>Timing Point Location (Name)</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
<h3>Spotted an issue with the site?</h3>
|
||||
<p>Let me know by <a href="./report.html">reporting an issue</a>.</p>
|
||||
</body>
|
||||
</html>
|
BIN
static/images/app-icons/any/apple-192.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
static/images/app-icons/any/plain-logo-512.png
Normal file
After Width: | Height: | Size: 33 KiB |
2
static/images/app-icons/any/plain-logo.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 667.26 706.8" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><g transform="translate(-111.19 -90.003)"><rect x="213.88" y="260.42" width="81.997" height="48.755" fill="#fcfc09"/><rect x="167.35" y="187.29" width="181.72" height="68.7" fill="#f0ffff"/></g><g transform="matrix(1.3333 0 0 -1.3333 -111.19 790)" fill="#00b7b7"><g transform="translate(7.5,-7.5)"><path d="m112.25 369.32c12.988-10.101 30.989 0.076 39.875 15.722 9.969-34.512 37.501-53.367 57.648-37.596-6.503-20.085-34.373-34.554-61.522 13.215-15.947-14.598-28.559-14.416-36.001 8.659m14.302-81.369c15.405-6.791 28.15 7.974 30.66 25.885 21.351-31.973 53.097-43.81 65.802-22.905 1.237-21.743-18.954-43.315-60.644-2.464-9.305-18.663-20.837-21.694-35.818-0.516m33.549 175.93 24.381-9.342 24.382 9.342-24.382-24.381zm-14.899 47.955c1.574 0 3.075-0.31 4.448-0.869-1.973-1.1-3.309-3.206-3.309-5.626 0-3.554 2.883-6.436 6.437-6.436 1.608 0 3.079 0.59 4.208 1.566 5e-3 -0.143 8e-3 -0.284 8e-3 -0.428 0-6.512-5.28-11.791-11.792-11.791s-11.791 5.279-11.791 11.791c0 6.513 5.279 11.793 11.791 11.793m77.642 0c1.573 0 3.075-0.31 4.447-0.869-1.973-1.1-3.308-3.206-3.308-5.626 0-3.554 2.882-6.436 6.437-6.436 1.608 0 3.079 0.59 4.207 1.566 6e-3 -0.143 9e-3 -0.284 9e-3 -0.428 0-6.512-5.28-11.791-11.792-11.791-6.513 0-11.792 5.279-11.792 11.791 0 6.513 5.279 11.793 11.792 11.793m0 8.887c11.421 0 20.677-9.259 20.677-20.68 0-11.42-9.256-20.677-20.677-20.677-11.42 0-20.678 9.257-20.678 20.677 0 11.421 9.258 20.68 20.678 20.68m-77.642 0c11.42 0 20.679-9.259 20.679-20.68 0-11.42-9.259-20.677-20.679-20.677s-20.678 9.257-20.678 20.677c0 11.421 9.258 20.68 20.678 20.68m222.62-271.32c-5.257-16.303-14.169-16.431-25.436-6.118-19.182-33.751-38.872-23.527-43.468-9.336 14.236-11.143 33.688 2.178 40.73 26.562 6.28-11.055 18.998-18.245 28.174-11.108m-7.657 101.75c-5.26-16.304-14.169-16.433-25.436-6.118-19.182-33.751-38.873-23.529-43.469-9.338 14.236-11.142 33.688 2.179 40.731 26.564 6.279-11.055 18.997-18.247 28.174-11.108m3.828-50.877c-5.259-16.302-14.168-16.429-25.435-6.117-19.182-33.752-38.873-23.528-43.469-9.338 14.236-11.14 33.687 2.181 40.731 26.564 6.279-11.055 18.996-18.243 28.173-11.109m-185-126.56 8.456 14.687-2.481 14.064c8.24-6.441 16.897-12.257 25.895-17.419l-13.787-20.682c-5.163 5.163-11.523 8.215-18.083 9.35m214.44 47.276-16.013 24.214v93.753c0 40.019-32.441 72.459-72.458 72.459-37.742 0-68.739-28.855-72.144-65.707-0.563 6.626-0.974 13.336-1.228 20.135 0 36.412 26.858 66.546 61.843 71.684 15.118 42.436 3.44 91.058-31.815 121.88-0.332 0.288-0.668 0.569-1.002 0.854-14.934-8.987-28.921-18.756-41.766-29.467 14.159 17.506 30.102 32.253 47.212 45.198-47.368 32.008-116.69 32.008-164.06 0 17.11-12.946 33.054-27.694 47.212-45.201-11.522 9.609-23.965 18.462-37.188 26.67-40.329-37.119-47.937-98.2-17.545-144.16 4.83-7.304 7.155-15.981 6.624-24.722-3.808-62.683 19.436-123.99 63.84-168.4 3.505-3.505 7.116-6.877 10.824-10.115l-15.66-23.489c-18.988 18.987-54.154 9.494-56.053-19.369 4.94 7.294 12.965 10.334 21.93 10.306 4.624 3.683 10.595 5.668 18.277 5.498-14.197-2.669-23.71-11.684-25.256-26.007 9.826 11.065 21.246 13.643 34.785 11.262-7.045-4.94-12.081-12.841-12.767-23.274 4.941 7.293 12.965 10.335 21.932 10.303 1.457 1.162 3.047 2.155 4.783 2.963 0.557-0.597 1.095-1.202 1.614-1.819-7.504-4.493-12.316-11.823-13.378-21.652 5.924 6.673 12.428 10.259 19.627 11.533 1.471-3.662 2.152-7.45 1.823-11.24 4.057 2.964 6.698 7.077 8.006 11.651 2.475-0.187 5.026-0.574 7.654-1.132 12.8-8.149 22.377-20.41 21.31-32.736 11.849 8.659 11.621 27.116 1.367 38.28l8.659 15.04-2.26 12.808c15.693-7.825 32.28-13.746 49.402-17.626-7.313 17.831-12.59 36.793-15.633 56.995 19.737-52.989 51.201-99.462 92.224-140.63 3.548-6.147 10.784-9.143 17.64-7.305 6.856 1.837 11.623 8.048 11.623 15.147v65.793c33.793-9.913 62.771-33.463 79.074-66.13l119.85 29.509c-14.567 69.186-99.576 110.14-175.59 96.362z" fill="#00b7b7" fill-rule="evenodd"/></g><path transform="scale(1,-1)" d="m426.75-584.57h11.635v242.67h-11.635z" fill-rule="evenodd" stroke-width=".80733"/><path transform="scale(1,-1)" d="m533.35-584.09h11.635v242.67h-11.635z" fill-rule="evenodd" stroke-width=".80733"/><path transform="scale(1,-1)" d="m404.98-554.66h159.56v34.904h-159.56z" fill-rule="evenodd" stroke-width=".75"/><path transform="scale(1,-1)" d="m404.98-481.16h159.56v34.904h-159.56z" fill-rule="evenodd" stroke-width=".75"/><path transform="scale(1,-1)" d="m404.98-404.66h159.56v34.904h-159.56z" fill-rule="evenodd" stroke-width=".75"/></g></svg>
|
After Width: | Height: | Size: 4.7 KiB |
2
static/images/app-icons/maskable/mask-icon.svg
Normal file
After Width: | Height: | Size: 5.1 KiB |
2
static/images/icon.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 667.26 706.8" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><g transform="translate(-101.19 -80.003)"><rect transform="matrix(1.3333 0 0 1.3333 0 880)" x="152.91" y="-472.18" width="61.498" height="36.566" fill="#fcfc09" stroke-width=".75"/><rect x="157.35" y="177.29" width="181.72" height="68.7" fill="#f0ffff"/></g><g transform="matrix(1.3333 0 0 -1.3333 -101.19 800)"><g fill="#00b7b7"><path d="m112.25 369.32c12.988-10.101 30.989 0.076 39.875 15.722 9.969-34.512 37.501-53.367 57.648-37.596-6.503-20.085-34.373-34.554-61.522 13.215-15.947-14.598-28.559-14.416-36.001 8.659m14.302-81.369c15.405-6.791 28.15 7.974 30.66 25.885 21.351-31.973 53.097-43.81 65.802-22.905 1.237-21.743-18.954-43.315-60.644-2.464-9.305-18.663-20.837-21.694-35.818-0.516m33.549 175.93 24.381-9.342 24.382 9.342-24.382-24.381zm-14.899 47.955c1.574 0 3.075-0.31 4.448-0.869-1.973-1.1-3.309-3.206-3.309-5.626 0-3.554 2.883-6.436 6.437-6.436 1.608 0 3.079 0.59 4.208 1.566 5e-3 -0.143 8e-3 -0.284 8e-3 -0.428 0-6.512-5.28-11.791-11.792-11.791s-11.791 5.279-11.791 11.791c0 6.513 5.279 11.793 11.791 11.793m77.642 0c1.573 0 3.075-0.31 4.447-0.869-1.973-1.1-3.308-3.206-3.308-5.626 0-3.554 2.882-6.436 6.437-6.436 1.608 0 3.079 0.59 4.207 1.566 6e-3 -0.143 9e-3 -0.284 9e-3 -0.428 0-6.512-5.28-11.791-11.792-11.791-6.513 0-11.792 5.279-11.792 11.791 0 6.513 5.279 11.793 11.792 11.793m0 8.887c11.421 0 20.677-9.259 20.677-20.68 0-11.42-9.256-20.677-20.677-20.677-11.42 0-20.678 9.257-20.678 20.677 0 11.421 9.258 20.68 20.678 20.68m-77.642 0c11.42 0 20.679-9.259 20.679-20.68 0-11.42-9.259-20.677-20.679-20.677s-20.678 9.257-20.678 20.677c0 11.421 9.258 20.68 20.678 20.68m222.62-271.32c-5.257-16.303-14.169-16.431-25.436-6.118-19.182-33.751-38.872-23.527-43.468-9.336 14.236-11.143 33.688 2.178 40.73 26.562 6.28-11.055 18.998-18.245 28.174-11.108m-7.657 101.75c-5.26-16.304-14.169-16.433-25.436-6.118-19.182-33.751-38.873-23.529-43.469-9.338 14.236-11.142 33.688 2.179 40.731 26.564 6.279-11.055 18.997-18.247 28.174-11.108m3.828-50.877c-5.259-16.302-14.168-16.429-25.435-6.117-19.182-33.752-38.873-23.528-43.469-9.338 14.236-11.14 33.687 2.181 40.731 26.564 6.279-11.055 18.996-18.243 28.173-11.109m-185-126.56 8.456 14.687-2.481 14.064c8.24-6.441 16.897-12.257 25.895-17.419l-13.787-20.682c-5.163 5.163-11.523 8.215-18.083 9.35m214.44 47.276-16.013 24.214v93.753c0 40.019-32.441 72.459-72.458 72.459-37.742 0-68.739-28.855-72.144-65.707-0.563 6.626-0.974 13.336-1.228 20.135 0 36.412 26.858 66.546 61.843 71.684 15.118 42.436 3.44 91.058-31.815 121.88-0.332 0.288-0.668 0.569-1.002 0.854-14.934-8.987-28.921-18.756-41.766-29.467 14.159 17.506 30.102 32.253 47.212 45.198-47.368 32.008-116.69 32.008-164.06 0 17.11-12.946 33.054-27.694 47.212-45.201-11.522 9.609-23.965 18.462-37.188 26.67-40.329-37.119-47.937-98.2-17.545-144.16 4.83-7.304 7.155-15.981 6.624-24.722-3.808-62.683 19.436-123.99 63.84-168.4 3.505-3.505 7.116-6.877 10.824-10.115l-15.66-23.489c-18.988 18.987-54.154 9.494-56.053-19.369 4.94 7.294 12.965 10.334 21.93 10.306 4.624 3.683 10.595 5.668 18.277 5.498-14.197-2.669-23.71-11.684-25.256-26.007 9.826 11.065 21.246 13.643 34.785 11.262-7.045-4.94-12.081-12.841-12.767-23.274 4.941 7.293 12.965 10.335 21.932 10.303 1.457 1.162 3.047 2.155 4.783 2.963 0.557-0.597 1.095-1.202 1.614-1.819-7.504-4.493-12.316-11.823-13.378-21.652 5.924 6.673 12.428 10.259 19.627 11.533 1.471-3.662 2.152-7.45 1.823-11.24 4.057 2.964 6.698 7.077 8.006 11.651 2.475-0.187 5.026-0.574 7.654-1.132 12.8-8.149 22.377-20.41 21.31-32.736 11.849 8.659 11.621 27.116 1.367 38.28l8.659 15.04-2.26 12.808c15.693-7.825 32.28-13.746 49.402-17.626-7.313 17.831-12.59 36.793-15.633 56.995 19.737-52.989 51.201-99.462 92.224-140.63 3.548-6.147 10.784-9.143 17.64-7.305 6.856 1.837 11.623 8.048 11.623 15.147v65.793c33.793-9.913 62.771-33.463 79.074-66.13l119.85 29.509c-14.567 69.186-99.576 110.14-175.59 96.362z" fill="#00b7b7" fill-rule="evenodd"/></g></g></svg>
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/images/logo/mono-logo-33.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
2
static/images/logo/mono-logo.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 667.26 706.8" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><g transform="matrix(1.3333 0 0 -1.3333 -101.19 800)"><g fill="#fff"><path d="m112.25 369.32c12.988-10.101 30.989 0.076 39.875 15.722 9.969-34.512 37.501-53.367 57.648-37.596-6.503-20.085-34.373-34.554-61.522 13.215-15.947-14.598-28.559-14.416-36.001 8.659m14.302-81.369c15.405-6.791 28.15 7.974 30.66 25.885 21.351-31.973 53.097-43.81 65.802-22.905 1.237-21.743-18.954-43.315-60.644-2.464-9.305-18.663-20.837-21.694-35.818-0.516m33.549 175.93 24.381-9.342 24.382 9.342-24.382-24.381zm-14.899 47.955c1.574 0 3.075-0.31 4.448-0.869-1.973-1.1-3.309-3.206-3.309-5.626 0-3.554 2.883-6.436 6.437-6.436 1.608 0 3.079 0.59 4.208 1.566 5e-3 -0.143 8e-3 -0.284 8e-3 -0.428 0-6.512-5.28-11.791-11.792-11.791s-11.791 5.279-11.791 11.791c0 6.513 5.279 11.793 11.791 11.793m77.642 0c1.573 0 3.075-0.31 4.447-0.869-1.973-1.1-3.308-3.206-3.308-5.626 0-3.554 2.882-6.436 6.437-6.436 1.608 0 3.079 0.59 4.207 1.566 6e-3 -0.143 9e-3 -0.284 9e-3 -0.428 0-6.512-5.28-11.791-11.792-11.791-6.513 0-11.792 5.279-11.792 11.791 0 6.513 5.279 11.793 11.792 11.793m0 8.887c11.421 0 20.677-9.259 20.677-20.68 0-11.42-9.256-20.677-20.677-20.677-11.42 0-20.678 9.257-20.678 20.677 0 11.421 9.258 20.68 20.678 20.68m-77.642 0c11.42 0 20.679-9.259 20.679-20.68 0-11.42-9.259-20.677-20.679-20.677s-20.678 9.257-20.678 20.677c0 11.421 9.258 20.68 20.678 20.68m222.62-271.32c-5.257-16.303-14.169-16.431-25.436-6.118-19.182-33.751-38.872-23.527-43.468-9.336 14.236-11.143 33.688 2.178 40.73 26.562 6.28-11.055 18.998-18.245 28.174-11.108m-7.657 101.75c-5.26-16.304-14.169-16.433-25.436-6.118-19.182-33.751-38.873-23.529-43.469-9.338 14.236-11.142 33.688 2.179 40.731 26.564 6.279-11.055 18.997-18.247 28.174-11.108m3.828-50.877c-5.259-16.302-14.168-16.429-25.435-6.117-19.182-33.752-38.873-23.528-43.469-9.338 14.236-11.14 33.687 2.181 40.731 26.564 6.279-11.055 18.996-18.243 28.173-11.109m-185-126.56 8.456 14.687-2.481 14.064c8.24-6.441 16.897-12.257 25.895-17.419l-13.787-20.682c-5.163 5.163-11.523 8.215-18.083 9.35m214.44 47.276-16.013 24.214v93.753c0 40.019-32.441 72.459-72.458 72.459-37.742 0-68.739-28.855-72.144-65.707-0.563 6.626-0.974 13.336-1.228 20.135 0 36.412 26.858 66.546 61.843 71.684 15.118 42.436 3.44 91.058-31.815 121.88-0.332 0.288-0.668 0.569-1.002 0.854-14.934-8.987-28.921-18.756-41.766-29.467 14.159 17.506 30.102 32.253 47.212 45.198-47.368 32.008-116.69 32.008-164.06 0 17.11-12.946 33.054-27.694 47.212-45.201-11.522 9.609-23.965 18.462-37.188 26.67-40.329-37.119-47.937-98.2-17.545-144.16 4.83-7.304 7.155-15.981 6.624-24.722-3.808-62.683 19.436-123.99 63.84-168.4 3.505-3.505 7.116-6.877 10.824-10.115l-15.66-23.489c-18.988 18.987-54.154 9.494-56.053-19.369 4.94 7.294 12.965 10.334 21.93 10.306 4.624 3.683 10.595 5.668 18.277 5.498-14.197-2.669-23.71-11.684-25.256-26.007 9.826 11.065 21.246 13.643 34.785 11.262-7.045-4.94-12.081-12.841-12.767-23.274 4.941 7.293 12.965 10.335 21.932 10.303 1.457 1.162 3.047 2.155 4.783 2.963 0.557-0.597 1.095-1.202 1.614-1.819-7.504-4.493-12.316-11.823-13.378-21.652 5.924 6.673 12.428 10.259 19.627 11.533 1.471-3.662 2.152-7.45 1.823-11.24 4.057 2.964 6.698 7.077 8.006 11.651 2.475-0.187 5.026-0.574 7.654-1.132 12.8-8.149 22.377-20.41 21.31-32.736 11.849 8.659 11.621 27.116 1.367 38.28l8.659 15.04-2.26 12.808c15.693-7.825 32.28-13.746 49.402-17.626-7.313 17.831-12.59 36.793-15.633 56.995 19.737-52.989 51.201-99.462 92.224-140.63 3.548-6.147 10.784-9.143 17.64-7.305 6.856 1.837 11.623 8.048 11.623 15.147v65.793c33.793-9.913 62.771-33.463 79.074-66.13l119.85 29.509c-14.567 69.186-99.576 110.14-175.59 96.362z" fill="#fff" fill-rule="evenodd"/></g></g></svg>
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/images/logo/square-logo-100.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
2
static/images/logo/square-logo.svg
Normal file
After Width: | Height: | Size: 6.8 KiB |
2
static/images/logo/wide_logo.svg
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
static/images/logo/wide_logo_200.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/images/logo/wide_logo_250.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/images/nav/alert_icon-50.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
10
static/images/nav/alert_icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 448 433" version="1.1" viewBox="0 0 448 433" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<radialGradient id="a" cx="216.7" cy="393.79" r="296.7" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F4D708" offset="0"/>
|
||||
<stop stop-color="#FCB400" offset="1"/>
|
||||
</radialGradient>
|
||||
<path d="m8.551 390.5 184.85-368.8s26.409-31.504 52.815 0c26.41 31.501 180.19 370.65 180.19 370.65s3.105 18.534-27.961 18.534-361.94 0-361.94 0-23.299 0-27.959-20.38z" fill="url(#a)"/>
|
||||
<path d="m8.551 390.5 184.85-368.8s26.409-31.504 52.815 0c26.41 31.501 180.19 370.65 180.19 370.65s3.105 18.534-27.961 18.534-361.94 0-361.94 0-23.299 0-27.959-20.38z" fill="none" stroke="#E2A713" stroke-width="5"/>
|
||||
<path d="m212.5 292.63c-13.168-79.969-19.75-123.12-19.75-129.45 0-7.703 2.551-13.926 7.66-18.676 5.105-4.746 10.871-7.121 17.293-7.121 6.949 0 12.82 2.535 17.609 7.598s7.188 11.023 7.188 17.883c0 6.543-6.668 49.801-20 129.77h-10zm27 38.17c0 6.098-2.156 11.301-6.469 15.613-4.313 4.309-9.461 6.465-15.453 6.465-6.098 0-11.301-2.156-15.613-6.465-4.313-4.313-6.465-9.516-6.465-15.613 0-5.992 2.152-11.141 6.465-15.453s9.516-6.469 15.613-6.469c5.992 0 11.141 2.156 15.453 6.469s6.48 9.45 6.48 15.44z"/>
|
||||
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Warning Notification</dc:title><dc:date>2007-02-08T17:08:47</dc:date><dc:description>Beveled yellow caution sign</dc:description><dc:source>http://openclipart.org/detail/3130/warning-notification-by-eastshores</dc:source><dc:creator><cc:Agent><dc:title>eastshores</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>alert</rdf:li><rdf:li>caution</rdf:li><rdf:li>clip art</rdf:li><rdf:li>clipart</rdf:li><rdf:li>icon</rdf:li><rdf:li>image</rdf:li><rdf:li>media</rdf:li><rdf:li>public domain</rdf:li><rdf:li>svg</rdf:li><rdf:li>warning</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>
|
After Width: | Height: | Size: 2.6 KiB |
BIN
static/images/nav/back-40.png
Normal file
After Width: | Height: | Size: 195 B |
6
static/images/nav/back.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="4.191mm" height="4.191mm" version="1.1" viewBox="0 0 4.191 4.191" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-91.032 -156.47)">
|
||||
<path d="m95.223 158.3v0.52916h-3.175c0.48507 0.48507 0.97014 0.97014 1.4552 1.4552-0.12524 0.12524-0.25047 0.25047-0.37571 0.37571l-2.0955-2.0955 2.0955-2.0955c0.12524 0.12524 0.25047 0.25047 0.37571 0.37571-0.48507 0.48507-0.97014 0.97014-1.4552 1.4552h3.175z" fill="#f9f9f9" stroke-width=".26458"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 510 B |
BIN
static/images/nav/close-40.png
Normal file
After Width: | Height: | Size: 468 B |
6
static/images/nav/close.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="3.7042mm" height="3.7042mm" version="1.1" viewBox="0 0 3.7042 3.7042" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-20.8 -106.38)">
|
||||
<path d="m24.505 106.75-0.37306-0.37306-1.479 1.479-1.479-1.479-0.37306 0.37306 1.479 1.479-1.479 1.479 0.37306 0.37306 1.479-1.479 1.479 1.479 0.37306-0.37306-1.479-1.479z" fill="#f9f9f9" stroke-width=".26458"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 422 B |
6
static/images/nav/hamburger.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="4.7625mm" height="3.175mm" version="1.1" viewBox="0 0 4.7625 3.175" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-54.25 -207.32)">
|
||||
<path d="m54.25 207.32h4.7625v0.52917h-4.7625v-0.52917m0 1.3229h4.7625v0.52916h-4.7625v-0.52916m0 1.3229h4.7625v0.52917h-4.7625z" fill="#f9f9f9" stroke-width=".26458"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 378 B |
BIN
static/images/nav/home_icon-25.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
15
static/images/nav/home_icon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 97.74 85.154" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g transform="translate(-51.081 -910.49)">
|
||||
<path d="m100.72 910.49-24.82 23.287-24.82 23.285 16.678 0.084v38.498h23.467v-32.783h17.453v32.783h23.467v-38.174l16.676 0.084-48.1-47.064z" color="#000000" color-rendering="auto" fill="#fff" fill-rule="evenodd" image-rendering="auto" shape-rendering="auto" solid-color="#000000" style="isolation:auto;mix-blend-mode:normal"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 836 B |
BIN
static/images/nav/save-59.png
Normal file
After Width: | Height: | Size: 928 B |
6
static/images/nav/save.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="4.7625mm" height="4.7625mm" version="1.1" viewBox="0 0 4.7625 4.7625" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-123.47 -217.85)">
|
||||
<path d="m126.38 220.44c-0.1323-0.12965-0.3228-0.21167-0.52917-0.21167-0.43921 0-0.79375 0.35454-0.79375 0.79375 0 0.3466 0.22225 0.63765 0.52917 0.74612 0.0185-0.56885 0.33602-1.0636 0.79375-1.3282m-0.76994 1.6404h-1.6113v-3.7042h2.9554l0.74877 0.74877v1.1959c0.19844 0.0688 0.37571 0.17198 0.52917 0.31486v-1.7304l-1.0583-1.0583h-3.175c-0.29369 0-0.52917 0.23812-0.52917 0.52917v3.7042c0 0.29104 0.23548 0.52917 0.52917 0.52917h1.8018c-0.0926-0.1614-0.15875-0.33867-0.1905-0.52917m-1.3467-2.3812h2.3812v-1.0583h-2.3812v1.0583m2.5797 2.9104-0.72761-0.79375 0.30692-0.30692 0.42069 0.42069 0.94985-0.94985 0.30692 0.37306-1.2568 1.2568" fill="#f9f9f9" stroke-width=".26458"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 888 B |
BIN
static/images/nre/nre-powered.xcf
Normal file
BIN
static/images/nre/nre-powered_200w.jxl
Normal file
BIN
static/images/nre/nre-powered_200w.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
static/images/nre/nre-powered_200w.webp
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
static/images/nre/nre-powered_400w.jxl
Normal file
BIN
static/images/nre/nre-powered_400w.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/images/nre/nre-powered_400w.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
static/images/nre/nre-powered_800w.jxl
Normal file
BIN
static/images/nre/nre-powered_800w.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
static/images/nre/nre-powered_800w.webp
Normal file
After Width: | Height: | Size: 19 KiB |
70
static/index.html
Normal file
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<script src="./js/lib.main.js" defer></script>
|
||||
<script src="./js/index.js" defer></script>
|
||||
<title>OwlBoard</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading Box -->
|
||||
<div id="loading">
|
||||
<div class="spinner">
|
||||
</div>
|
||||
<p id="loading_desc">Loading</p>
|
||||
</div>
|
||||
|
||||
<!-- Popup Menu -->
|
||||
<div id="top_button" class="hide_micro">
|
||||
<picture aria-label="Menu" class="sidebar_control" id="sidebar_open_short" onclick="sidebarOpen()">
|
||||
<source srcset="/images/nav/hamburger.svg" type="image/svg+xml">
|
||||
<img src="hamburger_40.png" alt="Open menu">
|
||||
</picture>
|
||||
<picture aria-label="Close Menu" class="sidebar_control" id="sidebar_close_short" onclick="sidebarClose()">
|
||||
<source srcset="/images/nav/close.svg" type="image/svg+xml">
|
||||
<img src="close-40.png" alt="Close menu">
|
||||
</picture>
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
<a href="/">Home</a>
|
||||
<a href="/find-code.html">Code Search</a>
|
||||
<a href="/settings.html">Settings</a>
|
||||
<a href="/help.html">Help</a>
|
||||
<a href="/issue.html">Report Issue</a>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Begins -->
|
||||
<picture>
|
||||
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml">
|
||||
<source media="(max-height: 739px)" srcset="/images/logo/logo-full-200.png" type="image/png">
|
||||
<source srcset="/images/logo/logo-full-250.png" type="image/png">
|
||||
<img class="titleimg" src="/images/logo/logo-full-250.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<br>
|
||||
<form action="board.html">
|
||||
<input class="lookup-box" type="text" id="crs-lookup" name="stn" placeholder="Enter CRS/TIPLOC" autocomplete="off"/>
|
||||
<br>
|
||||
<input type="submit" value="Lookup Board" class="lookup-button" onclick="vibe('ok')">
|
||||
</form>
|
||||
|
||||
<h2>Quick Links</h2>
|
||||
<div id="quick_links">
|
||||
</div>
|
||||
<div class="text-description">
|
||||
<p>Customise your quick links on the <a href="/settings.html">Settings</a> page.</p>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>Created by <a href="https://fredboniface.co.uk" target="_blank" rel="noreferrer noopener">Fred Boniface</a> - 0.0.3</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
58
static/issue.html
Normal file
@ -0,0 +1,58 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="./styles/issue.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<script src="./js/lib.main.js" defer></script>
|
||||
<script src="./js/issue.js" defer></script>
|
||||
<title>OwlBoard - Report</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading Box -->
|
||||
<div id="loading">
|
||||
<div class="spinner">
|
||||
</div>
|
||||
<p id="loading_desc">Loading</p>
|
||||
</div>
|
||||
<div id="top_button" class="hide_micro">
|
||||
<a href="/">
|
||||
<picture aria-label="Home" class="sidebar_control">
|
||||
<source srcset="/images/nav/back.svg" type="image/svg+xml">
|
||||
<img src="back-40.png" alt="Home">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
<picture>
|
||||
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml">
|
||||
<source media="(max-height: 739px)" srcset="/images/logo/logo-full-200.png" type="image/png">
|
||||
<source srcset="/images/logo/logo-full-250.png" type="image/png">
|
||||
<img class="titleimg" src="/images/logo/logo-full-250.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<h2>Report an Issue</h2>
|
||||
<p>To help diagnosing an issue, data about your browser and device will be
|
||||
collected alongside the data that you enter below.</p>
|
||||
<p>The data will be available publically in the <a href="https://git.fjla.uk/fred.boniface/owlboard/issues">
|
||||
OwlBoard Issue Tracker</a>. A preview will be shown before the data is sent.</p>
|
||||
<label for="subject">Subject:</label><br>
|
||||
<input type="text" name="subject" id="subject" class="text-entry"/><br>
|
||||
<label for="content">Message:</label><br>
|
||||
<textarea name="message" id="message" class="text-entry-long"></textarea><br>
|
||||
<input type="submit" name="submit" id="submit" label="Preview" class="lookup-button" onclick="submit()">
|
||||
<div id="preflight">
|
||||
<h3>Check & Send</h3>
|
||||
<h3>---</h3>
|
||||
<h4 id="pre_subject"></h4>
|
||||
<p id="pre_message"></p>
|
||||
<button id="send" class="lookup-button" onclick="send()">Send</button>
|
||||
<button id="cancel" class="lookup-button" onclick="cancel()">Cancel</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
97
static/js/find-code.js
Normal file
@ -0,0 +1,97 @@
|
||||
hideLoading();
|
||||
|
||||
async function fetchEntry(){ // This can be condensed
|
||||
showLoading();
|
||||
var name = document.getElementById("name")
|
||||
var crs = document.getElementById("3alpha")
|
||||
var nlc = document.getElementById("nlc")
|
||||
var tiploc = document.getElementById("tiploc")
|
||||
var stanox = document.getElementById("stanox")
|
||||
|
||||
var values = {
|
||||
name: name.value,
|
||||
crs: crs.value,
|
||||
nlc: nlc.value,
|
||||
tiploc: tiploc.value,
|
||||
stanox: stanox.value
|
||||
}
|
||||
parseData(values)
|
||||
}
|
||||
|
||||
async function parseData(values){
|
||||
vibe()
|
||||
if (values.crs != ""){
|
||||
setLoadingDesc(`Searching\n${values.crs.toUpperCase()}`)
|
||||
var data = await getData("crs", values.crs)
|
||||
} else if (values.nlc != ""){
|
||||
setLoadingDesc(`Searching\n${values.nlc.toUpperCase()}`)
|
||||
var data = await getData("nlc", values.nlc)
|
||||
} else if (values.tiploc != ""){
|
||||
setLoadingDesc(`Searching\n${values.tiploc.toUpperCase()}`)
|
||||
var data = await getData("tiploc", values.tiploc)
|
||||
} else if (values.stanox != ""){
|
||||
setLoadingDesc(`Searching\n${values.stanox.toUpperCase()}`)
|
||||
var data = await getData("stanox", values.stanox)
|
||||
} else if (values.name != ""){
|
||||
setLoadingDesc(`Searching\n${values.name}`)
|
||||
var data = await getData("name", values.name)
|
||||
} else {
|
||||
log("find-code.parseData: No data entered", "WARN")
|
||||
await clearForm();
|
||||
document.getElementById("name").value = "No data entered"
|
||||
vibe("err");
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
displayData(data);
|
||||
}
|
||||
|
||||
async function getData(type, value){
|
||||
log(`find-code.getData: Looking for: ${type} '${value}'`, "INFO")
|
||||
try {
|
||||
var url = `${window.location.origin}/api/v1/find/${type}/${value}`;
|
||||
var resp = await fetch(url);
|
||||
return await resp.json()
|
||||
} catch (err) {
|
||||
log(`find-code.getData: Error getting data: ${err}`, "WARN")
|
||||
vibe("err")
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function displayData(data){
|
||||
hideLoading();
|
||||
if (data.status === "failed" || data == ""){
|
||||
log(`find-code.displayData: Unable to find data`, "WARN")
|
||||
clearForm();
|
||||
document.getElementById("name").value = "Not Found";
|
||||
} else {
|
||||
log(`find-code.displayData: Inserting data`, "INFO")
|
||||
vibe("ok")
|
||||
try {
|
||||
document.getElementById("name").value = data['0']['NLCDESC']
|
||||
} catch (err) {}
|
||||
try {
|
||||
document.getElementById("3alpha").value = data['0']['3ALPHA']
|
||||
} catch (err) {}
|
||||
try {
|
||||
document.getElementById("nlc").value = data['0']['NLC']
|
||||
} catch (err) {}
|
||||
try {
|
||||
document.getElementById("tiploc").value = data['0']['TIPLOC']
|
||||
} catch (err) {}
|
||||
try {
|
||||
document.getElementById("stanox").value = data['0']['STANOX']
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
async function clearForm(){
|
||||
document.getElementById("name").value = ""
|
||||
document.getElementById("3alpha").value = ""
|
||||
document.getElementById("nlc").value = ""
|
||||
document.getElementById("tiploc").value = ""
|
||||
document.getElementById("stanox").value = ""
|
||||
vibe("ok");
|
||||
hideLoading();
|
||||
}
|
26
static/js/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
// Init:
|
||||
pageInit();
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/sw.js");
|
||||
}
|
||||
|
||||
async function pageInit() {
|
||||
await loadQuickLinks();
|
||||
hideLoading(); // From lib.main
|
||||
}
|
||||
|
||||
async function gotoBoard(station){
|
||||
vibe("ok")
|
||||
window.location.assign(`${window.location.origin}/board.html?stn=${station}`);
|
||||
}
|
||||
|
||||
async function loadQuickLinks(){
|
||||
var data = await getQuickLinks(); // From lib.main
|
||||
var buttons = "";
|
||||
for(var i = 0; i < data.length; i++) {
|
||||
buttons += `
|
||||
<button class="actionbutton" onclick="gotoBoard('${data[i]}')">${data[i].toUpperCase()}</button>`
|
||||
}
|
||||
document.getElementById("quick_links").insertAdjacentHTML("beforeend", buttons)
|
||||
}
|
88
static/js/issue.js
Normal file
@ -0,0 +1,88 @@
|
||||
init();
|
||||
|
||||
async function init() {
|
||||
hideLoading()
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
setLoadingDesc("Collecting\nData")
|
||||
showLoading()
|
||||
var browserData = await getBrowserData();
|
||||
setLoadingDesc("Reading\nForm")
|
||||
var formData = await getFormData();
|
||||
preflight({browserData: browserData, formData: formData})
|
||||
}
|
||||
|
||||
async function getFormData() {
|
||||
let data = {}
|
||||
data.subject = document.getElementById("subject").value
|
||||
data.message = document.getElementById("message").value
|
||||
return data
|
||||
}
|
||||
|
||||
async function getBrowserData() {
|
||||
let data = {}
|
||||
data.userAgent = navigator.userAgent
|
||||
data.userAgentData = JSON.stringify(navigator.userAgentData)
|
||||
data.localStorage = JSON.stringify(await storageAvailable('localStorage'))
|
||||
data.sessionStorage = JSON.stringify(await storageAvailable('sessionStorage'))
|
||||
data.viewport = `${window.innerWidth} x ${window.innerHeight}`
|
||||
return data
|
||||
}
|
||||
|
||||
async function preflight(data) {
|
||||
document.getElementById("pre_subject").textContent = data.formData.subject
|
||||
pre_msg = `UserAgent: ${data.browserData.userAgent}
|
||||
\nUserAgentData: ${data.browserData.userAgentData}
|
||||
\nlocalStorage Avail: ${data.browserData.localStorage}
|
||||
\nsessionStorage Avail: ${data.browserData.sessionStorage}
|
||||
\nViewport size: ${data.browserData.viewport}
|
||||
\nUser message:\n\n${data.formData.message}`
|
||||
document.getElementById("pre_message").innerText = pre_msg
|
||||
hideLoading()
|
||||
document.getElementById("preflight").style = "display: block"
|
||||
sessionStorage.setItem("preflight_subject", data.formData.subject)
|
||||
sessionStorage.setItem("preflight_msg", pre_msg)
|
||||
}
|
||||
|
||||
async function cancel() {
|
||||
document.getElementById("preflight").style = "display: none"
|
||||
}
|
||||
|
||||
async function send() {
|
||||
setLoadingDesc("Sending\nData")
|
||||
document.getElementById("preflight").style = "display: none"
|
||||
showLoading()
|
||||
var subject = sessionStorage.getItem("preflight_subject");
|
||||
var msg = sessionStorage.getItem("preflight_msg")
|
||||
if (typeof subject != "string") {
|
||||
subject = document.getElementById("preflight_subject").innerText
|
||||
}
|
||||
if (typeof msg != "string") {
|
||||
msg = document.getElementById("preflight_msg")
|
||||
}
|
||||
var payload = JSON.stringify({subject: subject, msg: msg})
|
||||
console.log(payload);
|
||||
let opt = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
redirect: 'follow',
|
||||
body: payload
|
||||
}
|
||||
var res = await fetch(`${window.location.origin}/api/v1/issue`, opt)
|
||||
if (res.status == 200) {
|
||||
setLoadingDesc("Success")
|
||||
vibe("ok")
|
||||
await delay(2500)
|
||||
window.location.replace("/")
|
||||
} else {
|
||||
setLoadingDesc("Error")
|
||||
vibe("err")
|
||||
await delay(2500)
|
||||
hideLoading()
|
||||
document.getElementById("preflight").style = "display: none;"
|
||||
}
|
||||
}
|
248
static/js/lib.board.js
Normal file
@ -0,0 +1,248 @@
|
||||
/* Fetch Functions */
|
||||
async function publicLdb(stn) {
|
||||
var url = `${window.location.origin}/api/v1/ldb/${stn}`;
|
||||
var resp = await fetch(url);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
/* Set page heading */
|
||||
async function setHeaders(title,time) {
|
||||
var prefix = `OwlBoard - `
|
||||
document.title = `${prefix}${title}`
|
||||
document.getElementById("stn_name").textContent = title
|
||||
document.getElementById("fetch_time").textContent = time.toLocaleTimeString()
|
||||
sessionStorage.setItem("board_location", title);
|
||||
}
|
||||
|
||||
/* Display No Trains Message */
|
||||
async function displayNoTrains() {
|
||||
log("No Trains", "WARN")
|
||||
document.getElementById('no_services').style = "display: block;";
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
/* Parse the value of `platform` to account for unknown platforms */
|
||||
async function parsePlatform(svc){
|
||||
if (svc.platform != undefined) {
|
||||
var platform = svc.platform;
|
||||
} else {
|
||||
var platform = "-";
|
||||
}
|
||||
if (svc.platformChanged) { // Not present in public API, ready for staff version.
|
||||
var changed = "changed";
|
||||
} else {
|
||||
var changed = "";
|
||||
}
|
||||
return {num: platform, change: changed}
|
||||
}
|
||||
|
||||
|
||||
/* Change value of time strings to fit well on small screens */
|
||||
async function parseTime(string){
|
||||
switch (string) {
|
||||
case "Delayed":
|
||||
var output = "LATE";
|
||||
var change = "changed";
|
||||
break;
|
||||
case "Cancelled":
|
||||
var output = "CANC";
|
||||
var change = "cancelled";
|
||||
break;
|
||||
case "On time":
|
||||
var output = "RT";
|
||||
var change = "";
|
||||
break;
|
||||
case "":
|
||||
var output = "-";
|
||||
var change = "";
|
||||
break;
|
||||
case undefined:
|
||||
var output = "-";
|
||||
var change = "";
|
||||
break;
|
||||
case "No report":
|
||||
var output = "-";
|
||||
var change = "";
|
||||
break;
|
||||
case "undefined":
|
||||
var output = false;
|
||||
var change = "";
|
||||
break;
|
||||
default:
|
||||
var output = string;
|
||||
var change = "changed";
|
||||
}
|
||||
return {data: output, changed: change};
|
||||
}
|
||||
|
||||
/* Convert multiple Origin/Destinations to single string */
|
||||
async function parseName(location) {
|
||||
if (Array.isArray(location)) {
|
||||
var name = `${location[0]['locationName']} & ${location[1]['locationName']}`
|
||||
return name;
|
||||
}
|
||||
else {
|
||||
return location.locationName;
|
||||
}
|
||||
}
|
||||
|
||||
// Display Alert Messages
|
||||
async function displayAlerts(array) {
|
||||
var counter = 0
|
||||
var messages = ""
|
||||
for(var i = 0; i < array.length; i++) {
|
||||
// Increment counter
|
||||
counter += 1;
|
||||
// Reset Vars
|
||||
messages += `<p>${array[i]}</p>`;
|
||||
}
|
||||
if (counter > 0) {
|
||||
document.getElementById("alerts_msg").insertAdjacentHTML("beforeend", messages)
|
||||
document.getElementById("alerts").style = "display:block"
|
||||
document.getElementById("alerts_bar").style = "display:block"
|
||||
if (counter == 1) {
|
||||
document.getElementById("alert_bar_note").textContent = `There is ${counter} active alert`
|
||||
} else if (counter > 1) {
|
||||
document.getElementById("alert_bar_note").textContent = `There are ${counter} active alerts`
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* Show/Hide alerts box */
|
||||
async function inflateAlerts() {
|
||||
document.getElementById("alerts_msg").style = "display:block;";
|
||||
document.getElementById("alert_expand_arrow").style = "transform: rotate(180deg);";
|
||||
document.getElementById("alerts_bar").setAttribute("onclick", "deflateAlerts()")
|
||||
}
|
||||
|
||||
async function deflateAlerts() {
|
||||
document.getElementById("alerts_msg").style = "display.none;";
|
||||
document.getElementById("alert_expand_arrow").style = "transform: rotate(0deg);";
|
||||
document.getElementById("alerts_bar").setAttribute("onclick", "inflateAlerts()")
|
||||
}
|
||||
|
||||
/*//// SERVICE DETAIL LISTS ////*/
|
||||
// Build calling list: -- This outputs calling point data to sessionStorage in the format: key{pre: [{PREVIOUS_Stops}], post: [{POST_STOPS}]}
|
||||
async function buildCallLists(svc) {
|
||||
var sSvcId = svc.serviceID;
|
||||
var oSvcData = {
|
||||
plat: svc.platform,
|
||||
sta: svc.sta,
|
||||
eta: svc.eta,
|
||||
std: svc.std,
|
||||
etd: svc.etd
|
||||
};
|
||||
try {
|
||||
if (typeof svc.previousCallingPoints.callingPointList.callingPoint != 'undefined') {
|
||||
let array = await makeArray(svc.previousCallingPoints.callingPointList.callingPoint);
|
||||
oSvcData.pre = array;
|
||||
}
|
||||
} catch (err) { /* Do nothing if ERR */ }
|
||||
try {
|
||||
if (typeof svc.subsequentCallingPoints.callingPointList.callingPoint != 'undefined') {
|
||||
let array = await makeArray(svc.subsequentCallingPoints.callingPointList.callingPoint);
|
||||
oSvcData.post = array;
|
||||
}
|
||||
} catch (err) { /* Do nothing if ERR */ }
|
||||
sessionStorage.setItem(sSvcId, JSON.stringify(oSvcData))
|
||||
}
|
||||
|
||||
/* Display calling list: - Read data from sessionStorage and write to DOM. */
|
||||
async function showCalls(id) {
|
||||
log(`Showing details for service ${id}`, "INFO")
|
||||
var svcDetail = await JSON.parse(sessionStorage.getItem(id));
|
||||
var pre = "";
|
||||
var post = "";
|
||||
if (typeof svcDetail.pre != 'undefined') {
|
||||
for(var preCall = 0; preCall < svcDetail.pre.length; preCall++) {
|
||||
pre += await singleCall(svcDetail.pre[preCall]);
|
||||
}
|
||||
}
|
||||
if (typeof svcDetail.post != 'undefined') {
|
||||
for(var postCall = 0; postCall < svcDetail.post.length; postCall++) {
|
||||
post += await singleCall(svcDetail.post[postCall]);
|
||||
}
|
||||
}
|
||||
/* Run retreived data through parsers */
|
||||
var thisStd = await parseTime(svcDetail.std);
|
||||
var thisEtd = await parseTime(svcDetail.etd);
|
||||
var thisSta = await parseTime(svcDetail.sta);
|
||||
var thisEta = await parseTime(svcDetail.eta);
|
||||
/* Prepare data for this station */
|
||||
if (thisStd.data != "-") {
|
||||
var sTime = `${thisStd.data}`
|
||||
var eTime = `${thisEtd.data}`
|
||||
var change = thisEtd.changed
|
||||
} else {
|
||||
var sTime = `${thisSta.data}`
|
||||
var eTime = `${thisEta.data}`
|
||||
var change = thisEta.changed
|
||||
};
|
||||
|
||||
let here = `<tr>
|
||||
<td class="detail-name detail-name-here detail-table-content">${sessionStorage.getItem("board_location")}</td>
|
||||
<td class="detail-table-content">${sTime}</td>
|
||||
<td class="detail-table-content ${change}">${eTime}</td>
|
||||
</tr> `
|
||||
/* Prepare then insert DOM Data */
|
||||
let dom = ` <div id="${id}" class="call-data">
|
||||
<p class="close-data" onclick="hideCalls('${id}')">X</p>
|
||||
<table class="call-table">
|
||||
<tr>
|
||||
<th class="detail-name-head">Location</th>
|
||||
<th class="time">Schedule</th>
|
||||
<th class="time">Act/Est</th>
|
||||
</tr>
|
||||
${pre}
|
||||
${here}
|
||||
${post}
|
||||
</table>
|
||||
</div>`
|
||||
|
||||
document.body.insertAdjacentHTML("beforeend", dom);
|
||||
document.getElementById(id).style = "display: block;";
|
||||
return;
|
||||
}
|
||||
|
||||
async function hideCalls(id) {
|
||||
let element = document.getElementById(id)
|
||||
element.style = "display: none;";
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Builds the train data information in to a table row */
|
||||
async function singleCall(data) {
|
||||
if (typeof data.et != "undefined") {
|
||||
var time = await parseTime(data.et)
|
||||
} else if (typeof data.at != "undefined") {
|
||||
var time = await parseTime(data.at)
|
||||
}
|
||||
return `<tr>
|
||||
<td class="detail-name detail-table-content">${data.locationName}</td>
|
||||
<td class="detail-table-content">${data.st}</td>
|
||||
<td class="detail-table-content ${time.changed}">${time.data}</td>
|
||||
</tr>`
|
||||
}
|
||||
|
||||
/* Error Handler */
|
||||
async function errorHandler() {
|
||||
if (sessionStorage.getItem("failcount")) {
|
||||
var errCount = parseInt(sessionStorage.getItem("failcount"))
|
||||
} else {
|
||||
var errCount = 0;
|
||||
}
|
||||
errCount += 1;
|
||||
sessionStorage.setItem("failcount", errCount.toString())
|
||||
if (errCount < 10){
|
||||
await delay(3000);
|
||||
vibe("err")
|
||||
location.reload()
|
||||
} else {
|
||||
sessionStorage.removeItem("failcount");
|
||||
window.location.assign("conn-err.html")
|
||||
}
|
||||
}
|
132
static/js/lib.main.js
Normal file
@ -0,0 +1,132 @@
|
||||
/* Feature Detectors */
|
||||
|
||||
/* Valid values for ${type}: localstorage, sessionstorage */
|
||||
async function storageAvailable(type) { // Currently not used
|
||||
try {
|
||||
let storage = window[type];
|
||||
let x = '__storage_test__';
|
||||
storage.setItem(x, "test");
|
||||
storage.getItem(x);
|
||||
storage.removeItem(x);
|
||||
log(`lib.main.storageAvailable: ${type} is available`, "INFO")
|
||||
return true;
|
||||
} catch (err) {
|
||||
log(`lib.main.storageAvailable: ${type} is not available`, "ERR")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Array Converter
|
||||
Converts a string to a single item array */
|
||||
async function makeArray(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
var array = [];
|
||||
array.push(data);
|
||||
return array;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/* Timeouts */
|
||||
/* Usage: '' */
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
|
||||
/* Log Helper */
|
||||
/* Values for level: 1, 2, 3 */
|
||||
/* Maintains backwards compatibility for previous
|
||||
implementation of log helper */
|
||||
async function log(msg, type) {
|
||||
var time = new Date().toISOString();
|
||||
switch (type) {
|
||||
case "ERR":
|
||||
console.error(`${time} - ${msg}`);
|
||||
break;
|
||||
case "WARN":
|
||||
console.warn(`${time} - ${msg}`);
|
||||
break;
|
||||
case "INFO":
|
||||
console.info(`${time} - ${msg}`);
|
||||
break;
|
||||
default:
|
||||
console.log(`${time} - ${msg}`);
|
||||
break;
|
||||
};
|
||||
};
|
||||
|
||||
/* Show/Hide - Menu Control */
|
||||
async function sidebarOpen() {
|
||||
document.getElementById("sidebar").style.width = "50%";
|
||||
document.getElementById("sidebar_open_short").style.display = "none";
|
||||
document.getElementById("sidebar_close_short").style.display = "block";
|
||||
}
|
||||
|
||||
async function sidebarClose() {
|
||||
document.getElementById("sidebar").style.width = "0%"
|
||||
document.getElementById("sidebar_open_short").style.display = "block";
|
||||
document.getElementById("sidebar_close_short").style.display = "none";
|
||||
}
|
||||
|
||||
/* Loading Box Control */
|
||||
async function hideLoading() {
|
||||
document.getElementById("loading").style = "display: none;";
|
||||
}
|
||||
|
||||
/* DEPRECIATED: Alias for hideLoading() - Marked for removal*/
|
||||
async function clearLoading() {
|
||||
log("Depreciated function called - clearLoading() - Alias to hideLoading()", "WARN")
|
||||
await hideLoading();
|
||||
}
|
||||
|
||||
async function showLoading() {
|
||||
document.getElementById("loading").style = "display: block;";
|
||||
}
|
||||
|
||||
async function setLoadingDesc(desc) {
|
||||
document.getElementById("loading_desc").textContent = `${desc}`;
|
||||
}
|
||||
|
||||
/* Fetch User Settings */
|
||||
async function getQuickLinks() {
|
||||
var defaults =
|
||||
["bri","lwh","srd","mtp","rda","cfn",
|
||||
"sml","shh","pri","avn","sar","svb"];
|
||||
try {
|
||||
if (localStorage.getItem("qlOpt")) {
|
||||
var data = JSON.parse(localStorage.getItem("qlOpt"));
|
||||
} else {
|
||||
data = defaults;
|
||||
}
|
||||
} catch (err) {
|
||||
data = defaults;
|
||||
}
|
||||
return data.sort();
|
||||
}
|
||||
|
||||
/* Fetch a known query parameter from the pages URL */
|
||||
async function getQuery(param) {
|
||||
var params = new URLSearchParams(window.location.search)
|
||||
var query = params.get(param)
|
||||
if (query) {
|
||||
return query
|
||||
} else {
|
||||
return 'false'
|
||||
}
|
||||
}
|
||||
|
||||
async function vibe(type) {
|
||||
let canVibrate = "vibrate" in navigator || "mozVibrate" in navigator
|
||||
if (canVibrate && !("vibrate" in navigator)){
|
||||
navigator.vibrate = navigator.mozVibrate
|
||||
}
|
||||
switch (type) {
|
||||
case "err":
|
||||
navigator.vibrate([300])
|
||||
break;
|
||||
case "ok":
|
||||
navigator.vibrate([50,50,50])
|
||||
break;
|
||||
default:
|
||||
navigator.vibrate(30)
|
||||
}
|
||||
}
|
57
static/js/settings.js
Normal file
@ -0,0 +1,57 @@
|
||||
// Init:
|
||||
const ql = ["ql0","ql1","ql2","ql3","ql4","ql5","ql6","ql7","ql8","ql9","ql10","ql11"]
|
||||
storageAvailable("localStorage");
|
||||
getQl();
|
||||
hideLoading();
|
||||
|
||||
async function getQl(){
|
||||
var qlOpt = await getQuickLinks()
|
||||
if (qlOpt){
|
||||
var i = 0
|
||||
while (i < 12) {
|
||||
if (qlOpt[i] != 'undefined') {
|
||||
document.getElementById(`ql${i}`).value = qlOpt[i]
|
||||
i +=1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setQl(){
|
||||
await showLoading();
|
||||
var qlSet = []
|
||||
for (i in ql) {
|
||||
var opt = document.getElementById(`ql${i}`).value
|
||||
if (opt != ""){
|
||||
qlSet.push(opt)
|
||||
}
|
||||
qlSet.sort()
|
||||
}
|
||||
localStorage.setItem("qlOpt", JSON.stringify(qlSet))
|
||||
log(`settings.setQl: User settings saved`, "INFO")
|
||||
await hideLoading();
|
||||
await showDone();
|
||||
vibe("ok")
|
||||
await delay(800);
|
||||
hideDone();
|
||||
}
|
||||
|
||||
async function clearQl(){
|
||||
showLoading();
|
||||
localStorage.removeItem("qlOpt")
|
||||
log(`settings.setQl: User settings reset to default`, "INFO")
|
||||
getQl()
|
||||
await hideLoading();
|
||||
await showDone();
|
||||
vibe("ok");
|
||||
await delay(800);
|
||||
hideDone();
|
||||
}
|
||||
|
||||
async function showDone() {
|
||||
document.getElementById("done").style = "opacity: 1";
|
||||
}
|
||||
|
||||
async function hideDone() {
|
||||
document.getElementById("done").style = "opacity: 0";
|
||||
}
|
235
static/js/simple-board.js
Normal file
@ -0,0 +1,235 @@
|
||||
/* Page Init: */
|
||||
init()
|
||||
|
||||
/* Init function */
|
||||
async function init() {
|
||||
console.time("Loading Time")
|
||||
setLoadingDesc(`Loading\nservices`)
|
||||
var stn = await getQuery("stn");
|
||||
setLoadingDesc(`Loading\n${stn.toUpperCase()}`)
|
||||
log(`init: Looking up: ${stn}`);
|
||||
var sv = await getQuery("sv");
|
||||
log(`init: Staff Version: ${sv}`);
|
||||
if (sv === 'true') {
|
||||
log("init: Staff Version not supported yet.")
|
||||
log("init: Unable to proceed.")
|
||||
} else {
|
||||
try {
|
||||
var data = await publicLdb(stn)
|
||||
setLoadingDesc(`${stn.toUpperCase()}\nParsing Data`)
|
||||
log("simple-board.init: Fetched LDB Data", "INFO")
|
||||
} catch (err) {
|
||||
var data = "err"
|
||||
setLoadingDesc(`Waiting\nConnection`)
|
||||
log(`simple-board.init: Error fetching data: ${err}`, "ERR")
|
||||
}
|
||||
parseLdb(data)
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for any errors in data returned from the Fetch call
|
||||
If no errors, if there are none, call buildPage(). */
|
||||
async function parseLdb(data) {
|
||||
if (data.ERROR == "NOT_FOUND") { // Station not found
|
||||
hideLoading();
|
||||
document.getElementById("error_notice").style = "display: block;";
|
||||
document.getElementById("err_not_found").style = "display: block;";
|
||||
setHeaders("Not Found",new Date())
|
||||
} else if (data == false) { // No data for station
|
||||
hideLoading();
|
||||
document.getElementById("error_notice").style = "display: block;";
|
||||
document.getElementById("err_no_data").style = "display:block;";
|
||||
setHeaders("No Data",new Date())
|
||||
} else if (data == "err") { // Connection Error
|
||||
await delay(2000);
|
||||
hideLoading();
|
||||
document.getElementById("error_notice").style = "display: block;";
|
||||
document.getElementById("err_conn").style = "display: block;";
|
||||
setHeaders("Connection Error",new Date())
|
||||
showLoading();
|
||||
await delay(5000);
|
||||
log(`parseLdb: Passing to error handler`, "ERR")
|
||||
errorHandler();
|
||||
} else {
|
||||
buildPage(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Build and Display Functions
|
||||
async function buildPage(data) {
|
||||
setLoadingDesc('Loading\nData')
|
||||
var stationName = data.GetStationBoardResult.locationName;
|
||||
log(`buildPage: Data ready for ${stationName}`);
|
||||
var generateTime = new Date(await data.GetStationBoardResult.generatedAt);
|
||||
log(`buildPage: Data prepared at ${generateTime.toLocaleString()}`)
|
||||
setHeaders(stationName, generateTime);
|
||||
// Check for notices and if true pass to function
|
||||
if (data.GetStationBoardResult.nrccMessages) {
|
||||
setLoadingDesc('Loading\nAlerts')
|
||||
await displayAlerts(await makeArray(data.GetStationBoardResult.nrccMessages.message));
|
||||
}
|
||||
if (data.GetStationBoardResult.trainServices) {
|
||||
setLoadingDesc('Loading\nTrains')
|
||||
displayTrains(await makeArray(data.GetStationBoardResult.trainServices.service))
|
||||
} else {
|
||||
displayNoTrains()
|
||||
}
|
||||
if (data.GetStationBoardResult.ferryServices) {
|
||||
setLoadingDesc('Loading\nFerries')
|
||||
displayFerry(await makeArray(data.GetStationBoardResult.ferryServices.service))
|
||||
}
|
||||
if (data.GetStationBoardResult.busServices) {
|
||||
setLoadingDesc('Loading\nBusses')
|
||||
displayBus(await makeArray(data.GetStationBoardResult.busServices.service))
|
||||
}
|
||||
console.timeEnd("Loading Time")
|
||||
}
|
||||
|
||||
|
||||
async function displayTrains(data) {
|
||||
log(`simple-board.displayTrains: Inserting data in DOM`)
|
||||
for(var i = 0; i < data.length; i++) {
|
||||
// Reset Vars
|
||||
var svc = data[i];
|
||||
displayService(svc);
|
||||
buildCallLists(svc);
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
document.getElementById("output").style = "display:block;";
|
||||
log(`simple-board.displayTrains: Insertion complete`)
|
||||
}
|
||||
|
||||
async function displayFerry(ferrySvc) {
|
||||
for(var i = 0; i < ferrySvc.length; i++) {
|
||||
displayFerryService(ferrySvc[i])
|
||||
}
|
||||
}
|
||||
|
||||
async function displayBus(busSvc) {
|
||||
for(var i = 0; i < busSvc.length; i++) {
|
||||
displayBusService(busSvc[i])
|
||||
buildCallLists(busSvc[i])
|
||||
}
|
||||
}
|
||||
|
||||
async function displayService(svc) {
|
||||
var table = document.getElementById("output");
|
||||
|
||||
// Determine Time Message
|
||||
var sta = await parseTime(svc.sta);
|
||||
var eta = await parseTime(svc.eta);
|
||||
var std = await parseTime(svc.std);
|
||||
var etd = await parseTime(svc.etd);
|
||||
// Determine Platform Message
|
||||
//if (svc.platform != undefined){var plt = svc.platform} else {var plt = "-"};
|
||||
var plt = await parsePlatform(svc);
|
||||
// Define Table Row
|
||||
var row = `
|
||||
<table>
|
||||
<tr>
|
||||
<td class="name name-item" onclick="showCalls('${svc.serviceID}')">${await parseName(svc.origin.location)}</td>
|
||||
<td class="name name-item" onclick="showCalls('${svc.serviceID}')">${await parseName(svc.destination.location)}</td>
|
||||
<td class="plat ${plt.changed}">${plt.num}</td>
|
||||
<td class="time">${sta.data}</td>
|
||||
<td class="time ${eta.changed}">${eta.data}</td>
|
||||
<td class="time">${std.data}</td>
|
||||
<td class="time ${etd.changed}">${etd.data}</td>
|
||||
</tr>
|
||||
</table>`
|
||||
// Put Table Row
|
||||
table.insertAdjacentHTML("beforeend", row)
|
||||
// Display Operator
|
||||
if (svc.operator) {
|
||||
var opRow = `<p class="msg op">A ${svc.operator} service</p>`
|
||||
table.insertAdjacentHTML("beforeend", opRow);
|
||||
}
|
||||
// Parse cancelReason & delayReason
|
||||
if (svc.cancelReason) {
|
||||
var cancelRow = `<p class="msg">${svc.cancelReason}</p>`
|
||||
table.insertAdjacentHTML("beforeend", cancelRow);
|
||||
}
|
||||
if (svc.delayReason) {
|
||||
var delayRow = `<p class="msg">${svc.delayReason}</p>`
|
||||
table.insertAdjacentHTML("beforeend", delayRow);
|
||||
}
|
||||
}
|
||||
|
||||
async function displayFerryService(svc) {
|
||||
var table = document.getElementById("ferry");
|
||||
log(JSON.stringify(svc))
|
||||
// Determine Time Message
|
||||
var sta = await parseTime(svc.sta);
|
||||
var eta = await parseTime(svc.eta);
|
||||
var std = await parseTime(svc.std);
|
||||
var etd = await parseTime(svc.etd);
|
||||
// Determine Platform Message
|
||||
var plt = "";
|
||||
// Define Table Row
|
||||
var row = `
|
||||
<table>
|
||||
<tr>
|
||||
<td class="name name-item">${await parseName(svc.origin.location)}</td>
|
||||
<td class="name name-item">${await parseName(svc.destination.location)}</td>
|
||||
<td class="plat}">${plt}</td>
|
||||
<td class="time">${sta.data}</td>
|
||||
<td class="time ${eta.changed}">${eta.data}</td>
|
||||
<td class="time">${std.data}</td>
|
||||
<td class="time ${etd.changed}">${etd.data}</td>
|
||||
</tr>
|
||||
</table>`
|
||||
// Put Table Row
|
||||
table.insertAdjacentHTML("beforeend", row)
|
||||
// Parse cancelReason & delayReason
|
||||
if (svc.cancelReason) {
|
||||
var cancelRow = `<p class="msg">${svc.cancelReason}</p>`
|
||||
table.insertAdjacentHTML("beforeend", cancelRow);
|
||||
}
|
||||
if (svc.delayReason) {
|
||||
var delayRow = `<p class="msg">${svc.delayReason}</p>`
|
||||
table.insertAdjacentHTML("beforeend", delayRow);
|
||||
}
|
||||
document.getElementById("ferry").style = "display:block"
|
||||
}
|
||||
|
||||
async function displayBusService(svc) {
|
||||
var table = document.getElementById("bus");
|
||||
log(JSON.stringify(svc))
|
||||
// Determine Time Message
|
||||
var sta = await parseTime(svc.sta);
|
||||
var eta = await parseTime(svc.eta);
|
||||
var std = await parseTime(svc.std);
|
||||
var etd = await parseTime(svc.etd);
|
||||
// Determine Platform Message
|
||||
var plt = "";
|
||||
// Define Table Row
|
||||
var row = `
|
||||
<table>
|
||||
<tr>
|
||||
<td class="name name-item" onclick="showCalls('${svc.serviceID}')">${svc.origin.location.locationName}</td>
|
||||
<td class="name name-item" onclick="showCalls('${svc.serviceID}')">${svc.destination.location.locationName}</td>
|
||||
<td class="plat}">${plt}</td>
|
||||
<td class="time">${sta.data}</td>
|
||||
<td class="time ${eta.changed}">${eta.data}</td>
|
||||
<td class="time">${std.data}</td>
|
||||
<td class="time ${etd.changed}">${etd.data}</td>
|
||||
</tr>
|
||||
</table>`
|
||||
// Put Table Row
|
||||
table.insertAdjacentHTML("beforeend", row)
|
||||
// Display operator
|
||||
if (svc.operator) {
|
||||
var opRow = `<p class="msg op">A ${svc.operator} service</p>`
|
||||
table.insertAdjacentHTML("beforeend", opRow);
|
||||
}
|
||||
// Parse cancelReason & delayReason
|
||||
if (svc.cancelReason) {
|
||||
var cancelRow = `<p class="msg">${svc.cancelReason}</p>`
|
||||
table.insertAdjacentHTML("beforeend", cancelRow);
|
||||
}
|
||||
if (svc.delayReason) {
|
||||
var delayRow = `<p class="msg">${svc.delayReason}</p>`
|
||||
table.insertAdjacentHTML("beforeend", delayRow);
|
||||
}
|
||||
document.getElementById("bus").style = "display:block"
|
||||
}
|
24
static/js/stat.js
Normal file
@ -0,0 +1,24 @@
|
||||
init();
|
||||
|
||||
async function init() {
|
||||
display(await get())
|
||||
}
|
||||
|
||||
async function get() {
|
||||
var url = `${window.location.origin}/api/v1/stats`;
|
||||
var resp = await fetch(url);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
async function display(data) {
|
||||
document.getElementById('server_host').textContent = `HOST: ${data.host}`;
|
||||
let dat = data.dat[0]
|
||||
console.log(JSON.stringify(dat))
|
||||
document.getElementById('time').textContent = dat.since;
|
||||
document.getElementById('ldbws').textContent = dat.ldbws || "0";
|
||||
document.getElementById('ldbsvws').textContent = dat.ldbsvws || "0";
|
||||
document.getElementById('corpus').textContent = dat.corpus || "0";
|
||||
document.getElementById('stations').textContent = dat.stations || "0";
|
||||
document.getElementById('users').textContent = dat.user || "0";
|
||||
document.getElementById('meta').textContent = dat.meta || "0";
|
||||
}
|
33
static/manifest.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "OwlBoard",
|
||||
"short_name": "OwlBoard",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#404c55",
|
||||
"description": "Live station departures - aimed at train-crew.",
|
||||
"categories": "travel,utilities",
|
||||
"lang": "en",
|
||||
"orientation": "portrait",
|
||||
"theme_color": "#00b7b7",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/app-icons/maskable/mask-icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/app-icons/any/plain-logo.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/images/app-icons/any/plain-logo-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
72
static/settings.html
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="OwlBoard - Live train departures for traincrew."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="application-name" content="OwlBoard">
|
||||
<meta name="author" content="Frederick Boniface">
|
||||
<meta name="theme-color" content="#00b7b7">
|
||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png">
|
||||
<link rel="stylesheet" type="text/css" href="./styles/main.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="./styles/settings.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/icon.svg"/>
|
||||
<link rel="manifest" type="application/json" href="./manifest.json"/>
|
||||
<title>OwlBoard - Settings</title>
|
||||
<script src="./js/lib.main.js" defer></script>
|
||||
<script src="./js/settings.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loading">
|
||||
<div class="spinner">
|
||||
</div>
|
||||
<p>Loading</p>
|
||||
</div>
|
||||
|
||||
<div id="done">
|
||||
<!-- Insert white tick SVG Here -->
|
||||
<picture id="save-icon">
|
||||
<source srcset="./images/nav/save.svg" type="image/svg+xml">
|
||||
<img src="./images/nav/save-59.png" alt="">
|
||||
</picture>
|
||||
<p>Saved</p>
|
||||
</div>
|
||||
|
||||
<div id="top_button" class="hide_micro">
|
||||
<a href="/">
|
||||
<picture aria-label="Home" class="sidebar_control">
|
||||
<source srcset="/images/nav/back.svg" type="image/svg+xml">
|
||||
<img src="back-40.png" alt="Home">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<picture>
|
||||
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml">
|
||||
<source media="(max-height: 739px)" srcset="/images/logo/logo-full-200.png" type="image/png">
|
||||
<source srcset="/images/logo/logo-full-250.png" type="image/png">
|
||||
<img class="titleimg" src="/images/logo/logo-full-250.png" alt="OwlBoard Logo">
|
||||
</picture>
|
||||
<h2>Settings</h2>
|
||||
<p>Any settings you apply will only apply to the device you are using now.</p>
|
||||
|
||||
<label>Personal Quick Links:</label><br>
|
||||
<p>Enter one CRS/3ALPHA code per box</p>
|
||||
<input type="text" maxlength="3" id="ql0" name="ql0" autocomplete="off" class="small-lookup-box">
|
||||
<input type="text" maxlength="3" id="ql1" name="ql1" autocomplete="off" class="small-lookup-box"><br>
|
||||
<input type="text" maxlength="3" id="ql2" name="ql2" autocomplete="off" class="small-lookup-box">
|
||||
<input type="text" maxlength="3" id="ql3" name="ql3" autocomplete="off" class="small-lookup-box"><br>
|
||||
<input type="text" maxlength="3" id="ql4" name="ql4" autocomplete="off" class="small-lookup-box">
|
||||
<input type="text" maxlength="3" id="ql5" name="ql5" autocomplete="off" class="small-lookup-box"><br>
|
||||
<input type="text" maxlength="3" id="ql6" name="ql6" autocomplete="off" class="small-lookup-box">
|
||||
<input type="text" maxlength="3" id="ql7" name="ql7" autocomplete="off" class="small-lookup-box"><br>
|
||||
<input type="text" maxlength="3" id="ql8" name="ql8" autocomplete="off" class="small-lookup-box">
|
||||
<input type="text" maxlength="3" id="ql9" name="ql9" autocomplete="off" class="small-lookup-box"><br>
|
||||
<input type="text" maxlength="3" id="ql10" name="ql10" autocomplete="off" class="small-lookup-box">
|
||||
<input type="text" maxlength="3" id="ql11" name="ql11" autocomplete="off" class="small-lookup-box"><br>
|
||||
<button onclick="setQl()" class="lookup-button">Apply</button>
|
||||
<button onclick="clearQl()" class="lookup-button">Defaults</button>
|
||||
|
||||
</body>
|
||||
</html>
|
45
static/stat.html
Normal file
@ -0,0 +1,45 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OwlBoard - STATS</title>
|
||||
<script src="./js/stat.js"></script>
|
||||
</head>
|
||||
<body style="text-align:center">
|
||||
<h1>OwlBoard Server Stats</h1>
|
||||
<h2 id="server_host"></h2>
|
||||
<p>Counters Reset - <span id="time"></span></p>
|
||||
<table style="margin:auto;text-align:center;">
|
||||
<tr>
|
||||
<th>Resource</th>
|
||||
<th>Hit Count</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LDBWS</td>
|
||||
<td id="ldbws"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LDBSVWS</td>
|
||||
<td id="ldbsvws"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DB-CORPUS</td>
|
||||
<td id="corpus"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DB-Stations</td>
|
||||
<td id="stations"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DB-Users</td>
|
||||
<td id="users"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DB-Meta</td>
|
||||
<td id="meta"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<br><br>
|
||||
<p>The statistics represent hits & queries on all servers attached to the database.
|
||||
Multiple servers are served by each database server.</p>
|
||||
</body>
|
||||
</html>
|