Dev
This commit is contained in:
parent
bd01209283
commit
9b2bf554b1
18 changed files with 760 additions and 0 deletions
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# Local development config file
|
||||
config.json
|
||||
package-lock.json
|
||||
|
||||
public/assets/app.js
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
24
api/config.mjs
Normal file
24
api/config.mjs
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Nconf from 'nconf-lite'
|
||||
|
||||
const nconf = new Nconf()
|
||||
|
||||
// Helper method for global usage.
|
||||
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||
|
||||
// Load env
|
||||
nconf.overrides({
|
||||
NODE_ENV: (process.env.NODE_ENV || 'development').toLocaleLowerCase()
|
||||
})
|
||||
|
||||
// Load empty overrides that can be overwritten later
|
||||
nconf.overrides({})
|
||||
|
||||
nconf.defaults({
|
||||
"frontend": {
|
||||
"url": "http://streamer.filadelfia.is"
|
||||
},
|
||||
"jwtsecret": "w2bkdWAButfdfEkCs8dpE3L2n6QzCfhna0T4"
|
||||
})
|
||||
|
||||
|
||||
export default nconf
|
14
api/encoder/routes.mjs
Normal file
14
api/encoder/routes.mjs
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
export default class EncoderRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, opts)
|
||||
}
|
||||
|
||||
registerIo(server, ctx) {
|
||||
ctx.socket.safeOn('encoder.status', this.status.bind(this))
|
||||
}
|
||||
|
||||
status(ctx) {
|
||||
ctx.socket.emit('encoder.status', { running: false })
|
||||
}
|
||||
}
|
59
api/io.mjs
Normal file
59
api/io.mjs
Normal file
|
@ -0,0 +1,59 @@
|
|||
import socket from 'socket.io-serveronly'
|
||||
|
||||
export default class SocketServer {
|
||||
constructor(db, log, routes) {
|
||||
this.db = db
|
||||
this.log = log
|
||||
this.routes = routes
|
||||
}
|
||||
|
||||
init(server) {
|
||||
this.io = new socket(server)
|
||||
this.io.on('connection', this.onNewConnection.bind(this))
|
||||
}
|
||||
|
||||
onNewConnection(socket) {
|
||||
let ctx = {
|
||||
io: this.io,
|
||||
socket: socket,
|
||||
log: this.log.child({ id: socket.id }),
|
||||
db: this.db
|
||||
}
|
||||
|
||||
ctx.log.info('New socket connection', { id: socket.id })
|
||||
|
||||
ctx.socket.safeOn = this.socketSafeOn.bind(this, ctx)
|
||||
|
||||
this.register(ctx)
|
||||
|
||||
ctx.socket.emit('data', { bla: 'test' })
|
||||
}
|
||||
|
||||
register(ctx) {
|
||||
let keys = Object.keys(this.routes)
|
||||
for (let key of keys) {
|
||||
if (this.routes[key].registerIo) {
|
||||
this.routes[key].registerIo(this, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
socketSafeOn(ctx, name, fn) {
|
||||
ctx.socket.on(name, data => {
|
||||
ctx.log.info('IO: ' + name)
|
||||
let res;
|
||||
try {
|
||||
res = fn(ctx, data)
|
||||
} catch (err) {
|
||||
ctx.log.error(error, `Error processing ${name}`)
|
||||
}
|
||||
|
||||
if (res && typeof(res.then) === 'function') {
|
||||
res.then(
|
||||
() => {},
|
||||
error => ctx.log.error(error, `Error processing ${name}`)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
61
api/serve.mjs
Normal file
61
api/serve.mjs
Normal file
|
@ -0,0 +1,61 @@
|
|||
import path from 'path'
|
||||
import { FileResponse } from 'flaska'
|
||||
import fs from 'fs/promises'
|
||||
import fsSync from 'fs'
|
||||
|
||||
export default class ServeHandler {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, opts)
|
||||
Object.assign(this, {
|
||||
fs: this.fs || fs,
|
||||
fsSync: this.fsSync || fsSync,
|
||||
frontend: this.frontend || 'http://localhost:4000',
|
||||
version: this.version || 'version',
|
||||
})
|
||||
|
||||
this.index = fsSync.readFileSync(path.join(this.root, 'index.html'))
|
||||
}
|
||||
|
||||
register(server) {
|
||||
server.flaska.get('/::file', this.serve.bind(this))
|
||||
}
|
||||
|
||||
/** GET: /::file */
|
||||
serve(ctx) {
|
||||
if (ctx.params.file.startsWith('api/')) {
|
||||
return this.serveIndex(ctx)
|
||||
}
|
||||
|
||||
let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html'))
|
||||
|
||||
if (!ctx.params.file
|
||||
|| ctx.params.file === 'index.html') {
|
||||
return this.serveIndex(ctx)
|
||||
}
|
||||
|
||||
if (!file.startsWith(this.root)) {
|
||||
return this.serveIndex(ctx)
|
||||
}
|
||||
|
||||
return this.fs.stat(file)
|
||||
.then(function(stat) {
|
||||
ctx.headers['Cache-Control'] = 'no-store'
|
||||
ctx.body = new FileResponse(file, stat)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
return this.serveIndex(ctx)
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
async serveIndex(ctx) {
|
||||
ctx.body = this.index
|
||||
ctx.type = 'text/html; charset=utf-8'
|
||||
}
|
||||
|
||||
serveErrorPage(ctx) {
|
||||
|
||||
}
|
||||
}
|
139
api/server.mjs
Normal file
139
api/server.mjs
Normal file
|
@ -0,0 +1,139 @@
|
|||
import { Flaska, QueryHandler, JsonHandler, HttpError } from 'flaska'
|
||||
|
||||
import config from './config.mjs'
|
||||
import StaticRoutes from './static_routes.mjs'
|
||||
import ServeHandler from './serve.mjs'
|
||||
import SocketServer from './io.mjs'
|
||||
import EncoderRoutes from './encoder/routes.mjs'
|
||||
|
||||
export default class Server {
|
||||
constructor(http, port, core, opts = {}) {
|
||||
Object.assign(this, opts)
|
||||
Object.assign(this, {
|
||||
http, port, core,
|
||||
})
|
||||
let localUtil = new this.core.sc.Util(import.meta.url)
|
||||
|
||||
this.flaskaOptions = {
|
||||
appendHeaders: {
|
||||
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; connect-src 'self' https://media.nfp.is/; media-src 'self' https://cdn.nfp.is/`,
|
||||
},
|
||||
nonce: [],
|
||||
log: this.core.log,
|
||||
}
|
||||
this.jsonHandler = JsonHandler
|
||||
|
||||
this.routes = {
|
||||
static: new StaticRoutes(),
|
||||
encoder: new EncoderRoutes(),
|
||||
}
|
||||
this.routes.serve = new ServeHandler({
|
||||
root: localUtil.getPathFromRoot('../public'),
|
||||
version: this.core.app.running,
|
||||
frontend: config.get('frontend:url'),
|
||||
})
|
||||
}
|
||||
|
||||
runCreateServer() {
|
||||
// Create our server
|
||||
this.flaska = new Flaska(this.flaskaOptions, this.http)
|
||||
|
||||
// configure our server
|
||||
if (config.get('NODE_ENV') === 'development') {
|
||||
this.flaska.devMode()
|
||||
}
|
||||
|
||||
this.flaska.onerror((err, ctx) => {
|
||||
if (err instanceof HttpError && err.status !== 500) {
|
||||
ctx.status = err.status
|
||||
ctx.log.warn(err.message)
|
||||
} else {
|
||||
ctx.log.error(err.inner || err)
|
||||
if (err.extra) {
|
||||
ctx.log.error({ extra: err.extra }, 'Database parameters')
|
||||
}
|
||||
ctx.status = 500
|
||||
}
|
||||
ctx.body = {
|
||||
status: ctx.status,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
|
||||
this.flaska.before(function(ctx) {
|
||||
ctx.state.started = new Date().getTime()
|
||||
ctx.req.ip = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress
|
||||
ctx.log = ctx.log.child({
|
||||
id: Math.random().toString(36).substring(2, 14),
|
||||
})
|
||||
ctx.db = this.pool
|
||||
}.bind(this))
|
||||
this.flaska.before(QueryHandler())
|
||||
|
||||
let healthChecks = 0
|
||||
let healthCollectLimit = 60 * 60 * 12
|
||||
|
||||
this.flaska.after(function(ctx) {
|
||||
if (ctx.aborted && ctx.status === 200) {
|
||||
ctx.status = 299
|
||||
}
|
||||
let ended = new Date().getTime()
|
||||
var requestTime = ended - ctx.state.started
|
||||
|
||||
let status = ''
|
||||
let level = 'info'
|
||||
if (ctx.status >= 400) {
|
||||
status = ctx.status + ' '
|
||||
level = 'warn'
|
||||
}
|
||||
if (ctx.status >= 500) {
|
||||
level = 'error'
|
||||
}
|
||||
|
||||
if (ctx.url === '/health' || ctx.url === '/api/health') {
|
||||
healthChecks++
|
||||
if (healthChecks >= healthCollectLimit) {
|
||||
ctx.log[level]({
|
||||
duration: Math.round(ended),
|
||||
status: ctx.status,
|
||||
}, `<-- ${status}${ctx.method} ${ctx.url} {has happened ${healthChecks} times}`)
|
||||
healthChecks = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.log[level]({
|
||||
duration: requestTime,
|
||||
status: ctx.status,
|
||||
ip: ctx.req.ip,
|
||||
}, (ctx.aborted ? '[ABORT]' : '<--') + ` ${status}${ctx.method} ${ctx.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
runRegisterRoutes() {
|
||||
let keys = Object.keys(this.routes)
|
||||
for (let key of keys) {
|
||||
if (this.routes[key].register) {
|
||||
this.routes[key].register(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runCreateSocket() {
|
||||
this.io = new SocketServer(this.db, this.core.log, this.routes)
|
||||
this.io.init(this.flaska.server)
|
||||
}
|
||||
|
||||
runStartListen() {
|
||||
return this.flaska.listenAsync(this.port).then(() => {
|
||||
this.core.log.info('Server is listening on port ' + this.port)
|
||||
this.runCreateSocket()
|
||||
})
|
||||
}
|
||||
|
||||
run() {
|
||||
this.runCreateServer()
|
||||
this.runRegisterRoutes()
|
||||
return this.runStartListen()
|
||||
}
|
||||
}
|
17
api/static_routes.mjs
Normal file
17
api/static_routes.mjs
Normal file
|
@ -0,0 +1,17 @@
|
|||
import config from './config.mjs'
|
||||
|
||||
export default class StaticRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, { })
|
||||
}
|
||||
|
||||
register(server) {
|
||||
server.flaska.get('/api/health', this.health.bind(this))
|
||||
}
|
||||
|
||||
health(ctx) {
|
||||
ctx.body = {
|
||||
environment: config.get('NODE_ENV'),
|
||||
}
|
||||
}
|
||||
}
|
78
app/api/client.js
Normal file
78
app/api/client.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const m = require('mithril')
|
||||
var io = require('./socket')
|
||||
var socket = io()
|
||||
|
||||
class Client {
|
||||
constructor(socket) {
|
||||
this.socket = socket
|
||||
this.isConnected = false
|
||||
|
||||
this.socket.on('disconnect', this.disconnected.bind(this))
|
||||
this.socket.on('connect', this.connected.bind(this))
|
||||
this.components = new Map()
|
||||
}
|
||||
|
||||
registerComponent(component) {
|
||||
this.components.set(component, [])
|
||||
if (component.ioInit) {
|
||||
component.ioInit()
|
||||
}
|
||||
if (this.isConnected && component.ioConnected) {
|
||||
component.ioConnected()
|
||||
}
|
||||
}
|
||||
|
||||
safeGetComponent(component) {
|
||||
if (typeof(component) === 'string') {
|
||||
throw new Error('io.on was called without specifying active component')
|
||||
}
|
||||
let arr = this.components.get(component)
|
||||
if (!arr) {
|
||||
throw new Error('Registered component was missing but was attempting to add socket listener')
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
on(component, name, fn) {
|
||||
let arr = this.safeGetComponent(component)
|
||||
arr.push([name, fn])
|
||||
|
||||
this.socket.on(name, (data) => {
|
||||
Promise.resolve().then(() => {
|
||||
return fn(data)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error running io handler for ' + name, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emit(name, data) {
|
||||
this.socket.emit(name, data)
|
||||
}
|
||||
|
||||
unregisterComponent(component) {
|
||||
let arr = this.safeGetComponent(component)
|
||||
for (let item of arr) {
|
||||
this.socket.off(item[0], item[1])
|
||||
}
|
||||
this.components.delete(component)
|
||||
}
|
||||
|
||||
disconnected() {
|
||||
this.isConnected = false
|
||||
m.redraw()
|
||||
}
|
||||
|
||||
connected() {
|
||||
this.isConnected = true
|
||||
for (let component of this.components) {
|
||||
if (component[0].ioConnected) {
|
||||
component[0].ioConnected()
|
||||
}
|
||||
}
|
||||
m.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Client(socket)
|
8
app/api/socket.js
Normal file
8
app/api/socket.js
Normal file
File diff suppressed because one or more lines are too long
21
app/index.js
Normal file
21
app/index.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const m = require('mithril')
|
||||
const Status = require('./page_status')
|
||||
window.m = m
|
||||
|
||||
m.route.setOrig = m.route.set
|
||||
m.route.set = function(path, data, options){
|
||||
m.route.setOrig(path, data, options)
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
m.route.linkOrig = m.route.link
|
||||
m.route.link = function(vnode){
|
||||
m.route.linkOrig(vnode)
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
m.route.prefix = ''
|
||||
|
||||
m.route(document.getElementById('main'), '/', {
|
||||
'/': Status,
|
||||
})
|
30
app/page_status.js
Normal file
30
app/page_status.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const m = require('mithril')
|
||||
const client = require('./api/client')
|
||||
|
||||
const Status = {
|
||||
oninit: function(vnode) {
|
||||
client.registerComponent(this)
|
||||
},
|
||||
|
||||
onremove: function(vnode) {
|
||||
client.unregisterComponent(this)
|
||||
},
|
||||
|
||||
ioInit: function() {
|
||||
client.on(this, 'encoder.status', status => {
|
||||
console.log('status', status)
|
||||
})
|
||||
},
|
||||
|
||||
ioConnected: function() {
|
||||
client.emit('encoder.status')
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m('div', `Hello world, connection status: ${client.isConnected}`),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Status
|
103
arduino/arduino.ino
Normal file
103
arduino/arduino.ino
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
Blink
|
||||
|
||||
The circuit:
|
||||
* VFD Data to digital pin 2 (blue)
|
||||
* VFD Clock to digital pin 3 (yellow)
|
||||
* VFD Chip select to digital pin 4 (green)
|
||||
* VFD VCC (power) to 5V
|
||||
* VFD Ground (power) to Ground
|
||||
*/
|
||||
|
||||
// include the library code:
|
||||
#include <SPI_VFD.h>
|
||||
|
||||
// initialize the library with the numbers of the interface pins
|
||||
SPI_VFD vfd(2, 3, 4);
|
||||
|
||||
// | 1 2|
|
||||
// |12345678901234567890|
|
||||
String vfd_line_top = "Booting up... ";
|
||||
String vfd_line_bottom = "Waiting for stream. ";
|
||||
unsigned int vfd_line_position = 0;
|
||||
bool vfd_line_print = false;
|
||||
unsigned long millis_last_incoming_ping = 0;
|
||||
unsigned long millis_last_outgoing_ping = 0;
|
||||
|
||||
// the setup function runs once when you press reset or power the board
|
||||
void setup() {
|
||||
// initialize digital pin LED_BUILTIN as an output.
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
digitalWrite(LED_BUILTIN, LOW);
|
||||
|
||||
Serial.begin(9600);
|
||||
|
||||
// set up the VFD's number of columns and rows:
|
||||
vfd.begin(20, 2);
|
||||
update_display();
|
||||
}
|
||||
|
||||
void update_display() {
|
||||
vfd.setCursor(0, 0);
|
||||
vfd.print(vfd_line_top);
|
||||
vfd.setCursor(0, 1);
|
||||
vfd.print(vfd_line_bottom);
|
||||
}
|
||||
|
||||
void check_serial() {
|
||||
while (Serial.available() > 0) {
|
||||
millis_last_incoming_ping = millis();
|
||||
int incomingByte = Serial.read();
|
||||
if (incomingByte == 0 || incomingByte == '^') {
|
||||
incomingByte = vfd_line_position = 0;
|
||||
}
|
||||
if (incomingByte < 32) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (vfd_line_position < 20) {
|
||||
vfd_line_top.setCharAt(vfd_line_position, incomingByte);
|
||||
} else {
|
||||
vfd_line_bottom.setCharAt(vfd_line_position - 20, incomingByte);
|
||||
}
|
||||
vfd_line_position = (vfd_line_position + 1) % 40;
|
||||
vfd_line_print = vfd_line_position == 0;
|
||||
if (vfd_line_print) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the loop function runs over and over again forever
|
||||
void loop() {
|
||||
// We've been running for over 50 days and wrapped to zero. Reset all our timers
|
||||
if (millis() < 10000 && (millis_last_outgoing_ping > 10000 || millis_last_incoming_ping > 10000)) {
|
||||
millis_last_outgoing_ping = 0;
|
||||
millis_last_incoming_ping = 0;
|
||||
}
|
||||
|
||||
check_serial();
|
||||
|
||||
// If it's been 10 seconds since last incoming data
|
||||
if (millis() - millis_last_incoming_ping > 30000) {
|
||||
// |12345678901234567890|
|
||||
vfd_line_top = "Error stream is down";
|
||||
vfd_line_bottom = "Verify or restart...";
|
||||
vfd_line_print = true;
|
||||
millis_last_incoming_ping = millis();
|
||||
}
|
||||
|
||||
// Send outgoing ping every 5 seconds if we can.
|
||||
if (millis() - millis_last_outgoing_ping > 30000 && Serial.availableForWrite()) {
|
||||
Serial.write('^');
|
||||
millis_last_outgoing_ping = millis();
|
||||
}
|
||||
|
||||
// If we have something to print, print it
|
||||
if (vfd_line_print) {
|
||||
vfd_line_print = false;
|
||||
update_display();
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
38
index.mjs
Normal file
38
index.mjs
Normal file
|
@ -0,0 +1,38 @@
|
|||
import fs from 'fs'
|
||||
import { pathToFileURL } from 'url'
|
||||
import config from './api/config.mjs'
|
||||
|
||||
export function start(http, port, ctx) {
|
||||
config.sources[1].store = ctx.config
|
||||
|
||||
return import('./api/server.mjs')
|
||||
.then(function(module) {
|
||||
let server = new module.default(http, port, ctx)
|
||||
return server.run()
|
||||
})
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
import('service-core').then(core => {
|
||||
const port = 4510
|
||||
|
||||
var core = new core.ServiceCore('filadelfia_streamer', import.meta.url, port, '')
|
||||
|
||||
let config = {
|
||||
frontend: {
|
||||
url: 'http://localhost:' + port
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync('./config.json'))
|
||||
} catch {}
|
||||
|
||||
config.port = port
|
||||
|
||||
core.setConfig(config)
|
||||
core.init({ start }).then(function() {
|
||||
return core.run()
|
||||
})
|
||||
})
|
||||
}
|
49
package.json
Normal file
49
package.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "church_streamer",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"start": "node index.mjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "esbuild app/index.js --bundle --outfile=public/assets/app.js",
|
||||
"dev:build": "eltro --watch build --npm build",
|
||||
"dev:server": "eltro --watch server --npm server",
|
||||
"dev:build:old": "npm-watch build",
|
||||
"dev:server:old": "npm-watch server",
|
||||
"server": "node index.mjs | bunyan"
|
||||
},
|
||||
"watch": {
|
||||
"server": {
|
||||
"patterns": [
|
||||
"api"
|
||||
],
|
||||
"extensions": "js,mjs",
|
||||
"quiet": true,
|
||||
"inherit": true
|
||||
},
|
||||
"build": {
|
||||
"patterns": [
|
||||
"app"
|
||||
],
|
||||
"extensions": "js",
|
||||
"quiet": true,
|
||||
"inherit": true
|
||||
}
|
||||
},
|
||||
"author": "Jonatan Nilsson",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"bunyan-lite": "^1.2.1",
|
||||
"flaska": "^1.3.4",
|
||||
"nconf-lite": "^2.0.0",
|
||||
"serialport": "^12.0.0",
|
||||
"socket.io-serveronly": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eltro": "^1.4.4",
|
||||
"esbuild": "^0.19.5",
|
||||
"mithril": "^2.2.2",
|
||||
"service-core": "^3.0.0-beta.17"
|
||||
}
|
||||
}
|
25
public/assets/app.css
Normal file
25
public/assets/app.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
:root { --bg: #fff; --bg-component: #f3f7ff; --bg-component-half: #f3f7ff77; --bg-component-alt: #ffd99c; --color: #031131; --color-alt: #7a9ad3; --main: #18597d; --main-fg: #fff; --error: red; --error-bg: hsl(0, 75%, 80%); } /* Box sizing rules */
|
||||
*, *::before, *::after { box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Remove default margin */
|
||||
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; }
|
||||
|
||||
body { min-height: 100vh; text-rendering: optimizeSpeed; line-height: 1.5; font-size: 16px; font-family: 'Inter var', Helvetica, Arial, sans-serif; font-variation-settings: "slnt" 0; font-feature-settings: "case", "frac", "tnum", "ss02", "calt", "ccmp", "kern"; background: var(--bg); color: var(--color); display: flex; flex-direction: column; }
|
||||
|
||||
.italic { font-variation-settings: "slnt" 10deg; }
|
||||
|
||||
input, button, textarea, select { font: inherit; }
|
||||
|
||||
h1 { font-size: 1.88rem; }
|
||||
h2 { font-size: 1.66rem; }
|
||||
h3 { font-size: 1.44rem; }
|
||||
h4 { font-size: 1.22rem; }
|
||||
h5 { font-size: 1.0rem; }
|
||||
|
||||
.row { display: flex; }
|
||||
.column { display: flex; flex-direction: column; }
|
||||
.filler { flex-grow: 2; }
|
13
public/assets/font.css
Normal file
13
public/assets/font.css
Normal file
File diff suppressed because one or more lines are too long
16
public/index.html
Normal file
16
public/index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Filadelfia Streamer</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/assets/font.css">
|
||||
<link rel="stylesheet" type="text/css" href="/assets/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="main"></main>
|
||||
<script type="text/javascript" src="/assets/app.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue