diff --git a/.gitignore b/.gitignore index 05f3d8a..08ddc1d 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,5 @@ node_modules tmp/ build/ config/config.js +config/stravatoken data/ diff --git a/app/WebServer.js b/app/WebServer.js index 368e0fa..6eb1eec 100644 --- a/app/WebServer.js +++ b/app/WebServer.js @@ -28,14 +28,14 @@ function createWebServer () { const wss = new WebSocketServer({ server }) - wss.on('connection', function connection (ws) { + wss.on('connection', function connection (client) { log.debug('websocket client connected') - emitter.emit('clientConnected', ws) - ws.on('message', function incoming (data) { + emitter.emit('clientConnected', client) + client.on('message', function incoming (data) { try { const message = JSON.parse(data) if (message) { - emitter.emit('messageReceived', message) + emitter.emit('messageReceived', message, client) } else { log.warn(`invalid message received: ${data}`) } @@ -43,13 +43,24 @@ function createWebServer () { log.error(err) } }) - ws.on('close', function () { + client.on('close', function () { log.debug('websocket client disconnected') }) }) - function notifyClients (message) { - const messageString = JSON.stringify(message) + function notifyClient (client, type, data) { + const messageString = JSON.stringify({ type, data }) + if (wss.clients.has(client)) { + if (client.readyState === WebSocket.OPEN) { + client.send(messageString) + } + } else { + log.error('trying to send message to a client that does not exist') + } + } + + function notifyClients (type, data) { + const messageString = JSON.stringify({ type, data }) wss.clients.forEach(function each (client) { if (client.readyState === WebSocket.OPEN) { client.send(messageString) @@ -58,6 +69,7 @@ function createWebServer () { } return Object.assign(emitter, { + notifyClient, notifyClients }) } diff --git a/app/client/components/AppDialog.js b/app/client/components/AppDialog.js new file mode 100644 index 0000000..eab5d2e --- /dev/null +++ b/app/client/components/AppDialog.js @@ -0,0 +1,111 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Component that renders a html dialog +*/ + +import { AppElement, html, css } from './AppElement.js' +import { customElement, property } from 'lit/decorators.js' +import { ref, createRef } from 'lit/directives/ref.js' + +@customElement('app-dialog') +export class AppDialog extends AppElement { + constructor () { + super() + this.dialog = createRef() + } + + static styles = css` + dialog { + border: none; + color: var(--theme-font-color); + background-color: var(--theme-widget-color); + border-radius: var(--theme-border-radius); + box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px; + padding: 1.6rem; + max-width: 80%; + } + dialog::backdrop { + background: none; + backdrop-filter: contrast(15%) blur(2px); + } + + button { + outline:none; + background-color: var(--theme-button-color); + border: 0; + border-radius: var(--theme-border-radius); + color: var(--theme-font-color); + margin: 0.2em 0; + font-size: 60%; + text-decoration: none; + display: inline-flex; + width: 4em; + height: 2.5em; + justify-content: center; + align-items: center; + } + button:hover { + filter: brightness(150%); + } + + fieldset { + border: 0; + margin: unset; + padding: unset; + margin-block-end: 1em; + } + ::slotted(*) { font-size: 80%; } + ::slotted(p) { font-size: 55%; } + + menu { + display: flex; + gap: 0.5em; + justify-content: flex-end; + margin: 0; + padding: 0; + } + ` + + @property({ type: Boolean, reflect: true }) + dialogOpen + + render () { + return html` + +
+
+ +
+ + + + +
+
+ ` + } + + close (event) { + if (event.target.returnValue !== 'confirm') { + this.dispatchEvent(new CustomEvent('close', { detail: 'cancel' })) + } else { + this.dispatchEvent(new CustomEvent('close', { detail: 'confirm' })) + } + } + + firstUpdated () { + this.dialog.value.showModal() + } + + updated (changedProperties) { + if (changedProperties.has('dialogOpen')) { + if (this.dialogOpen) { + this.dialog.value.showModal() + } else { + this.dialog.value.close() + } + } + } +} diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index f92d4cb..8eb5359 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -6,8 +6,9 @@ */ import { AppElement, html, css } from './AppElement.js' -import { customElement } from 'lit/decorators.js' -import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth } from '../lib/icons.js' +import { customElement, state } from 'lit/decorators.js' +import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload } from '../lib/icons.js' +import './AppDialog.js' @customElement('dashboard-actions') export class DashboardActions extends AppElement { @@ -16,14 +17,23 @@ export class DashboardActions extends AppElement { outline:none; background-color: var(--theme-button-color); border: 0; + border-radius: var(--theme-border-radius); color: var(--theme-font-color); - padding: 0.5em 0.9em 0.3em 0.9em; margin: 0.2em 0; font-size: 60%; - text-align: center; text-decoration: none; - display: inline-block; + display: inline-flex; width: 3.5em; + height: 2.5em; + justify-content: center; + align-items: center; + } + button:hover { + filter: brightness(150%); + } + + #fullscreen-icon { + display: inline-flex; } #windowed-icon { @@ -31,7 +41,7 @@ export class DashboardActions extends AppElement { } .icon { - height: 1.8em; + height: 1.7em; } .peripheral-mode { @@ -43,18 +53,22 @@ export class DashboardActions extends AppElement { display: none; } #windowed-icon { - display: inline; + display: inline-flex; } } ` + @state({ type: Object }) + dialog + render () { return html` - - ${this.renderOptionalButtons()} - -
${this.peripheralMode()}
- ` + + ${this.renderOptionalButtons()} + +
${this.peripheralMode()}
+ ${this.dialog ? this.dialog : ''} + ` } renderOptionalButtons () { @@ -62,33 +76,41 @@ export class DashboardActions extends AppElement { // changing to fullscreen mode only makes sence when the app is openend in a regular // webbrowser (kiosk and standalone mode are always in fullscreen view) and if the // browser supports this feature - if (this.appState.appMode === 'BROWSER' && document.documentElement.requestFullscreen) { + if (this.appState?.appMode === 'BROWSER' && document.documentElement.requestFullscreen) { buttons.push(html` - - `) + + `) } - // a shutdown button only makes sence when the app is openend as app on a mobile - // device. at some point we might also think of using this to power down the raspi - // when we are running in kiosk mode - if (this.appState.appMode === 'STANDALONE') { + // add a button to power down the device, if browser is running on the device in kiosk mode + // and the shutdown feature is enabled + // (might also make sence to enable this for all clients but then we would need visual feedback) + if (this.appState?.appMode === 'KIOSK' && this.appState?.config?.shutdownEnabled) { buttons.push(html` - - `) + + `) + } + + if (this.appState?.config?.stravaUploadEnabled) { + buttons.push(html` + + `) } return buttons } peripheralMode () { - const value = this.appState?.peripheralMode + const value = this.appState?.config?.peripheralMode if (value === 'PM5') { return 'C2 PM5' } else if (value === 'FTMSBIKE') { return 'FTMS Bike' - } else { + } else if (value === 'FTMS') { return 'FTMS Rower' + } else { + return '' } } @@ -103,10 +125,6 @@ export class DashboardActions extends AppElement { } } - close () { - window.close() - } - reset () { this.sendEvent('triggerAction', { command: 'reset' }) } @@ -114,4 +132,34 @@ export class DashboardActions extends AppElement { switchPeripheralMode () { this.sendEvent('triggerAction', { command: 'switchPeripheralMode' }) } + + uploadTraining () { + this.dialog = html` + + ${icon_upload}
Upload to Strava?
+

Do you want to finish your workout and upload it to Strava?

+
+ ` + function dialogClosed (event) { + this.dialog = undefined + if (event.detail === 'confirm') { + this.sendEvent('triggerAction', { command: 'uploadTraining' }) + } + } + } + + shutdown () { + this.dialog = html` + + ${icon_poweroff}
Shutdown Open Rowing Monitor?
+

Do you want to shutdown the device?

+
+ ` + function dialogClosed (event) { + this.dialog = undefined + if (event.detail === 'confirm') { + this.sendEvent('triggerAction', { command: 'shutdown' }) + } + } + } } diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index a928b28..0d04013 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -37,6 +37,7 @@ export class PerformanceDashboard extends AppElement { text-align: center; position: relative; padding: 0.5em 0.2em 0 0.2em; + border-radius: var(--theme-border-radius); } dashboard-actions { diff --git a/app/client/index.html b/app/client/index.html index b49ccde..22ef8e8 100644 --- a/app/client/index.html +++ b/app/client/index.html @@ -23,6 +23,7 @@ --theme-font-family: Verdana, "Lucida Sans Unicode", sans-serif; --theme-font-color: #f5f5f5; --theme-warning-color: #ff0000; + --theme-border-radius: 3px; } body { diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 074f8e2..90f5801 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -12,23 +12,40 @@ const rowingMetricsFields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', ' 'heartrateBatteryLevel', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted'] export function createApp (app) { - const mode = window.location.hash - const appMode = mode === '#:standalone:' ? 'STANDALONE' : mode === '#:kiosk:' ? 'KIOSK' : 'BROWSER' + const urlParameters = new URLSearchParams(window.location.search) + const mode = urlParameters.get('mode') + const appMode = mode === 'standalone' ? 'STANDALONE' : mode === 'kiosk' ? 'KIOSK' : 'BROWSER' app.updateState({ ...app.getState(), appMode }) + const stravaAuthorizationCode = urlParameters.get('code') + let socket initWebsocket() resetFields() requestWakeLock() + function websocketOpened () { + if (stravaAuthorizationCode) { + handleStravaAuthorization(stravaAuthorizationCode) + } + } + + function handleStravaAuthorization (stravaAuthorizationCode) { + if (socket)socket.send(JSON.stringify({ command: 'stravaAuthorizationCode', data: stravaAuthorizationCode })) + } + + let initialWebsocketOpenend = true function initWebsocket () { // use the native websocket implementation of browser to communicate with backend - // eslint-disable-next-line no-undef socket = new WebSocket(`ws://${location.host}/websocket`) socket.addEventListener('open', (event) => { console.log('websocket opened') + if (initialWebsocketOpenend) { + websocketOpened() + initialWebsocketOpenend = false + } }) socket.addEventListener('error', (error) => { @@ -46,21 +63,37 @@ export function createApp (app) { // todo: we should use different types of messages to make processing easier socket.addEventListener('message', (event) => { try { - const data = JSON.parse(event.data) - - let activeFields = rowingMetricsFields - // if we are in reset state only update heart rate - if (data.strokesTotal === 0) { - activeFields = ['heartrate', 'heartrateBatteryLevel'] + const message = JSON.parse(event.data) + if (!message.type) { + console.error('message does not contain messageType specifier', message) + return } + const data = message.data + switch (message.type) { + case 'config': { + app.updateState({ ...app.getState(), config: data }) + break + } + case 'metrics': { + let activeFields = rowingMetricsFields + // if we are in reset state only update heart rate + if (data.strokesTotal === 0) { + activeFields = ['heartrate', 'heartrateBatteryLevel'] + } - const filteredData = filterObjectByKeys(data, activeFields) - - let updatedState = { ...app.getState(), metrics: filteredData } - if (data.peripheralMode) { - updatedState = { ...app.getState(), peripheralMode: data.peripheralMode } + const filteredData = filterObjectByKeys(data, activeFields) + app.updateState({ ...app.getState(), metrics: filteredData }) + break + } + case 'authorizeStrava': { + const currentUrl = encodeURIComponent(window.location.href) + window.location.href = `https://www.strava.com/oauth/authorize?client_id=${data.stravaClientId}&response_type=code&redirect_uri=${currentUrl}&approval_prompt=force&scope=activity:write` + break + } + default: { + console.error(`unknown message type: ${message.type}`, message.data) + } } - app.updateState(updatedState) } catch (err) { console.log(err) } @@ -90,13 +123,27 @@ export function createApp (app) { } function handleAction (action) { - if (action.command === 'switchPeripheralMode') { - if (socket)socket.send(JSON.stringify({ command: 'switchPeripheralMode' })) - } else if (action.command === 'reset') { - resetFields() - if (socket)socket.send(JSON.stringify({ command: 'reset' })) - } else { - console.error('no handler defined for action', action) + switch (action.command) { + case 'switchPeripheralMode': { + if (socket)socket.send(JSON.stringify({ command: 'switchPeripheralMode' })) + break + } + case 'reset': { + resetFields() + if (socket)socket.send(JSON.stringify({ command: 'reset' })) + break + } + case 'uploadTraining': { + if (socket)socket.send(JSON.stringify({ command: 'uploadTraining' })) + break + } + case 'shutdown': { + if (socket)socket.send(JSON.stringify({ command: 'shutdown' })) + break + } + default: { + console.error('no handler defined for action', action) + } } } diff --git a/app/client/lib/icons.js b/app/client/lib/icons.js index a5a22c8..23b9a75 100644 --- a/app/client/lib/icons.js +++ b/app/client/lib/icons.js @@ -24,3 +24,4 @@ export const icon_poweroff = svg`` export const icon_compress = svg`` export const icon_bluetooth = svg`` +export const icon_upload = svg`` diff --git a/app/client/manifest.json b/app/client/manifest.json index aad8d39..9c3c018 100644 --- a/app/client/manifest.json +++ b/app/client/manifest.json @@ -9,8 +9,8 @@ "type": "image/png" } ], - "background_color": "#0059B3", + "background_color": "#002b57", "display": "fullscreen", "orientation": "any", - "start_url": "/#:standalone:" + "start_url": "/?mode=standalone" } diff --git a/app/client/store/appState.js b/app/client/store/appState.js index 2a44f9b..3a93f55 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -8,8 +8,14 @@ export const APP_STATE = { // currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default) appMode: '', - // currently can be FTMS, FTMSBIKE or PM5 - peripheralMode: 'FTMS', // contains all the rowing metrics that are delivered from the backend - metrics: {} + metrics: {}, + config: { + // currently can be FTMS, FTMSBIKE or PM5 + peripheralMode: '', + // true if upload to strava is enabled + stravaUploadEnabled: false, + // true if remote device shutdown is enabled + shutdownEnabled: false + } } diff --git a/app/engine/WorkoutRecorder.js b/app/engine/WorkoutRecorder.js index 5496b5e..192b6ef 100644 --- a/app/engine/WorkoutRecorder.js +++ b/app/engine/WorkoutRecorder.js @@ -8,7 +8,7 @@ */ import log from 'loglevel' import zlib from 'zlib' -import { mkdir, writeFile } from 'fs/promises' +import fs from 'fs/promises' import xml2js from 'xml2js' import config from '../tools/ConfigManager.js' import { promisify } from 'util' @@ -34,33 +34,7 @@ function createWorkoutRecorder () { if (startTime === undefined) { startTime = new Date() } - // stroke recordings are currently only used to create tcx files, so we can skip it - // if tcx file creation is disabled - if (config.createTcxFiles) { - strokes.push(stroke) - } - } - - async function createTcxFile () { - const stringifiedStartTime = startTime.toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '') - const directory = `${config.dataDirectory}/recordings/${startTime.getFullYear()}/${(startTime.getMonth() + 1).toString().padStart(2, '0')}` - const filename = `${directory}/${stringifiedStartTime}_rowing.tcx${config.gzipTcxFiles ? '.gz' : ''}` - log.info(`saving session as tcx file ${filename}...`) - - try { - await mkdir(directory, { recursive: true }) - } catch (error) { - if (error.code !== 'EEXIST') { - log.error(`can not create directory ${directory}`, error) - } - } - - await buildAndSaveTcxFile({ - id: startTime.toISOString(), - filename, - startTime, - strokes - }) + strokes.push(stroke) } async function createRawDataFile () { @@ -70,7 +44,7 @@ function createWorkoutRecorder () { log.info(`saving session as raw data file ${filename}...`) try { - await mkdir(directory, { recursive: true }) + await fs.mkdir(directory, { recursive: true }) } catch (error) { if (error.code !== 'EEXIST') { log.error(`can not create directory ${directory}`, error) @@ -79,7 +53,46 @@ function createWorkoutRecorder () { await createFile(rotationImpulses.join('\n'), filename, config.gzipRawDataFiles) } - async function buildAndSaveTcxFile (workout) { + async function createTcxFile () { + const tcxRecord = await activeWorkoutToTcx() + if (tcxRecord === undefined) { + log.error('error creating tcx file') + return + } + const directory = `${config.dataDirectory}/recordings/${startTime.getFullYear()}/${(startTime.getMonth() + 1).toString().padStart(2, '0')}` + const filename = `${directory}/${tcxRecord.filename}${config.gzipTcxFiles ? '.gz' : ''}` + log.info(`saving session as tcx file ${filename}...`) + + try { + await fs.mkdir(directory, { recursive: true }) + } catch (error) { + if (error.code !== 'EEXIST') { + log.error(`can not create directory ${directory}`, error) + } + } + + await createFile(tcxRecord.tcx, `${filename}`, config.gzipTcxFiles) + } + + async function activeWorkoutToTcx () { + // we need at least two strokes to generate a valid tcx file + if (strokes.length < 2) return + const stringifiedStartTime = startTime.toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '') + const filename = `${stringifiedStartTime}_rowing.tcx` + + const tcx = await workoutToTcx({ + id: startTime.toISOString(), + startTime, + strokes + }) + + return { + tcx, + filename + } + } + + async function workoutToTcx (workout) { let versionArray = process.env.npm_package_version.split('.') if (versionArray.length < 3) versionArray = [0, 0, 0] const lastStroke = workout.strokes[strokes.length - 1] @@ -164,7 +177,7 @@ function createWorkoutRecorder () { } const builder = new xml2js.Builder() - await createFile(builder.buildObject(tcxObject), workout.filename, config.gzipTcxFiles) + return builder.buildObject(tcxObject) } async function reset () { @@ -177,9 +190,9 @@ function createWorkoutRecorder () { async function createFile (content, filename, compress = false) { if (compress) { const gzipContent = await gzip(content) - await writeFile(filename, gzipContent, (err) => { if (err) log.error(err) }) + await fs.writeFile(filename, gzipContent, (err) => { if (err) log.error(err) }) } else { - await writeFile(filename, content, (err) => { if (err) log.error(err) }) + await fs.writeFile(filename, content, (err) => { if (err) log.error(err) }) } } @@ -192,11 +205,8 @@ function createWorkoutRecorder () { return } - const minimumRecordingTimeInSeconds = 10 - const rotationImpulseTimeTotal = rotationImpulses.reduce((acc, impulse) => acc + impulse, 0) - const strokeTimeTotal = strokes.reduce((acc, stroke) => acc + stroke.strokeTime, 0) - if (Math.max(rotationImpulseTimeTotal, strokeTimeTotal) < minimumRecordingTimeInSeconds) { - log.debug(`recording time is less than ${minimumRecordingTimeInSeconds}s, skipping creation of recording files...`) + if (!minimumRecordingTimeHasPassed()) { + log.debug('workout is shorter than minimum workout time, skipping automatic creation of recordings...') return } @@ -211,10 +221,18 @@ function createWorkoutRecorder () { await Promise.all(parallelCalls) } + function minimumRecordingTimeHasPassed () { + const minimumRecordingTimeInSeconds = 10 + const rotationImpulseTimeTotal = rotationImpulses.reduce((acc, impulse) => acc + impulse, 0) + const strokeTimeTotal = strokes.reduce((acc, stroke) => acc + stroke.strokeTime, 0) + return (Math.max(rotationImpulseTimeTotal, strokeTimeTotal) > minimumRecordingTimeInSeconds) + } + return { recordStroke, recordRotationImpulse, handlePause, + activeWorkoutToTcx, reset } } diff --git a/app/engine/WorkoutUploader.js b/app/engine/WorkoutUploader.js new file mode 100644 index 0000000..1f4e4af --- /dev/null +++ b/app/engine/WorkoutUploader.js @@ -0,0 +1,57 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Handles uploading workout data to different cloud providers +*/ +import log from 'loglevel' +import EventEmitter from 'events' +import { createStravaAPI } from '../tools/StravaAPI.js' +import config from '../tools/ConfigManager.js' + +function createWorkoutUploader (workoutRecorder) { + const emitter = new EventEmitter() + + let stravaAuthorizationCodeResolver + let requestingClient + + function getStravaAuthorizationCode () { + return new Promise((resolve) => { + emitter.emit('authorizeStrava', { stravaClientId: config.stravaClientId }, requestingClient) + stravaAuthorizationCodeResolver = resolve + }) + } + + const stravaAPI = createStravaAPI(getStravaAuthorizationCode) + + function stravaAuthorizationCode (stravaAuthorizationCode) { + if (stravaAuthorizationCodeResolver) { + stravaAuthorizationCodeResolver(stravaAuthorizationCode) + stravaAuthorizationCodeResolver = undefined + } + } + + async function upload (client) { + log.debug('uploading workout to strava...') + try { + requestingClient = client + // todo: we might signal back to the client whether we had success or not + const tcxActivity = await workoutRecorder.activeWorkoutToTcx() + if (tcxActivity !== undefined) { + await stravaAPI.uploadActivityTcx(tcxActivity) + emitter.emit('resetWorkout') + } else { + log.error('can not upload an empty workout to strava') + } + } catch (error) { + log.error('can not upload workout to strava:', error.message) + } + } + + return Object.assign(emitter, { + upload, + stravaAuthorizationCode + }) +} + +export { createWorkoutUploader } diff --git a/app/server.js b/app/server.js index 179d6d4..32ce762 100644 --- a/app/server.js +++ b/app/server.js @@ -6,7 +6,8 @@ everything together while figuring out the physics and model of the application. todo: refactor this as we progress */ -import { fork } from 'child_process' +import child_process from 'child_process' +import { promisify } from 'util' import log from 'loglevel' import config from './tools/ConfigManager.js' import { createRowingEngine } from './engine/RowingEngine.js' @@ -17,6 +18,8 @@ import { createAntManager } from './ant/AntManager.js' // eslint-disable-next-line no-unused-vars import { replayRowingSession } from './tools/RowingRecorder.js' import { createWorkoutRecorder } from './engine/WorkoutRecorder.js' +import { createWorkoutUploader } from './engine/WorkoutUploader.js' +const exec = promisify(child_process.exec) // set the log levels log.setLevel(config.loglevel.default) @@ -51,7 +54,7 @@ peripheralManager.on('control', (event) => { peripheralManager.notifyStatus({ name: 'startedOrResumedByUser' }) event.res = true } else if (event?.req?.name === 'peripheralMode') { - webServer.notifyClients({ peripheralMode: event.req.peripheralMode }) + webServer.notifyClients('config', getConfig()) event.res = true } else { log.info('unhandled Command', event.req) @@ -65,7 +68,7 @@ function resetWorkout () { peripheralManager.notifyStatus({ name: 'reset' }) } -const gpioTimerService = fork('./app/gpio/GpioTimerService.js') +const gpioTimerService = child_process.fork('./app/gpio/GpioTimerService.js') gpioTimerService.on('message', handleRotationImpulse) function handleRotationImpulse (dataPoint) { @@ -77,9 +80,10 @@ const rowingEngine = createRowingEngine(config.rowerSettings) const rowingStatistics = createRowingStatistics(config) rowingEngine.notify(rowingStatistics) const workoutRecorder = createWorkoutRecorder() +const workoutUploader = createWorkoutUploader(workoutRecorder) rowingStatistics.on('driveFinished', (metrics) => { - webServer.notifyClients(metrics) + webServer.notifyClients('metrics', metrics) peripheralManager.notifyMetrics('strokeStateChanged', metrics) }) @@ -88,7 +92,7 @@ rowingStatistics.on('recoveryFinished', (metrics) => { `, split: ${metrics.splitFormatted}, ratio: ${metrics.powerRatio.toFixed(2)}, dist: ${metrics.distanceTotal.toFixed(1)}m` + `, cal: ${metrics.caloriesTotal.toFixed(1)}kcal, SPM: ${metrics.strokesPerMinute.toFixed(1)}, speed: ${metrics.speed.toFixed(2)}km/h` + `, cal/hour: ${metrics.caloriesPerHour.toFixed(1)}kcal, cal/minute: ${metrics.caloriesPerMinute.toFixed(1)}kcal`) - webServer.notifyClients(metrics) + webServer.notifyClients('metrics', metrics) peripheralManager.notifyMetrics('strokeFinished', metrics) if (metrics.sessionState === 'rowing') { workoutRecorder.recordStroke(metrics) @@ -96,7 +100,7 @@ rowingStatistics.on('recoveryFinished', (metrics) => { }) rowingStatistics.on('webMetricsUpdate', (metrics) => { - webServer.notifyClients(metrics) + webServer.notifyClients('metrics', metrics) }) rowingStatistics.on('peripheralMetricsUpdate', (metrics) => { @@ -108,7 +112,7 @@ rowingStatistics.on('rowingPaused', () => { }) if (config.heartrateMonitorBLE) { - const bleCentralService = fork('./app/ble/CentralService.js') + const bleCentralService = child_process.fork('./app/ble/CentralService.js') bleCentralService.on('message', (heartrateMeasurement) => { rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement) }) @@ -121,21 +125,67 @@ if (config.heartrateMonitorANT) { }) } +workoutUploader.on('authorizeStrava', (data, client) => { + webServer.notifyClient(client, 'authorizeStrava', data) +}) + +workoutUploader.on('resetWorkout', () => { + resetWorkout() +}) + const webServer = createWebServer() -webServer.on('messageReceived', (message) => { - if (message.command === 'reset') { - resetWorkout() - } else if (message.command === 'switchPeripheralMode') { - peripheralManager.switchPeripheralMode() - } else { - log.warn('invalid command received:', message) +webServer.on('messageReceived', async (message, client) => { + switch (message.command) { + case 'switchPeripheralMode': { + peripheralManager.switchPeripheralMode() + break + } + case 'reset': { + resetWorkout() + break + } + case 'uploadTraining': { + workoutUploader.upload(client) + break + } + case 'shutdown': { + if (getConfig().shutdownEnabled) { + console.info('shutting down device...') + try { + const { stdout, stderr } = await exec(config.shutdownCommand) + if (stderr) { + log.error('can not shutdown: ', stderr) + } + log.info(stdout) + } catch (error) { + log.error('can not shutdown: ', error) + } + } + break + } + case 'stravaAuthorizationCode': { + workoutUploader.stravaAuthorizationCode(message.data) + break + } + default: { + log.warn('invalid command received:', message) + } } }) -webServer.on('clientConnected', () => { - webServer.notifyClients({ peripheralMode: peripheralManager.getPeripheralMode() }) +webServer.on('clientConnected', (client) => { + webServer.notifyClient(client, 'config', getConfig()) }) +// todo: extract this into some kind of state manager +function getConfig () { + return { + peripheralMode: peripheralManager.getPeripheralMode(), + stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret, + shutdownEnabled: !!config.shutdownCommand + } +} + /* replayRowingSession(handleRotationImpulse, { filename: 'recordings/WRX700_2magnets.csv', diff --git a/app/tools/AuthorizedStravaConnection.js b/app/tools/AuthorizedStravaConnection.js new file mode 100644 index 0000000..a653dfc --- /dev/null +++ b/app/tools/AuthorizedStravaConnection.js @@ -0,0 +1,130 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Creates an OAuth authorized connection to Strava (https://developers.strava.com/) +*/ +import log from 'loglevel' +import axios from 'axios' +import FormData from 'form-data' +import config from './ConfigManager.js' +import fs from 'fs/promises' + +const clientId = config.stravaClientId +const clientSecret = config.stravaClientSecret +const stravaTokenFile = './config/stravatoken' + +function createAuthorizedConnection (getStravaAuthorizationCode) { + let accessToken + let refreshToken + + const authorizedConnection = axios.create({ + baseURL: 'https://www.strava.com/api/v3' + }) + + authorizedConnection.interceptors.request.use(async config => { + if (!refreshToken) { + try { + refreshToken = await fs.readFile(stravaTokenFile, 'utf-8') + } catch (error) { + log.info('no strava token available yet') + } + } + // if no refresh token is set, then the app has not yet been authorized with Strava + // start oAuth authorization process + if (!refreshToken) { + const authorizationCode = await getStravaAuthorizationCode(); + ({ accessToken, refreshToken } = await authorize(authorizationCode)) + await writeToken('', refreshToken) + // otherwise we just need to get a valid accessToken + } else { + const oldRefreshToken = refreshToken; + ({ accessToken, refreshToken } = await getAccessTokens(refreshToken)) + if (!refreshToken) { + log.error(`strava token is invalid, deleting ${stravaTokenFile}...`) + await fs.unlink(stravaTokenFile) + // if the refreshToken has changed, persist it + } else { + await writeToken(oldRefreshToken, refreshToken) + } + } + + if (!accessToken) { + log.error('strava authorization not successful') + } + + Object.assign(config.headers, { Authorization: `Bearer ${accessToken}` }) + if (config.data instanceof FormData) { + Object.assign(config.headers, config.data.getHeaders()) + } + return config + }) + + authorizedConnection.interceptors.response.use(function (response) { + return response + }, function (error) { + if (error?.response?.status === 401 || error?.message === 'canceled') { + return Promise.reject(new Error('user unauthorized')) + } else { + return Promise.reject(error) + } + }) + + async function oAuthTokenRequest (token, grantType) { + let responsePayload + const payload = { + client_id: clientId, + client_secret: clientSecret, + grant_type: grantType + } + if (grantType === 'authorization_code') { + payload.code = token + } else { + payload.refresh_token = token + } + + try { + const response = await axios.post('https://www.strava.com/oauth/token', payload) + if (response?.status === 200) { + responsePayload = response.data + } else { + log.error(`response error at strava oAuth request for ${grantType}: ${response?.data?.message || response}`) + } + } catch (e) { + log.error(`general error at strava oAuth request for ${grantType}: ${e?.response?.data?.message || e}`) + } + return responsePayload + } + + async function authorize (authorizationCode) { + const response = await oAuthTokenRequest(authorizationCode, 'authorization_code') + return { + refreshToken: response?.refresh_token, + accessToken: response?.access_token + } + } + + async function getAccessTokens (refreshToken) { + const response = await oAuthTokenRequest(refreshToken, 'refresh_token') + return { + refreshToken: response?.refresh_token, + accessToken: response?.access_token + } + } + + async function writeToken (oldToken, newToken) { + if (oldToken !== newToken) { + try { + await fs.writeFile(stravaTokenFile, newToken, 'utf-8') + } catch (error) { + log.info(`can not write strava token to file ${stravaTokenFile}`, error) + } + } + } + + return authorizedConnection +} + +export { + createAuthorizedConnection +} diff --git a/app/tools/StravaAPI.js b/app/tools/StravaAPI.js new file mode 100644 index 0000000..f5dbc65 --- /dev/null +++ b/app/tools/StravaAPI.js @@ -0,0 +1,40 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Implements required parts of the Strava API (https://developers.strava.com/) +*/ +import zlib from 'zlib' +import FormData from 'form-data' +import { promisify } from 'util' +import { createAuthorizedConnection } from './AuthorizedStravaConnection.js' +const gzip = promisify(zlib.gzip) + +function createStravaAPI (getStravaAuthorizationCode) { + const authorizedStravaConnection = createAuthorizedConnection(getStravaAuthorizationCode) + + async function uploadActivityTcx (tcxRecord) { + const form = new FormData() + + form.append('file', await gzip(tcxRecord.tcx), tcxRecord.filename) + form.append('data_type', 'tcx.gz') + form.append('name', 'Indoor Rowing Session') + form.append('description', 'Uploaded from Open Rowing Monitor') + form.append('trainer', 'true') + form.append('activity_type', 'Rowing') + + return await authorizedStravaConnection.post('/uploads', form) + } + + async function getAthlete () { + return (await authorizedStravaConnection.get('/athlete')).data + } + + return { + uploadActivityTcx, + getAthlete + } +} +export { + createStravaAPI +} diff --git a/config/default.config.js b/config/default.config.js index f0f9f32..e493e26 100644 --- a/config/default.config.js +++ b/config/default.config.js @@ -99,5 +99,23 @@ export default { // the device to the profiles. // !! Only change this setting in the config/config.js file, and leave this on DEFAULT as that // is the fallback for the default profile settings - rowerSettings: rowerProfiles.DEFAULT + rowerSettings: rowerProfiles.DEFAULT, + + // command to shutdown the device via the user interface, leave empty to disable this feature + shutdownCommand: 'halt', + + // Configures the connection to Strava (to directly upload workouts to Strava) + // Note that these values are not your Strava credentials + // Instead you have to create a Strava API Application as described here: + // https://developers.strava.com/docs/getting-started/#account and use the corresponding values + // When creating your Strava API application, set the "Authorization Callback Domain" to the IP address + // of your Raspberry Pi + // WARNING: if you enabled the network share via the installer script, then this config file will be + // exposed via network share on your local network. You might consider disabling (or password protect) + // the Configuration share in smb.conf + // The "Client ID" of your Strava API Application + stravaClientId: '', + + // The "Client Secret" of your Strava API Application + stravaClientSecret: '' } diff --git a/docs/README.md b/docs/README.md index 875530c..7bbb630 100644 --- a/docs/README.md +++ b/docs/README.md @@ -55,7 +55,7 @@ Fitness Machine Service (FTMS) is a standardized GATT protocol for different typ Open Rowing Monitor can create Training Center XML files (TCX). You can upload these files to training platforms like [Strava](https://www.strava.com), [Garmin Connect](https://connect.garmin.com) or [Trainingpeaks](https://trainingpeaks.com) to track your training sessions. -Currently this is a manual step. The installer can set up a network share that contains all training data so it is easy to grab the files from there and upload them to the training platform of your choice. +Uploading your sessions to Strava is an integrated feature, for all other platforms this is currently a manual step. The installer can set up a network share that contains all training data so it is easy to grab the files from there and upload them to the training platform of your choice. Open Rowing Monitor can also store the raw measurements of the flywheel into CSV files. These files are great to start your own exploration of your rowing style and also to learn about the specifics of your rowing machine (some Excel files that can help with this are included in the `docs` folder). diff --git a/docs/backlog.md b/docs/backlog.md index 3e197b7..50a35b1 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -12,7 +12,6 @@ If you would like to contribute to this project, please read the [Contributing G ## Later -* automatically upload recorded rowing sessions to training platforms (i.e. Strava) * figure out where to set the Service Advertising Data (FTMS.pdf p 15) * add some attributes to BLE DeviceInformationService * record the workout and show a visual graph of metrics diff --git a/install/webbrowserkiosk.sh b/install/webbrowserkiosk.sh index d9336f6..057c732 100644 --- a/install/webbrowserkiosk.sh +++ b/install/webbrowserkiosk.sh @@ -12,4 +12,4 @@ openbox-session & # Start Chromium in kiosk mode sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' ~/.config/chromium/'Local State' sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]\+"/"exit_type":"Normal"/' ~/.config/chromium/Default/Preferences -chromium-browser --disable-infobars --kiosk --noerrdialogs --disable-session-crashed-bubble --disable-pinch --check-for-update-interval=604800 --app="http://127.0.0.1/#:kiosk:" +chromium-browser --disable-infobars --disable-features=AudioServiceSandbox --kiosk --noerrdialogs --disable-session-crashed-bubble --disable-pinch --check-for-update-interval=604800 --app="http://127.0.0.1/?mode=kiosk" diff --git a/jsconfig.json b/jsconfig.json index 504cd64..d2e2610 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "target": "es2021", + "moduleResolution": "node", + "checkJs": true, + "esModuleInterop": true, "experimentalDecorators": true } } diff --git a/package-lock.json b/package-lock.json index cb803f0..d534d7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@abandonware/noble": "1.9.2-15", "ant-plus": "0.1.24", "finalhandler": "1.1.2", + "form-data": "4.0.0", "lit": "2.1.2", "loglevel": "1.8.0", "nosleep.js": "0.12.0", @@ -30,6 +31,7 @@ "@rollup/plugin-node-resolve": "13.1.3", "@snowpack/plugin-babel": "2.1.7", "@web/rollup-plugin-html": "1.10.1", + "axios": "0.25.0", "eslint": "8.8.0", "eslint-config-standard": "17.0.0-0", "eslint-plugin-import": "2.25.4", @@ -146,6 +148,19 @@ "usb": "^1.7.2" } }, + "node_modules/@ampproject/remapping": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.0.3.tgz", + "integrity": "sha512-DmIAguV77yFP0MGVFWknCMgSLAtsLR3VlRTteR6xgMpIfYtwaZuMvjGv5YlpiqN7S/5q87DHyuIx8oa15kiyag==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.9", + "@jridgewell/trace-mapping": "^0.2.7" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", @@ -159,35 +174,35 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", - "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", + "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.16.12", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", - "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.0.tgz", + "integrity": "sha512-x/5Ea+RO5MvF9ize5DeVICJoVrNv0Mi2RnIABrZEKYvPEpldXwauPkgvYA17cKa6WpU3LoYvYbuEMFtSNFsarA==", "dev": true, "dependencies": { + "@ampproject/remapping": "^2.0.0", "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.8", + "@babel/generator": "^7.17.0", "@babel/helper-compilation-targets": "^7.16.7", "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.16.7", - "@babel/parser": "^7.16.12", + "@babel/helpers": "^7.17.0", + "@babel/parser": "^7.17.0", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.10", - "@babel/types": "^7.16.8", + "@babel/traverse": "^7.17.0", + "@babel/types": "^7.17.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" + "semver": "^6.3.0" }, "engines": { "node": ">=6.9.0" @@ -216,12 +231,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", - "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", + "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", "dev": true, "dependencies": { - "@babel/types": "^7.16.8", + "@babel/types": "^7.17.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" }, @@ -273,9 +288,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.10.tgz", - "integrity": "sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg==", + "version": "7.17.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz", + "integrity": "sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.16.7", @@ -294,13 +309,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz", - "integrity": "sha512-fk5A6ymfp+O5+p2yCkXAu5Kyj6v0xh0RBeNcAkYUMDvvAAoxvSKXn+Jb37t/yWFiQVDFK1ELpUTD8/aLhCPu+g==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", + "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.16.7", - "regexpu-core": "^4.7.1" + "regexpu-core": "^5.0.1" }, "engines": { "node": ">=6.9.0" @@ -554,14 +569,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", - "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.0.tgz", + "integrity": "sha512-Xe/9NFxjPwELUvW2dsukcMZIp6XwPSbI4ojFBJuX5ramHuVE22SVcZIwqzdWo5uCgeTXW8qV97lMvSOjq+1+nQ==", "dev": true, "dependencies": { "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/traverse": "^7.17.0", + "@babel/types": "^7.17.0" }, "engines": { "node": ">=6.9.0" @@ -582,9 +597,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.16.12", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz", - "integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -930,9 +945,9 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.16.7.tgz", - "integrity": "sha512-vQ+PxL+srA7g6Rx6I1e15m55gftknl2X8GCUW1JTlkTaXZLJOS0UcaY0eK9jYT7IYf4awn6qwyghVHLDz1WyMw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz", + "integrity": "sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.16.7" @@ -1693,9 +1708,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz", + "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.4" @@ -1719,19 +1734,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz", - "integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", + "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.8", + "@babel/generator": "^7.17.0", "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-function-name": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.16.10", - "@babel/types": "^7.16.8", + "@babel/parser": "^7.17.0", + "@babel/types": "^7.17.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1740,9 +1755,9 @@ } }, "node_modules/@babel/types": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz", - "integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.16.7", @@ -1773,9 +1788,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1828,6 +1843,31 @@ "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", "dev": true }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz", + "integrity": "sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz", + "integrity": "sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.2.7.tgz", + "integrity": "sha512-ZKfRhw6eK2vvdWqpU7DQq49+BZESqh5rmkYpNhuzkz01tapssl2sNNy6uMUIgrTtUWQDijomWJzJRCoevVrfgw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.9" + } + }, "node_modules/@lit/reactive-element": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.2.1.tgz", @@ -2477,9 +2517,9 @@ } }, "node_modules/@types/node": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.13.tgz", - "integrity": "sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw==", + "version": "17.0.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.15.tgz", + "integrity": "sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA==", "dev": true }, "node_modules/@types/parse-json": { @@ -2829,8 +2869,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/aws-sign2": { "version": "0.7.0", @@ -2847,6 +2886,15 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "node_modules/axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.7" + } + }, "node_modules/babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -2871,13 +2919,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz", - "integrity": "sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", + "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", "dev": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.20.0" + "core-js-compat": "^3.21.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -3317,9 +3365,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001303", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001303.tgz", - "integrity": "sha512-/Mqc1oESndUNszJP0kx0UaQU9kEv9nNtJ7Kn8AdA0mNnH8eR1cj0kG+NbNuC1Wq/b21eA8prhKRA3bbkjONegQ==", + "version": "1.0.30001307", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001307.tgz", + "integrity": "sha512-+MXEMczJ4FuxJAUp0jvAl6Df0NI/OfW1RWEE61eSmzS7hw6lz4IKutbhbXendwq8BljfFuHtu26VWsg4afQ7Ng==", "dev": true, "funding": { "type": "opencollective", @@ -3564,7 +3612,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3644,9 +3691,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz", - "integrity": "sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.0.tgz", + "integrity": "sha512-OSXseNPSK2OPJa6GdtkMz/XxeXx8/CJvfhQWTqd6neuUraujcL4jVsjkLQz1OWnax8xVQJnRPe0V2jqNWORA+A==", "dev": true, "dependencies": { "browserslist": "^4.19.1", @@ -3923,7 +3970,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4148,9 +4194,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "node_modules/electron-to-chromium": { - "version": "1.4.57", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz", - "integrity": "sha512-FNC+P5K1n6pF+M0zIK+gFCoXcJhhzDViL3DRIGy2Fv5PohuSES1JHR7T+GlwxSxlzx4yYbsuzCZvHxcBSRCIOw==", + "version": "1.4.65", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.65.tgz", + "integrity": "sha512-0/d8Skk8sW3FxXP0Dd6MnBlrwx7Qo9cqQec3BlIAlvKnrmS3pHsIbaroEi+nd0kZkGpQ6apMEre7xndzjlEnLw==", "dev": true }, "node_modules/emoji-regex": { @@ -4883,9 +4929,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5236,6 +5282,26 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -5246,17 +5312,16 @@ } }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/fresh": { @@ -6560,9 +6625,9 @@ "dev": true }, "node_modules/keyv": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.5.tgz", - "integrity": "sha512-531pkGLqV3BMg0eDqqJFI0R1mkK1Nm5xIP2mM6keP5P8WfFtCkg2IOwplTUmlGoTgIg9yQYZ/kdihhz89XH3vA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.1.0.tgz", + "integrity": "sha512-YsY3wr6HabE11/sscee+3nZ03XjvkrPWGouAmJFBdZoK92wiOlJCzI5/sDEIKdJhdhHO144ei45U9gXfbu14Uw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -6936,7 +7001,6 @@ "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -6945,7 +7009,6 @@ "version": "2.1.34", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, "dependencies": { "mime-db": "1.51.0" }, @@ -8595,14 +8658,14 @@ } }, "node_modules/postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", + "version": "8.4.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", + "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", "dev": true, "dependencies": { - "nanoid": "^3.1.30", + "nanoid": "^3.2.0", "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" + "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -8998,9 +9061,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", - "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -9037,15 +9100,15 @@ } }, "node_modules/regexpu-core": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", - "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", + "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", "dev": true, "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^9.0.0", - "regjsgen": "^0.5.2", - "regjsparser": "^0.7.0", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.0.0" }, @@ -9078,15 +9141,15 @@ } }, "node_modules/regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", "dev": true }, "node_modules/regjsparser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", - "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", "dev": true, "dependencies": { "jsesc": "~0.5.0" @@ -9145,6 +9208,20 @@ "node": ">= 6" } }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", @@ -9567,9 +9644,9 @@ } }, "node_modules/signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "devOptional": true }, "node_modules/simple-git-hooks": { @@ -11245,6 +11322,16 @@ } } }, + "@ampproject/remapping": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.0.3.tgz", + "integrity": "sha512-DmIAguV77yFP0MGVFWknCMgSLAtsLR3VlRTteR6xgMpIfYtwaZuMvjGv5YlpiqN7S/5q87DHyuIx8oa15kiyag==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.9", + "@jridgewell/trace-mapping": "^0.2.7" + } + }, "@babel/code-frame": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", @@ -11255,32 +11342,32 @@ } }, "@babel/compat-data": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", - "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", + "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", "dev": true }, "@babel/core": { - "version": "7.16.12", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", - "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.0.tgz", + "integrity": "sha512-x/5Ea+RO5MvF9ize5DeVICJoVrNv0Mi2RnIABrZEKYvPEpldXwauPkgvYA17cKa6WpU3LoYvYbuEMFtSNFsarA==", "dev": true, "requires": { + "@ampproject/remapping": "^2.0.0", "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.8", + "@babel/generator": "^7.17.0", "@babel/helper-compilation-targets": "^7.16.7", "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.16.7", - "@babel/parser": "^7.16.12", + "@babel/helpers": "^7.17.0", + "@babel/parser": "^7.17.0", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.10", - "@babel/types": "^7.16.8", + "@babel/traverse": "^7.17.0", + "@babel/types": "^7.17.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" + "semver": "^6.3.0" } }, "@babel/eslint-parser": { @@ -11295,12 +11382,12 @@ } }, "@babel/generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", - "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", + "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", "dev": true, "requires": { - "@babel/types": "^7.16.8", + "@babel/types": "^7.17.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -11337,9 +11424,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.10.tgz", - "integrity": "sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg==", + "version": "7.17.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz", + "integrity": "sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.16.7", @@ -11352,13 +11439,13 @@ } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz", - "integrity": "sha512-fk5A6ymfp+O5+p2yCkXAu5Kyj6v0xh0RBeNcAkYUMDvvAAoxvSKXn+Jb37t/yWFiQVDFK1ELpUTD8/aLhCPu+g==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", + "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.16.7", - "regexpu-core": "^4.7.1" + "regexpu-core": "^5.0.1" } }, "@babel/helper-define-polyfill-provider": { @@ -11549,14 +11636,14 @@ } }, "@babel/helpers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", - "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.0.tgz", + "integrity": "sha512-Xe/9NFxjPwELUvW2dsukcMZIp6XwPSbI4ojFBJuX5ramHuVE22SVcZIwqzdWo5uCgeTXW8qV97lMvSOjq+1+nQ==", "dev": true, "requires": { "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/traverse": "^7.17.0", + "@babel/types": "^7.17.0" } }, "@babel/highlight": { @@ -11571,9 +11658,9 @@ } }, "@babel/parser": { - "version": "7.16.12", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz", - "integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -11793,9 +11880,9 @@ } }, "@babel/plugin-syntax-decorators": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.16.7.tgz", - "integrity": "sha512-vQ+PxL+srA7g6Rx6I1e15m55gftknl2X8GCUW1JTlkTaXZLJOS0UcaY0eK9jYT7IYf4awn6qwyghVHLDz1WyMw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz", + "integrity": "sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.16.7" @@ -12310,9 +12397,9 @@ } }, "@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz", + "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" @@ -12330,27 +12417,27 @@ } }, "@babel/traverse": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz", - "integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", + "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.8", + "@babel/generator": "^7.17.0", "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-function-name": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.16.10", - "@babel/types": "^7.16.8", + "@babel/parser": "^7.17.0", + "@babel/types": "^7.17.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz", - "integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.16.7", @@ -12375,9 +12462,9 @@ }, "dependencies": { "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -12420,6 +12507,28 @@ "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", "dev": true }, + "@jridgewell/resolve-uri": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz", + "integrity": "sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz", + "integrity": "sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.2.7.tgz", + "integrity": "sha512-ZKfRhw6eK2vvdWqpU7DQq49+BZESqh5rmkYpNhuzkz01tapssl2sNNy6uMUIgrTtUWQDijomWJzJRCoevVrfgw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.9" + } + }, "@lit/reactive-element": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.2.1.tgz", @@ -12929,9 +13038,9 @@ } }, "@types/node": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.13.tgz", - "integrity": "sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw==", + "version": "17.0.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.15.tgz", + "integrity": "sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA==", "dev": true }, "@types/parse-json": { @@ -13214,8 +13323,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sign2": { "version": "0.7.0", @@ -13229,6 +13337,15 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.7" + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -13250,13 +13367,13 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz", - "integrity": "sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", + "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", "dev": true, "requires": { "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.20.0" + "core-js-compat": "^3.21.0" } }, "babel-plugin-polyfill-regenerator": { @@ -13589,9 +13706,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001303", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001303.tgz", - "integrity": "sha512-/Mqc1oESndUNszJP0kx0UaQU9kEv9nNtJ7Kn8AdA0mNnH8eR1cj0kG+NbNuC1Wq/b21eA8prhKRA3bbkjONegQ==", + "version": "1.0.30001307", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001307.tgz", + "integrity": "sha512-+MXEMczJ4FuxJAUp0jvAl6Df0NI/OfW1RWEE61eSmzS7hw6lz4IKutbhbXendwq8BljfFuHtu26VWsg4afQ7Ng==", "dev": true }, "caseless": { @@ -13774,7 +13891,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -13842,9 +13958,9 @@ } }, "core-js-compat": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz", - "integrity": "sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.0.tgz", + "integrity": "sha512-OSXseNPSK2OPJa6GdtkMz/XxeXx8/CJvfhQWTqd6neuUraujcL4jVsjkLQz1OWnax8xVQJnRPe0V2jqNWORA+A==", "dev": true, "requires": { "browserslist": "^4.19.1", @@ -14044,8 +14160,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -14220,9 +14335,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.4.57", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz", - "integrity": "sha512-FNC+P5K1n6pF+M0zIK+gFCoXcJhhzDViL3DRIGy2Fv5PohuSES1JHR7T+GlwxSxlzx4yYbsuzCZvHxcBSRCIOw==", + "version": "1.4.65", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.65.tgz", + "integrity": "sha512-0/d8Skk8sW3FxXP0Dd6MnBlrwx7Qo9cqQec3BlIAlvKnrmS3pHsIbaroEi+nd0kZkGpQ6apMEre7xndzjlEnLw==", "dev": true }, "emoji-regex": { @@ -14574,9 +14689,9 @@ "dev": true }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -15052,6 +15167,12 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "follow-redirects": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "dev": true + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -15059,13 +15180,12 @@ "dev": true }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -16045,9 +16165,9 @@ "dev": true }, "keyv": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.5.tgz", - "integrity": "sha512-531pkGLqV3BMg0eDqqJFI0R1mkK1Nm5xIP2mM6keP5P8WfFtCkg2IOwplTUmlGoTgIg9yQYZ/kdihhz89XH3vA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.1.0.tgz", + "integrity": "sha512-YsY3wr6HabE11/sscee+3nZ03XjvkrPWGouAmJFBdZoK92wiOlJCzI5/sDEIKdJhdhHO144ei45U9gXfbu14Uw==", "dev": true, "requires": { "json-buffer": "3.0.1" @@ -16342,14 +16462,12 @@ "mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { "version": "2.1.34", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, "requires": { "mime-db": "1.51.0" } @@ -17613,14 +17731,14 @@ } }, "postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", + "version": "8.4.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", + "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", "dev": true, "requires": { - "nanoid": "^3.1.30", + "nanoid": "^3.2.0", "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" + "source-map-js": "^1.0.2" } }, "postcss-modules": { @@ -17919,9 +18037,9 @@ "dev": true }, "regenerate-unicode-properties": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", - "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", "dev": true, "requires": { "regenerate": "^1.4.2" @@ -17949,15 +18067,15 @@ "dev": true }, "regexpu-core": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", - "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", + "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", "dev": true, "requires": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^9.0.0", - "regjsgen": "^0.5.2", - "regjsparser": "^0.7.0", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.0.0" } @@ -17981,15 +18099,15 @@ } }, "regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", "dev": true }, "regjsparser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", - "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -18035,6 +18153,19 @@ "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } } }, "requireindex": { @@ -18360,9 +18491,9 @@ } }, "signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "devOptional": true }, "simple-git-hooks": { diff --git a/package.json b/package.json index 824b35e..87e8f2a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openrowingmonitor", - "version": "0.8.1", - "description": "A rowing monitor for rowing exercise machines", + "version": "0.8.2", + "description": "A free and open source performance monitor for rowing machines", "main": "app/server.js", "author": "Lars Berning", "license": "GPL-3.0", @@ -21,7 +21,7 @@ "lint": "eslint ./app ./config && markdownlint-cli2 '**/*.md' '#node_modules'", "start": "node app/server.js", "dev": "npm-run-all --parallel dev:backend dev:frontend", - "dev:backend": "nodemon app/server.js", + "dev:backend": "nodemon --ignore 'app/client/**/*' app/server.js", "dev:frontend": "snowpack dev", "build": "rollup -c", "build:watch": "rollup -cw", @@ -35,6 +35,7 @@ "@abandonware/noble": "1.9.2-15", "ant-plus": "0.1.24", "finalhandler": "1.1.2", + "form-data": "4.0.0", "lit": "2.1.2", "loglevel": "1.8.0", "nosleep.js": "0.12.0", @@ -62,6 +63,7 @@ "@rollup/plugin-node-resolve": "13.1.3", "@snowpack/plugin-babel": "2.1.7", "@web/rollup-plugin-html": "1.10.1", + "axios": "0.25.0", "eslint": "8.8.0", "eslint-config-standard": "17.0.0-0", "eslint-plugin-import": "2.25.4", diff --git a/snowpack.config.js b/snowpack.config.js index dbebdce..83b32bb 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -43,7 +43,6 @@ export default { upgrade: (req, socket, head) => { const defaultWSHandler = (err, req, socket, head) => { if (err) { - console.error('proxy error', err) socket.destroy() } }