* docker: initial config

* docker: add entrypoint for headless run

* docker: resolve path from env variable

* docker: initial config

* docker: rename preloaded data directories

* docker: update README

* docker: prevent issue with port mappings

* docker: update README.md

* docker: add docker-compose

* docker: cleanup logs

* docker: add CI

* docker: cleanup unused

* docker: use static port in development

* refactor: isolate tasks

* docker: add demo db

* fix: download db resolves according to OS

* docker: config build

* fix: ensure db path exists

* fix: add object type to block

* docker: config build

* docker: config build

* docker: env is production

* docker: preload db

* fix: remove test dir

* docker: graceful shutdown node app

* docker: env is production

* docker: add docker-compose

* docker: remove unused mappings

* docker: version bump
This commit is contained in:
Carlos Valente
2022-06-08 21:57:22 +02:00
committed by GitHub
parent 94a7ef6318
commit 9c5c7ee69a
23 changed files with 214 additions and 187 deletions
+14
View File
@@ -0,0 +1,14 @@
# Ignore config files
.prettier*
# Ignore install stuff
**/yarn.lock
**/yarn-error.log
# Ignore test files
**/__tests__/**
**/**.test.js
# Ignore git and cache folders
.git
.cache
-103
View File
@@ -1,103 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
push:
# Pattern matched against refs/tags
tags:
# Push events to every tag not containing /
- '*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# build a file for windows
buildwin:
# The type of runner that the job will run on
runs-on: windows-latest
env:
CI: ''
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# install and build react
- name: Install React dependencies
run: yarn install
working-directory: ./client
- name: Build React App
run: yarn build
working-directory: ./client
# install node dependencies
- name: Install nodejs dependencies
run: yarn install
working-directory: ./server/src
# install and build electron
- name: Install Electron dependencies
run: yarn install
working-directory: ./server
- name: Build Electron App
run: yarn dist-win
working-directory: ./server
# release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-win64.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# build a file for mac
# buildmac:
# The type of runner that the job will run on
# runs-on: macOS-latest
# env:
# CI: ''
# Steps represent a sequence of tasks that will be executed as part of the job
# steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
# - uses: actions/checkout@v2
# - name: Use Node.js
# uses: actions/setup-node@v1
# with:
# node-version: '14.x'
# install and build react
# - name: Install React dependencies
# run: yarn install
# working-directory: ./client
# - name: Build React App
# run: yarn build
# working-directory: ./client
# install and build electron
# - name: Install Electron + nodejs dependencies
# run: yarn install
# working-directory: ./server
# - name: Build Electron App
# run: yarn dist-win
# working-directory: ./server
# release
# - name: Release
# uses: softprops/action-gh-release@v1
# with:
# files: ./server/dist/ontime-macOS.dmg
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+54 -8
View File
@@ -22,7 +22,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile
working-directory: ./client
- name: React - Build project
@@ -31,13 +31,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --production
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install && yarn setdb
run: yarn install --frozen-lockfile && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-mac
@@ -65,7 +65,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile
working-directory: ./client
- name: React - Build project
@@ -74,13 +74,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --production
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install && yarn setdb
run: yarn install --frozen-lockfile && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-win
@@ -108,7 +108,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile
working-directory: ./client
- name: React - Build project
@@ -117,7 +117,7 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --production
working-directory: ./server/src
# App
@@ -136,3 +136,49 @@ jobs:
files: ./server/dist/ontime-linux.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish_docker:
runs-on: ubuntu-latest
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Setup env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production
working-directory: ./server/src
# Login to docker
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Build and push
- name: Build and push
working-directory: ./
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }}
+2 -1
View File
@@ -28,9 +28,10 @@ yarn-error.log*
# working stuff
_SS/
db backup.json
server/src/data/db.json
server/src/preloaded-db/db.json
server/src/models/db.json
TODO.md
ontime-db/
# vscode stuff
.vscode/*
+20
View File
@@ -0,0 +1,20 @@
FROM node:14-alpine
WORKDIR /app/server
# Prepare UI
COPY /client/build ../client/build
# Prepare Backend
COPY /server/src ./
# Export default ports Main - OSC IN
EXPOSE 4001/tcp 8888/udp
ENV NODE_ENV=production
ENV ONTIME_DATA=/server/
CMD ["yarn", "start:headless"]
# Build an run commandsN
# docker build -t getontime/ontime .
# docker run -p 4001:4001 -p 10.0.0.12:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
+25 -1
View File
@@ -78,6 +78,8 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- [x] Logging view
- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (
same as app)
- [x] Multi platform (available on Windows, MacOS and Linux)
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
## Unopinionated
@@ -115,6 +117,29 @@ See [this repository](https://github.com/cpvalente/ontime-viewer-template) with
how to get you started and read the docs about
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
### Headless run
You can self host and run ontime in a docker image, the run command should:
- expose the necessary ports (listen in Dockerfile)
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
- the image name __getontime/ontime__
The docker image is in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
```bash
docker pull getontime/ontime
```
```bash
# Port 4001 - ontime server port
# Port 8888 - OSC input, bound to localhost IP Address
docker run -p 4001:4001 -p 127.0.0.1:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
```
or if running from the docker compose
```bash
docker-compose up
```
## Roadmap
### Continued development
@@ -126,7 +151,6 @@ friendly order unless there is user demand to bump any of them.
- [ ] Improvement with event component design
- [ ] New playback mode
for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
- [ ] Headless version (run server only anywhere, configure from a browser locally)
- [ ] Companion module
- [ ] Lower Third Manager
- [ ] Note only event
+10 -7
View File
@@ -1,4 +1,4 @@
export const NODE_PORT = 4001;
export const STATIC_PORT = 4001;
export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const USERFIELDS = 'userFields';
@@ -8,15 +8,18 @@ export const OSC_SETTINGS = 'oscSettings';
export const APP_SETTINGS = 'appSettings';
/**
* @description finds server path given the current location
* @description finds server path given the current location, it
* @return {*}
*/
const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
if (process.env?.NODE_ENV === 'development') {
return `http://localhost:${STATIC_PORT}`;
}
return window.location.origin;
};
export const serverURL = calculateServer();
export const eventURL = serverURL + EVENT_TABLE;
export const eventsURL = serverURL + EVENTS_TABLE;
export const playbackURL = `${serverURL}playback`;
export const ontimeURL = `${serverURL}ontime`;
export const eventURL = `${serverURL}/${EVENT_TABLE}`;
export const eventsURL = `${serverURL}/${EVENTS_TABLE}`;
export const playbackURL = `${serverURL}/playback`;
export const ontimeURL = `${serverURL}/ontime`;
+3 -1
View File
@@ -222,7 +222,9 @@ export const downloadEvents = async () => {
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(new Blob([response.data]));
const url = window.URL.createObjectURL(
new Blob([response.data], { type: 'application/json' })
);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
+12 -21
View File
@@ -154,15 +154,16 @@ const withSocket = (Component) => {
// Filter events only to pass down
useEffect(() => {
if (eventsData == null) return;
// filter just events with title
const pe = eventsData.filter(
(d) => d.type === 'event' && d.title !== '' && d.isPublic === true
);
setPublicEvents(pe);
if (Array.isArray(eventsData)) {
const pe = eventsData.filter(
(d) => d.type === 'event' && d.title !== '' && d.isPublic
);
setPublicEvents(pe);
// everything goes backstage
setBackstageEvents(eventsData);
// everything goes backstage
setBackstageEvents(eventsData);
}
}, [eventsData]);
// Set general data
@@ -178,13 +179,11 @@ const withSocket = (Component) => {
/********************************************/
// is there a now field?
let showNow = true;
if (!titles.titleNow && !titles.subtitleNow && !titles.presenterNow)
showNow = false;
if (!titles.titleNow && !titles.subtitleNow && !titles.presenterNow) showNow = false;
// is there a next field?
let showNext = true;
if (!titles.titleNext && !titles.subtitleNext && !titles.presenterNext)
showNext = false;
if (!titles.titleNext && !titles.subtitleNext && !titles.presenterNext) showNext = false;
const titleManager = { ...titles, showNow: showNow, showNext: showNext };
@@ -195,20 +194,12 @@ const withSocket = (Component) => {
/********************************************/
// is there a now field?
let showPublicNow = true;
if (
!publicTitles.titleNow &&
!publicTitles.subtitleNow &&
!publicTitles.presenterNow
)
if (!publicTitles.titleNow && !publicTitles.subtitleNow && !publicTitles.presenterNow)
showPublicNow = false;
// is there a next field?
let showPublicNext = true;
if (
!publicTitles.titleNext &&
!publicTitles.subtitleNext &&
!publicTitles.presenterNext
)
if (!publicTitles.titleNext && !publicTitles.subtitleNext && !publicTitles.presenterNext)
showPublicNext = false;
const publicTitleManager = {
+12
View File
@@ -0,0 +1,12 @@
version: "3"
services:
ontime:
container_name: ontime
image: getontime/ontime:test-alpine
ports:
- "4001:4001/tcp"
- "127.0.0.1:8888:8888/udp"
volumes:
- "./ontime-db:/server/preloaded-db"
restart: unless-stopped
+6 -4
View File
@@ -11,13 +11,16 @@ const {
} = require('electron');
const path = require('path');
const env = process.env.NODE_ENV || 'prod';
if (process.env.NODE_ENV === undefined) {
process.env.NODE_ENV = 'production';
}
const env = process.env.NODE_ENV;
let loaded = 'Nothing loaded';
let isQuitting = false;
const nodePath =
env !== 'prod'
env !== 'production'
? path.join('file://', __dirname, 'src/app.js')
: path.join('file://', __dirname, '../', 'extraResources', 'src/app.js');
@@ -142,7 +145,7 @@ app.whenReady().then(() => {
setTimeout(() => {
// Load page served by node
const reactApp =
env === 'prod' ? 'http://localhost:4001/editor' : 'http://localhost:3000/editor';
env === 'development' ? 'http://localhost:3000/editor' : 'http://localhost:4001/editor';
win.loadURL(reactApp).then(() => {
win.webContents.setBackgroundThrottling(false);
@@ -154,7 +157,6 @@ app.whenReady().then(() => {
splash.destroy();
// tray stuff
// TODO: get IP Address
tray.setToolTip(loaded);
});
}, 2000);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "1.1.1",
"version": "1.2.0",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
@@ -27,7 +27,7 @@
},
"scripts": {
"nodestart": "NODE_ENV=development node src/app.js",
"setdb": "cp data/db.json src/data/db.json",
"setdb": "cp demo-db/db.json src/preloaded-db/db.json",
"clean": "rm -rf ../client/build/ && rm -rf ../client/node_modules && rm -rf src/node_modules && rm -rf ./node_modules && rm -rf ./dist",
"prep": "yarn clean && yarn setdb",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
+9 -11
View File
@@ -26,7 +26,7 @@ import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
import { fileURLToPath } from 'url';
// get environment
const env = process.env.NODE_ENV || 'prod';
const env = process.env.NODE_ENV || 'production';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -56,11 +56,11 @@ app.use('/ontime', ontimeRouter);
app.use('/playback', playbackRouter);
// serve react
app.use(express.static(join(__dirname, env === 'prod' ? '../' : '../../', 'client/build')));
app.use(express.static(join(__dirname, env === 'production' ? '../' : '../../', 'client/build')));
app.get('*', (req, res) => {
res.sendFile(
resolve(__dirname, env === 'prod' ? '../' : '../../', 'client', 'build', 'index.html')
resolve(__dirname, env === 'production' ? '../' : '../../', 'client', 'build', 'index.html'),
);
});
@@ -107,14 +107,7 @@ export const startOSCServer = async (overrideConfig = null) => {
const server = http.createServer(app);
export const startServer = async (overrideConfig = null) => {
// Setup default port
// const port = overrideConfig?.port || serverPort;
/* Note: Port is hardcoded
* need to find a good solution for sharing
* the dynamic info with the frontend
*/
const port = 4001;
const port = 4001; // port hardcoded
// Start server
const returnMessage = `Ontime is listening on port ${port}`;
@@ -145,4 +138,9 @@ export const shutdown = async () => {
global.timer.shutdown();
};
// register shutdown signals
process.once('SIGHUP', shutdown)
process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)
export { server, app };
+1 -1
View File
@@ -1403,7 +1403,7 @@ export class EventTimer extends Timer {
this.messageStack.unshift(m);
this.io.emit('logger', m);
if (process.env.NODE_ENV !== 'prod') {
if (process.env.NODE_ENV !== 'production') {
console.log(`[${m.level}] \t ${m.origin} \t ${m.text}`);
}
+1
View File
@@ -6,6 +6,7 @@ export const config = {
port: 4001,
},
database: {
directory: 'preloaded-db',
filename: 'db.json',
tablename: 'events',
},
+3 -9
View File
@@ -1,15 +1,9 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// get database
import { data, db } from '../app.js';
import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js';
import { generateId } from '../utils/generate_id.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import { resolveDbPath } from '../modules/loadDb.js';
// Create controller for GET request to '/ontime/poll'
// Returns data for current state
@@ -28,9 +22,9 @@ export const poll = async (req, res) => {
// Returns -
export const dbDownload = async (req, res) => {
const fileTitle = data?.event?.title || 'ontime events';
const dbFile = path.resolve(__dirname, '../', 'data/db.json');
const dbInDisk = resolveDbPath();
res.download(dbFile, `${fileTitle}.json`, (err) => {
res.download(dbInDisk, `${fileTitle}.json`, (err) => {
if (err) {
res.status(500).send({
message: 'Could not download the file. ' + err,
View File
+19 -8
View File
@@ -8,16 +8,27 @@ import { dbModelv1 as dbModel } from '../models/dataModel.js';
import { parseJson_v1 as parseJson } from '../utils/parser.js';
/**
* @description ensures directories exist and picks available db path
* @description Decides which path the database is in
* @param ensure - whether it should create directory if it doesnt exist
* @return {string}
*/
export const resolveDbPath = (ensure = true) => {
const appPath = getAppDataPath();
const dbDirectory = join(appPath, config.database.directory);
if (ensure) {
ensureDirectory(dbDirectory);
}
return join(dbDirectory, config.database.filename);
};
/**
* @description ensures directories exist and populates database
* @param runningDirectory
* @return {string}
*/
const checkDirectories = (runningDirectory) => {
const appPath = getAppDataPath();
const dbDirectory = join(appPath, 'data');
const dbInDisk = join(dbDirectory, config.database.filename);
const startupDb = join(runningDirectory, 'data', config.database.filename);
ensureDirectory(dbDirectory);
const populateDb = (runningDirectory) => {
const startupDb = join(runningDirectory, config.database.directory, config.database.filename);
const dbInDisk = resolveDbPath();
// if dbInDisk doesnt exist we want to use startup db
if (!existsSync(dbInDisk)) {
@@ -53,7 +64,7 @@ const parseDb = async (fileToRead, adapterToUse) => {
* @return {Promise<{data: (number|*), db: Low<unknown>}>}
*/
export default async function loadDb(runningDirectory) {
const dbInDisk = checkDirectories(runningDirectory);
const dbInDisk = populateDb(runningDirectory);
const adapter = new JSONFile(dbInDisk);
const db = new Low(adapter);
+2 -1
View File
@@ -22,6 +22,7 @@
},
"scripts": {
"nodestart": "nodemon app.js",
"start": "node app.js"
"start": "node app.js",
"start:headless": "NODE_ENV=production node run.js"
}
}
+14
View File
@@ -0,0 +1,14 @@
(async () => {
let loaded;
try {
const { startServer, startOSCServer } = await import('./app.js');
// Start express server
loaded = await startServer();
// Start OSC Server (API)
await startOSCServer();
} catch (error) {
console.log(error);
}
console.log(loaded);
})();
+5 -9
View File
@@ -15,20 +15,16 @@ export function ensureDirectory(directory) {
}
}
/**
* @description Whether a file exists
* @param directory
* @return {boolean}
*/
export function doesFileExist(directory) {
return existsSync(directory);
}
/**
* @description Returns public path depending on OS
* @return {string|*}
*/
export function getAppDataPath() {
// handle docker
if (process.env.ONTIME_DATA) {
return path.join(process.env.ONTIME_DATA);
}
switch (process.platform) {
case 'darwin': {
return path.join(process.env.HOME, 'Library', 'Application Support', 'Ontime');