Initial Push (v0.0.2)

Signed-off-by: Fred Boniface <fred@fjla.uk>
This commit is contained in:
Fred Boniface 2023-02-09 20:34:53 +00:00
parent 6ea0e9f4bf
commit f2bd261414
121 changed files with 7709 additions and 2 deletions

11
.dockerignore Normal file
View File

@ -0,0 +1,11 @@
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
db-manager
run.sh
LICENSE
*.md
static

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# REVEALS CREDENTIALS
run.sh
# ---> Node
# Logs
logs

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

1
.test-tools/clean.json Normal file

File diff suppressed because one or more lines are too long

15
.test-tools/compose.yaml Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -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
View 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
View 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

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View 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"
}

View File

@ -0,0 +1,10 @@
module.exports = valid
const valid = [
"owlboard.co.uk",
"fjla.uk",
"gwr.com",
"swrailway.com",
"firstrail.com",
"networkrail.co.uk"
]

View File

@ -0,0 +1,6 @@
const version = {
api: ["/api/v1/",],
app: "0.0.2"
};
module.exports = version;

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
View 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;

View 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;

View 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
View 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
View 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;

View 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;

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

90
src/utils/dbinit.utils.js Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
function out(msg) {
var time = new Date().toISOString();
console.log(`${time} - ${msg}`)
}
module.exports = {
out
}

View 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
}

View 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,
}

View 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
View File

@ -0,0 +1,4 @@
.dockerignore
Dockerfile
*.xcf
*.inkscape.svg

34
static/404.html Normal file
View 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
View 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
View 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">&#8897;</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
View 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
View 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
View 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
View 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
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

2
static/images/icon.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

70
static/index.html Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

Some files were not shown because too many files have changed in this diff Show More