diff --git a/api/core/coremonitor.mjs b/api/core/coremonitor.mjs index 1c6a2f5..e720a65 100644 --- a/api/core/coremonitor.mjs +++ b/api/core/coremonitor.mjs @@ -1,7 +1,46 @@ import { formatLog } from './loghelper.mjs' -import { safeWrap } from '../util.mjs' +import { getStatus, safeWrap } from '../util.mjs' -export default function coremonitor(io, config, db, log, core) { +export default function coremonitor(io, ctx) { + + ctx.core.applications.forEach(function(app) { + app.on('updating', safeWrap(ctx.log, `${app.name}.on('updating')`, function() { + io.emit('core.status', getStatus(ctx)) + })) + app.on('running', safeWrap(ctx.log, `${app.name}.on('updating')`, function() { + io.emit('core.status', getStatus(ctx)) + })) + + app.on('updatelog', safeWrap(ctx.log, `${app.name}.on('updatelog')`, function(loglines) { + io.to(`app.${app.name}`).emit('app.updatelog', { + name: app.name, + log: loglines, + }) + })) + + if (app.name !== ctx.app.name) { + app.ctx.log.on('newlog', safeWrap(ctx.log, `${app.name}.log.on('newlog')`, function(data) { + io.to('logger.' + app.name).emit('newlog', formatLog(data)) + })) + } + }) + + ctx.core.log.on('newlog', safeWrap(ctx.log, `core.log.on('newlog')`, function(data) { + io.to('logger.service-core').emit('newlog', formatLog(data)) + })) + + ctx.log.on('newlog', safeWrap(ctx.log, 'coremonitor.on.newlog', function(data) { + // Stop infinite regression + if (data?.err?.signal === 'newlogerror') return + try { + io.to('logger.' + ctx.app.name).emit('newlog', formatLog(data)) + } catch (err) { + err.signal = 'newlogerror' + throw err + } + })) + + /* log.on('newlog', safeWrap(log, 'coremonitor.on.newlog', function(data) { io.to('logger').emit('newlog', formatLog(data)) })) @@ -22,5 +61,5 @@ export default function coremonitor(io, config, db, log, core) { name: 'manage', logs: manage.logs, }) - })) -} \ No newline at end of file + }))*/ +} diff --git a/api/core/ioroutes.mjs b/api/core/ioroutes.mjs index 69aabb8..0e01ddf 100644 --- a/api/core/ioroutes.mjs +++ b/api/core/ioroutes.mjs @@ -1,23 +1,8 @@ import defaults from '../defaults.mjs' import { formatLog } from './loghelper.mjs' +import { getStatus } from '../util.mjs' -/* - * Event: 'core.config' - * - * Get config - */ -export async function config(ctx, data, cb) { - let merge = { - applications: [] - } - for (let app of ctx.core.applications) { - merge[app.name] = app.config - merge.applications.push(app.name) - } - let out = defaults(ctx.db.config, merge) - console.log(out) - cb(out) -} +const stopSpam = {} /* * Event: 'core.restart' @@ -25,7 +10,12 @@ export async function config(ctx, data, cb) { * Restart server */ export async function restart(ctx, data, cb) { - ctx.core.restart() + if (ctx.db.config.allowRestart) { + ctx.core.restart() + } else { + ctx.log.fatal('Invalid core restart command') + ctx.log.event.error('Invalid core restart command') + } } /* @@ -34,7 +24,17 @@ export async function restart(ctx, data, cb) { * Returns last few log messages from log */ export async function getlastlogs(ctx, data, cb) { - cb(ctx.logroot.ringbuffer.records.map(formatLog)) + if (data.name === 'service-core') { + return cb(ctx.core.log.ringbuffer.records.map(formatLog)) + } + let app = ctx.core.applicationMap.get(data.name) + if (!app) { + ctx.log.warn('Invalid getlastlogs command for app ' + data.name) + ctx.log.event.warn('Invalid getlastlogs command for app ' + data.name) + return + } + + cb(app.ctx.log.ringbuffer.records.map(formatLog)) } /* @@ -42,8 +42,8 @@ export async function getlastlogs(ctx, data, cb) { * * Start listening to new log lines */ -export async function listenlogs(ctx) { - ctx.socket.join('logger') +export async function listenlogs(ctx, data) { + ctx.socket.join('logger.' + (data.name || 'service-core')) } /* @@ -51,8 +51,8 @@ export async function listenlogs(ctx) { * * Stop listening to new log lines */ -export async function unlistenlogs(ctx) { - ctx.socket.leave('logger') +export async function unlistenlogs(ctx, data) { + ctx.socket.leave('logger.' + (data.name || 'service-core')) } /* @@ -61,12 +61,27 @@ export async function unlistenlogs(ctx) { * Update specific software */ export async function update(ctx, data, cb) { - if (data.name !== 'app' && data.name !== 'manage') { + let app = ctx.core.applicationMap.get(data.name) + if (!app) { ctx.log.warn('Invalid update command for app ' + data.name) ctx.log.event.warn('Invalid update command for app ' + data.name) return } - await ctx.core.installLatestVersion(data.name) + + let d = new Date() + if (stopSpam[app.name] && d - stopSpam[app.name] < 1000 * 60 * 5) { + ctx.log.warn('Update called too fast for app ' + data.name) + ctx.log.event.warn('Update called too fast for app ' + data.name) + return + } + stopSpam[app.name] = d + + ctx.log.info('Checking for updates on app ' + data.name) + app.update().then(function(res) { + ctx.log.info(res, 'Update completed on app ' + data.name) + }, function(err) { + ctx.log.err(err, 'Error checking for updates on app ' + data.name) + }) } /* @@ -75,71 +90,52 @@ export async function update(ctx, data, cb) { * Start specific software */ export async function start(ctx, data, cb) { - if (data.name !== 'app' && data.name !== 'manage') { + let app = ctx.core.applicationMap.get(data.name) + + if (!app || (!app.config.scAllowStop && app.running)) { ctx.log.warn('Invalid start command for app ' + data.name) ctx.log.event.warn('Invalid start command for app ' + data.name) return } - await ctx.core.tryStartProgram(data.name) -} -/* - * Event: 'core.updatestart' - * - * Update and start specific software - */ -export async function updatestart(ctx, data, cb) { - if (data.name !== 'app' && data.name !== 'manage') { - ctx.log.warn('Invalid updatestart command for app ' + data.name) - ctx.log.event.warn('Invalid updatestart command for app ' + data.name) + let d = new Date() + if (app.running && stopSpam[app.name] && d - stopSpam[app.name] < 1000 * 60 * 5) { + ctx.log.warn('Update called too fast for app ' + data.name) + ctx.log.event.warn('Update called too fast for app ' + data.name) return } - await ctx.core.start(data.name) -} + stopSpam[app.name] = d -/* - * Event: 'core.listencore' - * - * Start listening to new log lines - */ -export async function listencore(ctx) { - ctx.socket.join('core') - ctx.socket.emit('core.db', ctx.db.data.core) - ctx.socket.emit('core.status', {}) -} - -/* - * Event: 'core.unlistencore' - * - * Stop listening to new log lines - */ -export async function unlistencore(ctx) { - ctx.socket.leave('core') + ctx.log.info('Checking for updates on app ' + data.name) + ctx.core.runApplication(app).then(function(res) { + ctx.log.info('Successfully started ' + data.name + ' running ' + app.running) + }, function(err) { + ctx.log.err(err, 'Error starting app ' + data.name) + }) } /* * Event: 'core.listentoapp' * - * Start listening to changes in core app + * Start listening to changes in core application name */ -export async function listentoapp(ctx) { - ctx.socket.join('core.app') - ctx.socket.emit('core.program.log', { - name: 'app', - logs: ctx.core.getProgramLogs('app') - }) -} +export async function listentoapp(ctx, data) { + if (!data.name) { + ctx.log.warn(`listento called with missing name`) + return + } -/* - * Event: 'core.listentomanage' - * - * Start listening to changes in core manage - */ -export async function listentomanage(ctx) { - ctx.socket.join('core.manage') - ctx.socket.emit('core.program.log', { - name: 'manage', - logs: ctx.core.getProgramLogs('manage') + let app = ctx.core.applicationMap.get(data.name) + + if (!app) { + ctx.log.warn(`listento called on non-existing app ${data.name}`) + return + } + ctx.socket.join('app.' + data.name) + let version = ctx.db.get(ctx.db.data.core[app.name].versions, ctx.db.data.core[app.name].latestInstalled) + ctx.socket.emit('app.updatelog', { + name: data.name, + log: version?.log || ctx.db.data.core[app.name].updater }) } @@ -148,15 +144,18 @@ export async function listentomanage(ctx) { * * Stop listening to new log lines */ -export async function unlistentoapp(ctx) { - ctx.socket.leave('core.app') -} +export async function unlistentoapp(ctx, data) { + if (!data.name) { + ctx.log.warn(`unlistento called with missing name`) + return + } -/* - * Event: 'core.unlistentomanage' - * - * Stop listening to new log lines - */ -export async function unlistentomanage(ctx) { - ctx.socket.leave('core.manage') + let app = ctx.core.applicationMap.get(data.name) + + if (!app) { + ctx.log.warn(`unlistento called on non-existing app ${data.name}`) + return + } + + ctx.socket.leave('app.' + data.name) } diff --git a/api/defaults.mjs b/api/defaults.mjs index a2ef666..893b734 100644 --- a/api/defaults.mjs +++ b/api/defaults.mjs @@ -16,6 +16,8 @@ export default function defaults(options, def) { if (isObject(item)) return defaults(item) return item }) + } else if (out[key] instanceof Date) { + out[key] = new Date(out[key]) } else if (out[key] && typeof out[key] === 'object') { out[key] = defaults(options[key], def && def[key]) } diff --git a/api/routerio.mjs b/api/routerio.mjs index 74ddaf9..ef45fb1 100644 --- a/api/routerio.mjs +++ b/api/routerio.mjs @@ -1,4 +1,5 @@ import * as core from './core/ioroutes.mjs' +import { getConfig, getStatus } from './util.mjs' function register(ctx, name, method) { if (typeof(method) === 'object') { @@ -46,6 +47,9 @@ function onConnection(server, ctx, data) { }) register(ioCtx, 'core', core) + + ioCtx.socket.emit('core.config', getConfig(ioCtx)) + ioCtx.socket.emit('core.status', getStatus(ioCtx)) } export default onConnection diff --git a/api/server.mjs b/api/server.mjs index 8cec5b1..88bb8ec 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -1,6 +1,6 @@ import socket from 'socket.io-serveronly' import nStatic from 'node-static' -// import coremonitor from './core/coremonitor.mjs' +import coremonitor from './core/coremonitor.mjs' import onConnection from './routerio.mjs' @@ -66,7 +66,7 @@ export function run(http, port, ctx) { const io = new socket(server) io.on('connection', onConnection.bind(this, io, ctx)) - // coremonitor(io, config, db, log, core) + coremonitor(io, ctx) return server.listenAsync(port) .then(function() { diff --git a/api/util.mjs b/api/util.mjs index aff5296..72d3bdf 100644 --- a/api/util.mjs +++ b/api/util.mjs @@ -1,3 +1,5 @@ +import defaults from './defaults.mjs' + export function safeWrap(log, name, fn) { return function(data, cb) { try { @@ -13,4 +15,42 @@ export function safeWrap(log, name, fn) { log.event.error('Unknown error occured in ' + name + ': ' + err.message) } } -} \ No newline at end of file +} + +export function getConfig(ctx) { + let merge = { + applications: [] + } + for (let app of ctx.core.applications) { + merge[app.name] = app.config + merge.applications.push(app.name) + } + return defaults(ctx.db.config, merge) +} + +export function getStatus(ctx) { + let status = {} + for (let app of ctx.core.applications) { + if (app.provider.static) { + status[app.name] = { + active: 'static', + latestInstalled: 'static', + updated: '', + running: app.running, + updating: false, + } + } else { + let appDb = ctx.db.data.core[app.name] + let active = ctx.db.get(appDb.versions, appDb.active) + let installed = ctx.db.get(appDb.versions, appDb.latestInstalled) + status[app.name] = { + active: active, + latestInstalled: installed, + updated: appDb.updater, + running: app.running, + updating: app.updating, + } + } + } + return status +} diff --git a/client/index.js b/client/index.js index f63e6c5..f98bb02 100644 --- a/client/index.js +++ b/client/index.js @@ -26,6 +26,7 @@ m.mount(document.getElementById('header'), Header) m.route(document.getElementById('content'), '/', { '/': Status, '/log': Log, + '/log/:id': Log, '/updater': Updater, '/updater/:id': Updater, }) diff --git a/client/log/log.js b/client/log/log.js index 2017bd4..c182a9c 100644 --- a/client/log/log.js +++ b/client/log/log.js @@ -3,7 +3,8 @@ const socket = require('../socket') const Module = require('../module') const Log = Module({ - init: function() { + init: function(vnode) { + this.activeAppName = 'service-core' this.connected = socket.connected this.loglines = [] this.logUpdated = false @@ -14,21 +15,39 @@ const Log = Module({ m.redraw() }) - this._socketOn(() => this.loadData()) + this.updateActiveApp(vnode.attrs.id) + this._socketOn(() => this.socketOpen()) + }, + + onupdate: function(vnode) { + this.updateActiveApp(vnode.attrs.id) }, remove: function() { - socket.emit('core.unlistenlogs', {}) + socket.emit('core.unlistenlogs', { name: this.activeAppName }) }, - loadData: function() { + updateActiveApp(name) { + if (!name) { + name = 'service-core' + } + if (this.activeAppName === name) return + if (this.activeAppName !== name) { + socket.emit('core.unlistenlogs', { name: this.activeAppName }) + } + this.activeAppName = name + this.socketOpen() + }, + + socketOpen: function() { this.loglines = [] - socket.emit('core.listenlogs', {}) - socket.emit('core.getlastlogs', {}, (res) => { + socket.emit('core.listenlogs', { name: this.activeAppName }) + socket.emit('core.getlastlogs', { name: this.activeAppName }, (res) => { this.loglines = res.map(this.formatLine) this.logUpdated = true m.redraw() }) + m.redraw() }, formatLine: function(line) { @@ -45,7 +64,18 @@ const Log = Module({ view: function() { return [ - m('h1.header', 'Log'), + m('div.actions', [ + m(m.route.Link, { + class: 'button ' + (this.activeAppName === 'service-core' ? 'active' : 'inactive'), + href: '/log', + }, 'service-core'), + this.core.apps.map((app) => { + return m(m.route.Link, { + class: 'button ' + (this.activeAppName === app.name ? 'active' : 'inactive'), + href: '/log/' + app.name, + }, app.name) + }), + ]), m('div#logger', { onupdate: (vnode) => { if (this.logUpdated) { diff --git a/client/module.js b/client/module.js index 5f4b6f9..cfc22f6 100644 --- a/client/module.js +++ b/client/module.js @@ -1,12 +1,44 @@ +const m = require('mithril') const defaults = require('./defaults') const socket = require('./socket') +const core = { + name: '...loading...', + title: '', + apps: [], + db: {}, + status: {}, +} + +socket.on('core.config', function(res) { + core.apps = [] + let keys = Object.keys(res) + for (let key of keys) { + if (typeof(res[key]) !== 'object') { + core[key] = res[key] + } + } + for (let appName of res.applications) { + core.apps.push({ + name: appName, + config: res[appName], + }) + } + m.redraw() +}) + +socket.on('core.status', function(res) { + core.status = res + m.redraw() +}) + module.exports = function Module(module) { return defaults(module, { init: function() {}, oninit: function(vnode) { this._listeners = [] + this.core = core this.init(vnode) }, @@ -21,6 +53,11 @@ module.exports = function Module(module) { }, on: function(name, cb) { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i][0] === name) { + return + } + } this._listeners.push([name, cb]) socket.on(name, cb) }, @@ -31,7 +68,7 @@ module.exports = function Module(module) { this.remove() if (!this._listeners) return for (let i = 0; i < this._listeners.length; i++) { - socket.removeListener(this._listeners[0], this._listeners[1]) + socket.off(this._listeners[i][0], this._listeners[i][1]) } }, }) diff --git a/client/status/status.js b/client/status/status.js index 5ac8674..9748ae4 100644 --- a/client/status/status.js +++ b/client/status/status.js @@ -1,121 +1,67 @@ const m = require('mithril') const socket = require('../socket') const Module = require('../module') +const util = require('../util') const Status = Module({ init: function() { - this._name = '...loading...' - this._apps = [] - - this._socketOn(() => this.loadData()) - }, - - loadData: function() { - socket.emit('core.config', {}, (res) => { - this._apps = [] - console.log('config', res) - this._name = res.name - if (res.title) { - this._name += ' - ' + res.title - } - for (let appName of res.applications) { - this._apps.push({ - name: appName, - config: res[appName], - }) - } - console.log(this._apps) - m.redraw() - }) - - this.on('core.db', (res) => { - console.log('db', res) - - m.redraw() - }) - - this.on('core.status', (res) => { - console.log(res) - /*this._management.running = res.manage - this._management.updating = res.manageUpdating - this._management.starting = res.manageStarting - this._app.running = res.app - this._app.updating = res.appUpdating - this._app.starting = res.appStarting*/ - - m.redraw() - }) - - socket.emit('core.listencore', {}) }, remove: function() { - socket.emit('core.unlistencore', {}) }, restartClicked: function() { socket.emit('core.restart', {}) }, - start: function(name) { - socket.emit('core.updatestart', { + startSoftware: function(name) { + socket.emit('core.start', { name: name, }) }, - getStatus: function(active) { - if (active.updating) { - return '< Updating >' - } else { - return '< Starting >' - } - }, - view: function() { - let loopOver = [ - ['Management service', '_management'], - ['Application service', '_app'], - ] + let name = this.core.name + if (this.core.title) { + name += ' - ' + this.core.title + } return m('div#status', [ - m('h1.header', this._name), + m('h1.header', name), m('div.split', [ - this._apps.map((app) => { - return m('div.item', [ - m('h4', app.name), - m('p', app.config.port - ? `Port: ${app.config.port}` - : ''), - m('p', app.config.repository - ? `${app.config.repository}` - : '< no repository >'), - m('p', app.config.active - ? `Running version: ${app.config.active}` - : '< no running version >'), - m('p', app.config.latestInstalled - ? `Latest installed: ${app.config.latestInstalled}` - : '< no version installed >'), - m('p', app.config.latestVersion - ? `Latest version: ${app.config.latestVersion}` - : '< no version found >'), - app.config.running !== null && app.config.repository - ? m('p', - { class: app.config.running ? 'running' : 'notrunning' }, - app.config.running ? 'Running' : 'Not Running' - ) - : null, - !app.config.running && (app.config.updating || app.config.starting) - ? m('div.status', this.getStatus(app.config)) - : null, - m('button', { - hidden: app.config.running || app.config.updating || app.config.starting || !app.config.repository, - onclick: () => this.start(app.config.name), - }, 'Update/Start') - ]) + this.core.apps.map((app) => { + let box = [] + let appStatus = this.core.status[app.name] || {} + + box.push(m('h4', [ + app.name + ' (', + m('span', { class: appStatus.running ? 'running' : 'notrunning' }, appStatus.running ? 'Running ' + appStatus.running : 'Not running'), + ')' + ])) + box.push(m('p', app.config.port + ? `Port: ${app.config.port}` + : '')) + box.push(m('p', util.getRepoMessage(app))) + + box.push(m('p', 'Running version: ' + util.getVersionSummary(appStatus.active, appStatus))) + box.push(m('p', 'Latest installed: ' + util.getVersionSummary(appStatus.latestInstalled))) + + if (!this.core.status[app.name].running) { + box.push(m('button', { + onclick: () => this.startSoftware(app.name), + }, 'Start')) + } else if (app.config.scAllowStop) { + box.push(m('button.fatal', { + onclick: () => this.startSoftware(app.name), + }, 'Restart')) + } + + return m('div.item', box) }), ]), - m('button', { + m('button.fatal', { + hidden: !this.core.allowRestart, onclick: () => this.restartClicked(), - }, 'Restart service') + }, 'Restart ' + this.core.name) ]) } }) diff --git a/client/updater/updater.js b/client/updater/updater.js index 4bffd54..1e75cc5 100644 --- a/client/updater/updater.js +++ b/client/updater/updater.js @@ -1,171 +1,146 @@ const m = require('mithril') const socket = require('../socket') const Module = require('../module') +const util = require('../util') const Updater = Module({ init: function(vnode) { - this.activeApp = vnode.attrs.id || null - this.appRepository = null - this.manageRepository = null - this.db = null - this.app = {} - this.status = {} + this.activeApp = null + this.activeAppName = null this.logUpdated = false this._socketOn(() => this.socketOpen()) - this._active = null - if (this.activeApp && this.activeApp !== 'app'&& this.activeApp !== 'manage') { - return m.route('/updater') - } - }, - - onupdate: function(vnode) { - if (this.activeApp === vnode.attrs.id) return - - this.activeApp = vnode.attrs.id || null - if (this.activeApp && this.activeApp !== 'app'&& this.activeApp !== 'manage') { - return m.route.set('/updater') - } - if (this.activeApp && (this.appRepository || this.manageRepository)) { - this.loadAppData() - } - m.redraw() - }, - - socketOpen: function() { - socket.emit('core.config', {}, (res) => { - this.appRepository = res.appRepository - this.manageRepository = res.manageRepository - if (this.activeApp) { - this.loadAppData() - } - m.redraw() - }) - - socket.on('core.status', (res) => { - this.status = res - m.redraw() - }) - - this.on('core.db', (res) => { - this.db = res - this.updateActiveDb() - m.redraw() - }) - - this.on('core.program.log', (res) => { - this.app.logs = res.logs + this.on('app.updatelog', (res) => { + if (!this.activeApp || this.activeApp.name !== res.name) return + this.activeApp.log = res.log.replace(/\. /g, '.\n') this.logUpdated = true m.redraw() }) - - socket.emit('core.listencore', {}) - }, - - updateActiveDb() { - if (this.db && this.activeApp) { - this.app = { - repository: this[this.activeApp + 'Repository'], - active: this.db[this.activeApp + 'Active'], - latestInstalled: this.db[this.activeApp + 'LatestInstalled'], - latestVersion: this.db[this.activeApp + 'LatestVersion'], - logs: '', + if (this.core.apps.length && vnode.attrs.id) { + this.updateActiveApp(vnode.attrs.id) + } else if (this.core.apps.length) { + let apps = this.core.apps.filter(function(app) { + return Boolean(app.config.url) + }) + if (apps.length === 1) { + return m.route.set('/updater/' + apps[0].name) } - } else { - this.app = {} } }, - loadAppData() { - this.updateActiveDb() - if (this.activeApp === 'app') { - socket.emit('core.unlistentomanage', {}) - socket.emit('core.listentoapp', {}) - } else { - socket.emit('core.unlistentoapp', {}) - socket.emit('core.listentomanage', {}) + onupdate: function(vnode) { + if (this.core.apps.length) { + if (!vnode.attrs.id) { + let apps = this.core.apps.filter(function(app) { + return Boolean(app.config.url) + }) + if (apps.length === 1) { + return m.route.set('/updater/' + apps[0].name) + } + } + this.updateActiveApp(vnode.attrs.id) } - /* request to listen to app updates */ + }, + + socketOpen: function() { + if (this.activeApp) { + socket.emit('core.listentoapp', { name: this.activeApp.name }) + } + }, + + updateActiveApp(orgName) { + let name = orgName || null + if (this.activeAppName === name && (!name || this.activeApp)) return + if (this.activeAppName !== name && this.activeAppName) { + socket.emit('core.unlistentoapp', { name: this.activeAppName }) + } + + this.activeAppName = name + this.activeApp = null + + if (!name) { + return + } + + for (let app of this.core.apps) { + if (app.name === this.activeAppName) { + this.activeApp = app + break + } + } + if (!this.activeApp) { + return + } + socket.emit('core.listentoapp', { name: this.activeApp.name }) }, remove: function() { - socket.emit('core.unlistencore', {}) - socket.emit('core.unlistentoapp', {}) - socket.emit('core.unlistentomanage', {}) + if (this.activeApp) { + socket.emit('core.unlistentoapp', { name: this.activeApp.name }) + } }, startUpdate: function() { socket.emit('core.update', { - name: this.activeApp, + name: this.activeApp.name, }) }, startSoftware: function() { socket.emit('core.start', { - name: this.activeApp, + name: this.activeApp.name, }) }, view: function() { + let apps = this.core.apps.filter(function(app) { + return Boolean(app.config.url) + }) return m('div#update', [ m('div.actions', [ - m('h1.header', 'Updater'), - m('div.filler'), - m(m.route.Link, { - hidden: !this.appRepository, - class: 'button ' + (this.activeApp === 'app' ? 'active' : 'inactive'), - href: '/updater/app', - }, 'Update App'), - m(m.route.Link, { - hidden: !this.manageRepository, - class: 'button ' + (this.activeApp === 'manage' ? 'active' : 'inactive'), - href: '/updater/manage', - }, 'Update Manager'), + apps.map((app) => { + return m(m.route.Link, { + class: 'button ' + (this.activeAppName === app.name ? 'active' : 'inactive'), + href: '/updater/' + app.name, + }, app.name) + }), ]), - this.activeApp && this.app ? [ - m('div.info', [ - m('p', this.app.repository - ? `Repository: ${this.app.repository}` - : '< no repository >'), - m('p', this.app.latestInstalled - ? `Latest installed: ${this.app.latestInstalled}` - : '< no version installed >'), - m('p', this.app.active - ? `Running version: ${this.app.active}` - : '< no running version >'), - m('p', this.app.latestVersion - ? `Latest version: ${this.app.latestVersion}` - : '< no version found >'), - ]), - m('div.console', { - onupdate: (vnode) => { - if (this.logUpdated) { - vnode.dom.scrollTop = vnode.dom.scrollHeight - this.logUpdated = false + this.activeApp ? (() => { + let appStatus = this.core.status[this.activeApp.name] || {} + console.log(appStatus) + + return [ + m('div.info', [ + m('p', util.getRepoMessage(this.activeApp)), + m('p', 'Active version: ' + util.getVersionSummary(appStatus.active, appStatus)), + m('p', appStatus.updating + ? 'Updating...' + : appStatus.running ? 'Running ' + appStatus.running : 'Not running'), + m('p', 'Latest installed: ' + util.getVersionSummary(appStatus.latestInstalled, appStatus)), + ]), + m('div.console', { + onupdate: (vnode) => { + if (this.logUpdated) { + vnode.dom.scrollTop = vnode.dom.scrollHeight + this.logUpdated = false + } } - } - }, - m('pre', this.app.logs && this.app.logs || '') - ), - this.db - ? m('div.actions', { - hidden: this.status[this.activeApp + 'Updating'], - }, [ - m('button', { - onclick: () => this.startUpdate(), - }, 'Update & Install'), - m('button', { - hidden: this.status[this.activeApp] || !(this.db[this.activeApp + 'LastActive'] || this.db[this.activeApp + 'LatestInstalled']), - onclick: () => this.startSoftware(), - }, 'Start'), - m('button', { - hidden: !this.db[this.activeApp + 'LastActive'] - || this.db[this.activeApp + 'LastActive'] === this.db[this.activeApp + 'Active'] - }, 'Use Last Version'), - ]) - : null, - ] : null + }, + m('pre', this.activeApp.log || '') + ), + m('div.actions', { + hidden: appStatus.updating, + }, [ + m('button', { + onclick: () => this.startSoftware(), + }, appStatus.running ? 'Restart' : 'Start'), + m('button', { + onclick: () => this.startUpdate(), + }, 'Check for updates'), + ]), + ] + })() : null ]) } }) diff --git a/client/util.js b/client/util.js new file mode 100644 index 0000000..dda65fe --- /dev/null +++ b/client/util.js @@ -0,0 +1,42 @@ +const m = require('mithril') + +const regexRepoName = /([^\/]+\/[^\/]+)\/releases/ + +module.exports.getRepoMessage = function(app) { + let prefix = app.config.provider[0].toUpperCase() + app.config.provider.slice(1) + if (!app.config.url) { + return [prefix, null] + } + let url = app.config.url.replace('/api/v1/repos', '').replace('/releases', '') + let name = app.config.url + let temp = regexRepoName.exec(app.config.url) + if (temp) { + name = temp[1] + } + + return [ + prefix + ': ', + m('a', { + target: '_blank', + href: url, + }, name), + ] +} + +module.exports.getVersionSummary = function(version, status) { + if (!version) { + if (status && status.running) { + return '< unknown version >' + } + return '< no version >' + } + if (typeof(version) === 'string') { + return version + } + return `${version.version} (${ + version.stable > 0 + ? 'Stable' + : version.stable === 0 + ? 'Not tested' + : 'Unstable'}) ${version.installed ? '' : '(Not installed)'}` +} diff --git a/dev.mjs b/dev.mjs index c283432..c60d497 100644 --- a/dev.mjs +++ b/dev.mjs @@ -7,15 +7,25 @@ const helloWorldConfig = { "provider": "git", "url": "https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases", "port": 8888, + "scAllowStop": true, } var core = new ServiceCore('sc-manager', import.meta.url) +core.log = getLog('sc-manager', [{ + "stream": process.stdout, + "level": "info" +}]) core.config['hello-world'] = helloWorldConfig +core.config.allowRestart = true core.setConfig({ port: 4271, }) -core.init(index).then(function() { +core.init(index).then(function() { + let coreLogger = getLog('service-core') + core.core.log = coreLogger + core.core.db.log = coreLogger + let provConstructor = Core.providers.get(core.db.config['hello-world'].provider) let provider = new provConstructor(core.db.config['hello-world']) @@ -33,9 +43,9 @@ core.init(index).then(function() { return core.run() }).then(function() { - return core.core.applicationMap.get('hello-world').update() + // return core.core.applicationMap.get('hello-world').update() }).then(function() { - return core.core.runApplication(core.core.applicationMap.get('hello-world')) + // return core.core.runApplication(core.core.applicationMap.get('hello-world')) }).then(function() { let helloApp = core.core.applicationMap.get('hello-world') helloApp.on('updated', core.core.runApplication.bind(core.core, helloApp)) diff --git a/package.json b/package.json index ff11a1a..3a6dc17 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "app", + "name": "sc-manager", "version": "1.0.0", "description": "", "main": "index.js", @@ -40,6 +40,6 @@ "devDependencies": { "asbundle": "^2.6.1", "mithril": "^2.0.4", - "service-core": "^3.0.0-beta.11" + "service-core": "^3.0.0-beta.13" } } diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..d780687 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..a03bdd3 Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..ead2eae Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index e48aefa..8384394 100644 --- a/public/index.html +++ b/public/index.html @@ -1,6 +1,8 @@ + + Service Core diff --git a/public/main.css b/public/main.css index 20355d4..46fd88b 100644 --- a/public/main.css +++ b/public/main.css @@ -87,11 +87,33 @@ a.button { text-align: center; } +button.fatal, +a.button.fatal { + background: hsl(0, 100%, 36%); + color: white; + border-color: hsl(0, 100%, 5%); +} + pre { margin: 0; padding: 0; } + +.actions { + margin: 1rem 0; + flex-wrap: wrap; + display: flex; + justify-content: center; +} + +.actions .button.inactive { + background: transparent; + color: #ffb843; +} +.actions .button.active { +} + /***************** Header ********************/ #header { display: flex; @@ -165,44 +187,38 @@ pre { align-self: center; width: calc(100% - 4rem); display: flex; + flex-direction: column; max-width: 700px; border: 1px solid #999; - border-right: none; } #status .item { flex: 2 1 50%; display: flex; flex-direction: column; - border-right: 1px solid #999; padding-bottom: 0.5rem; } #status .item h4 { + margin-top: -1px; font-size: 1.2rem; text-align: center; padding: 1rem; border-bottom: 1px solid #999; + border-top: 1px solid #999; margin-bottom: 0.5rem; } #status .item p { padding: 0.25rem 1rem; } -#status .item p.running { +#status .item .running { color: hsl(118, 84%, 46.3%); - text-align: center; } -#status .item p.notrunning { +#status .item .notrunning { color: hsl(354.7, 85.8%, 67.6%); - text-align: center; } + #status button { margin-top: 1rem; } -#status .status { - margin-top: 1rem; - align-self: center; - padding: 0.5rem; - color: #ccc; -} /***************** Updater ********************/ #update { @@ -212,22 +228,6 @@ pre { align-self: center; max-width: 700px; } -#update .actions { - margin: 1rem 0; - flex-wrap: wrap; - display: flex; - justify-content: center; -} -#update .actions .filler { - flex-grow: 2; -} - -#update .actions .button.inactive { - background: transparent; - color: #ffb843; -} -#update .actions .button.active { -} @media only screen and (max-device-width: 590px) { #update .actions .filler { @@ -273,7 +273,7 @@ pre { @media only screen and (min-height: 650px) { #update .console { - height: calc(100vh - 300px); + height: calc(100vh - 280px); } }