add websockets, cassette updates for admin

This commit is contained in:
Josh Harvey 2017-05-15 18:29:40 +03:00
parent a123170622
commit 5ed29ee67d
5 changed files with 631 additions and 12 deletions

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node
const EventEmitter = require('events')
const qs = require('querystring')
const os = require('os')
const fs = require('fs')
const path = require('path')
@ -13,6 +15,9 @@ const argv = require('minimist')(process.argv.slice(2))
const got = require('got')
const morgan = require('morgan')
const helmet = require('helmet')
const WebSocket = require('ws')
const http = require('http')
const SocketIo = require('socket.io')
const machineLoader = require('../lib/machine-loader')
const T = require('../lib/time')
@ -26,6 +31,7 @@ const server = require('../lib/admin/server')
const transactions = require('../lib/admin/transactions')
const NEVER = new Date(Date.now() + 100 * T.years)
const REAUTHENTICATE_INTERVAL = T.minute
const devMode = argv.dev
@ -200,12 +206,58 @@ process.on('unhandledRejection', err => {
process.exit(1)
})
const socketServer = http.createServer()
const io = SocketIo(socketServer)
socketServer.listen(3060)
const socketEmitter = new EventEmitter()
io.on('connection', client => {
client.on('message', msg => socketEmitter.emit('message', msg))
})
const webServer = https.createServer(options, app)
const wss = new WebSocket.Server({server: webServer})
function establishSocket (ws, token) {
return login.authenticate(token)
.then(success => {
if (!success) return ws.close(1008, 'Authentication error')
const listener = data => {
console.log('DEBUG200: %j', data)
ws.send(JSON.stringify(data))
}
// Reauthenticate every once in a while, in case token expired
setInterval(() => {
return login.authenticate(token)
.then(success => {
if (!success) {
socketEmitter.removeListener('message', listener)
ws.close()
}
})
}, REAUTHENTICATE_INTERVAL)
socketEmitter.on('message', listener)
console.log('DEBUG120: %j', token)
ws.send('Testing123')
})
}
wss.on('connection', ws => {
const token = qs.parse(ws.upgradeReq.headers.cookie).token
return establishSocket(ws, token)
})
if (devMode) {
https.createServer(options, app).listen(8070, () => {
webServer.listen(8070, () => {
console.log('lamassu-admin-server listening on port 8070')
})
} else {
https.createServer(options, app).listen(443, () => {
webServer.listen(443, () => {
console.log('lamassu-admin-server listening on port 443')
})
}

View file

@ -7,6 +7,7 @@ const T = require('./time')
const logger = require('./logger')
const plugins = require('./plugins')
const helper = require('./cash-out-helper')
const socket = require('./socket-client')
module.exports = {
post,
@ -171,7 +172,8 @@ function updateCassettes (tx) {
const sql = `update devices set
cassette1 = cassette1 - $1,
cassette2 = cassette2 - $2
where device_id = $3`
where device_id = $3
returning cassette1, cassette2`
const values = [
tx.bills[0].dispensed + tx.bills[0].rejected,
@ -179,7 +181,8 @@ function updateCassettes (tx) {
tx.deviceId
]
return db.none(sql, values)
return db.one(sql, values)
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
}
function wasJustAuthorized (oldTx, newTx, isZeroConf) {

9
lib/socket-client.js Normal file
View file

@ -0,0 +1,9 @@
const io = require('socket.io-client')
const socket = io('http://localhost:3060')
module.exports = {emit}
function emit (msg) {
socket.emit('message', msg)
}

View file

@ -20,6 +20,7 @@
"express": "^4.13.4",
"express-limiter": "^1.6.0",
"express-rate-limit": "^2.6.0",
"express-ws": "^3.0.0",
"got": "^6.6.3",
"helmet": "^3.1.0",
"lodash": "^4.17.2",
@ -39,12 +40,13 @@
"promise-sequential": "^1.1.1",
"ramda": "^0.22.1",
"serve-static": "^1.11.1",
"socket.io": "^1.7.1",
"socket.io": "^1.7.4",
"socket.io-client": "^1.7.1",
"twilio": "^2.11.1",
"uuid": "^3.0.0",
"web3": "^0.18.4",
"winston": "^2.3.0"
"winston": "^2.3.0",
"ws": "^2.3.1"
},
"repository": {
"type": "git",

View file

@ -10683,6 +10683,541 @@ var _elm_lang$svg$Svg_Attributes$accumulate = _elm_lang$virtual_dom$VirtualDom$a
var _elm_lang$svg$Svg_Attributes$accelerate = _elm_lang$virtual_dom$VirtualDom$attribute('accelerate');
var _elm_lang$svg$Svg_Attributes$accentHeight = _elm_lang$virtual_dom$VirtualDom$attribute('accent-height');
var _elm_lang$websocket$Native_WebSocket = function() {
function open(url, settings)
{
return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback)
{
try
{
var socket = new WebSocket(url);
socket.elm_web_socket = true;
}
catch(err)
{
return callback(_elm_lang$core$Native_Scheduler.fail({
ctor: err.name === 'SecurityError' ? 'BadSecurity' : 'BadArgs',
_0: err.message
}));
}
socket.addEventListener("open", function(event) {
callback(_elm_lang$core$Native_Scheduler.succeed(socket));
});
socket.addEventListener("message", function(event) {
_elm_lang$core$Native_Scheduler.rawSpawn(A2(settings.onMessage, socket, event.data));
});
socket.addEventListener("close", function(event) {
_elm_lang$core$Native_Scheduler.rawSpawn(settings.onClose({
code: event.code,
reason: event.reason,
wasClean: event.wasClean
}));
});
return function()
{
if (socket && socket.close)
{
socket.close();
}
};
});
}
function send(socket, string)
{
return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback)
{
var result =
socket.readyState === WebSocket.OPEN
? _elm_lang$core$Maybe$Nothing
: _elm_lang$core$Maybe$Just({ ctor: 'NotOpen' });
try
{
socket.send(string);
}
catch(err)
{
result = _elm_lang$core$Maybe$Just({ ctor: 'BadString' });
}
callback(_elm_lang$core$Native_Scheduler.succeed(result));
});
}
function close(code, reason, socket)
{
return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
try
{
socket.close(code, reason);
}
catch(err)
{
return callback(_elm_lang$core$Native_Scheduler.fail(_elm_lang$core$Maybe$Just({
ctor: err.name === 'SyntaxError' ? 'BadReason' : 'BadCode'
})));
}
callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Maybe$Nothing));
});
}
function bytesQueued(socket)
{
return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
callback(_elm_lang$core$Native_Scheduler.succeed(socket.bufferedAmount));
});
}
return {
open: F2(open),
send: F2(send),
close: F3(close),
bytesQueued: bytesQueued
};
}();
var _elm_lang$websocket$WebSocket_LowLevel$bytesQueued = _elm_lang$websocket$Native_WebSocket.bytesQueued;
var _elm_lang$websocket$WebSocket_LowLevel$send = _elm_lang$websocket$Native_WebSocket.send;
var _elm_lang$websocket$WebSocket_LowLevel$closeWith = _elm_lang$websocket$Native_WebSocket.close;
var _elm_lang$websocket$WebSocket_LowLevel$close = function (socket) {
return A2(
_elm_lang$core$Task$map,
_elm_lang$core$Basics$always(
{ctor: '_Tuple0'}),
A3(_elm_lang$websocket$WebSocket_LowLevel$closeWith, 1000, '', socket));
};
var _elm_lang$websocket$WebSocket_LowLevel$open = _elm_lang$websocket$Native_WebSocket.open;
var _elm_lang$websocket$WebSocket_LowLevel$Settings = F2(
function (a, b) {
return {onMessage: a, onClose: b};
});
var _elm_lang$websocket$WebSocket_LowLevel$WebSocket = {ctor: 'WebSocket'};
var _elm_lang$websocket$WebSocket_LowLevel$BadArgs = {ctor: 'BadArgs'};
var _elm_lang$websocket$WebSocket_LowLevel$BadSecurity = {ctor: 'BadSecurity'};
var _elm_lang$websocket$WebSocket_LowLevel$BadReason = {ctor: 'BadReason'};
var _elm_lang$websocket$WebSocket_LowLevel$BadCode = {ctor: 'BadCode'};
var _elm_lang$websocket$WebSocket_LowLevel$BadString = {ctor: 'BadString'};
var _elm_lang$websocket$WebSocket_LowLevel$NotOpen = {ctor: 'NotOpen'};
var _elm_lang$websocket$WebSocket$closeConnection = function (connection) {
var _p0 = connection;
if (_p0.ctor === 'Opening') {
return _elm_lang$core$Process$kill(_p0._1);
} else {
return _elm_lang$websocket$WebSocket_LowLevel$close(_p0._0);
}
};
var _elm_lang$websocket$WebSocket$after = function (backoff) {
return (_elm_lang$core$Native_Utils.cmp(backoff, 1) < 0) ? _elm_lang$core$Task$succeed(
{ctor: '_Tuple0'}) : _elm_lang$core$Process$sleep(
_elm_lang$core$Basics$toFloat(
10 * Math.pow(2, backoff)));
};
var _elm_lang$websocket$WebSocket$removeQueue = F2(
function (name, state) {
return _elm_lang$core$Native_Utils.update(
state,
{
queues: A2(_elm_lang$core$Dict$remove, name, state.queues)
});
});
var _elm_lang$websocket$WebSocket$updateSocket = F3(
function (name, connection, state) {
return _elm_lang$core$Native_Utils.update(
state,
{
sockets: A3(_elm_lang$core$Dict$insert, name, connection, state.sockets)
});
});
var _elm_lang$websocket$WebSocket$add = F2(
function (value, maybeList) {
var _p1 = maybeList;
if (_p1.ctor === 'Nothing') {
return _elm_lang$core$Maybe$Just(
{
ctor: '::',
_0: value,
_1: {ctor: '[]'}
});
} else {
return _elm_lang$core$Maybe$Just(
{ctor: '::', _0: value, _1: _p1._0});
}
});
var _elm_lang$websocket$WebSocket$buildSubDict = F2(
function (subs, dict) {
buildSubDict:
while (true) {
var _p2 = subs;
if (_p2.ctor === '[]') {
return dict;
} else {
if (_p2._0.ctor === 'Listen') {
var _v3 = _p2._1,
_v4 = A3(
_elm_lang$core$Dict$update,
_p2._0._0,
_elm_lang$websocket$WebSocket$add(_p2._0._1),
dict);
subs = _v3;
dict = _v4;
continue buildSubDict;
} else {
var _v5 = _p2._1,
_v6 = A3(
_elm_lang$core$Dict$update,
_p2._0._0,
function (_p3) {
return _elm_lang$core$Maybe$Just(
A2(
_elm_lang$core$Maybe$withDefault,
{ctor: '[]'},
_p3));
},
dict);
subs = _v5;
dict = _v6;
continue buildSubDict;
}
}
}
});
var _elm_lang$websocket$WebSocket_ops = _elm_lang$websocket$WebSocket_ops || {};
_elm_lang$websocket$WebSocket_ops['&>'] = F2(
function (t1, t2) {
return A2(
_elm_lang$core$Task$andThen,
function (_p4) {
return t2;
},
t1);
});
var _elm_lang$websocket$WebSocket$sendMessagesHelp = F3(
function (cmds, socketsDict, queuesDict) {
sendMessagesHelp:
while (true) {
var _p5 = cmds;
if (_p5.ctor === '[]') {
return _elm_lang$core$Task$succeed(queuesDict);
} else {
var _p9 = _p5._1;
var _p8 = _p5._0._0;
var _p7 = _p5._0._1;
var _p6 = A2(_elm_lang$core$Dict$get, _p8, socketsDict);
if ((_p6.ctor === 'Just') && (_p6._0.ctor === 'Connected')) {
return A2(
_elm_lang$websocket$WebSocket_ops['&>'],
A2(_elm_lang$websocket$WebSocket_LowLevel$send, _p6._0._0, _p7),
A3(_elm_lang$websocket$WebSocket$sendMessagesHelp, _p9, socketsDict, queuesDict));
} else {
var _v9 = _p9,
_v10 = socketsDict,
_v11 = A3(
_elm_lang$core$Dict$update,
_p8,
_elm_lang$websocket$WebSocket$add(_p7),
queuesDict);
cmds = _v9;
socketsDict = _v10;
queuesDict = _v11;
continue sendMessagesHelp;
}
}
}
});
var _elm_lang$websocket$WebSocket$subscription = _elm_lang$core$Native_Platform.leaf('WebSocket');
var _elm_lang$websocket$WebSocket$command = _elm_lang$core$Native_Platform.leaf('WebSocket');
var _elm_lang$websocket$WebSocket$State = F3(
function (a, b, c) {
return {sockets: a, queues: b, subs: c};
});
var _elm_lang$websocket$WebSocket$init = _elm_lang$core$Task$succeed(
A3(_elm_lang$websocket$WebSocket$State, _elm_lang$core$Dict$empty, _elm_lang$core$Dict$empty, _elm_lang$core$Dict$empty));
var _elm_lang$websocket$WebSocket$Send = F2(
function (a, b) {
return {ctor: 'Send', _0: a, _1: b};
});
var _elm_lang$websocket$WebSocket$send = F2(
function (url, message) {
return _elm_lang$websocket$WebSocket$command(
A2(_elm_lang$websocket$WebSocket$Send, url, message));
});
var _elm_lang$websocket$WebSocket$cmdMap = F2(
function (_p11, _p10) {
var _p12 = _p10;
return A2(_elm_lang$websocket$WebSocket$Send, _p12._0, _p12._1);
});
var _elm_lang$websocket$WebSocket$KeepAlive = function (a) {
return {ctor: 'KeepAlive', _0: a};
};
var _elm_lang$websocket$WebSocket$keepAlive = function (url) {
return _elm_lang$websocket$WebSocket$subscription(
_elm_lang$websocket$WebSocket$KeepAlive(url));
};
var _elm_lang$websocket$WebSocket$Listen = F2(
function (a, b) {
return {ctor: 'Listen', _0: a, _1: b};
});
var _elm_lang$websocket$WebSocket$listen = F2(
function (url, tagger) {
return _elm_lang$websocket$WebSocket$subscription(
A2(_elm_lang$websocket$WebSocket$Listen, url, tagger));
});
var _elm_lang$websocket$WebSocket$subMap = F2(
function (func, sub) {
var _p13 = sub;
if (_p13.ctor === 'Listen') {
return A2(
_elm_lang$websocket$WebSocket$Listen,
_p13._0,
function (_p14) {
return func(
_p13._1(_p14));
});
} else {
return _elm_lang$websocket$WebSocket$KeepAlive(_p13._0);
}
});
var _elm_lang$websocket$WebSocket$Connected = function (a) {
return {ctor: 'Connected', _0: a};
};
var _elm_lang$websocket$WebSocket$Opening = F2(
function (a, b) {
return {ctor: 'Opening', _0: a, _1: b};
});
var _elm_lang$websocket$WebSocket$BadOpen = function (a) {
return {ctor: 'BadOpen', _0: a};
};
var _elm_lang$websocket$WebSocket$GoodOpen = F2(
function (a, b) {
return {ctor: 'GoodOpen', _0: a, _1: b};
});
var _elm_lang$websocket$WebSocket$Die = function (a) {
return {ctor: 'Die', _0: a};
};
var _elm_lang$websocket$WebSocket$Receive = F2(
function (a, b) {
return {ctor: 'Receive', _0: a, _1: b};
});
var _elm_lang$websocket$WebSocket$open = F2(
function (name, router) {
return A2(
_elm_lang$websocket$WebSocket_LowLevel$open,
name,
{
onMessage: F2(
function (_p15, msg) {
return A2(
_elm_lang$core$Platform$sendToSelf,
router,
A2(_elm_lang$websocket$WebSocket$Receive, name, msg));
}),
onClose: function (details) {
return A2(
_elm_lang$core$Platform$sendToSelf,
router,
_elm_lang$websocket$WebSocket$Die(name));
}
});
});
var _elm_lang$websocket$WebSocket$attemptOpen = F3(
function (router, backoff, name) {
var badOpen = function (_p16) {
return A2(
_elm_lang$core$Platform$sendToSelf,
router,
_elm_lang$websocket$WebSocket$BadOpen(name));
};
var goodOpen = function (ws) {
return A2(
_elm_lang$core$Platform$sendToSelf,
router,
A2(_elm_lang$websocket$WebSocket$GoodOpen, name, ws));
};
var actuallyAttemptOpen = A2(
_elm_lang$core$Task$onError,
badOpen,
A2(
_elm_lang$core$Task$andThen,
goodOpen,
A2(_elm_lang$websocket$WebSocket$open, name, router)));
return _elm_lang$core$Process$spawn(
A2(
_elm_lang$websocket$WebSocket_ops['&>'],
_elm_lang$websocket$WebSocket$after(backoff),
actuallyAttemptOpen));
});
var _elm_lang$websocket$WebSocket$onEffects = F4(
function (router, cmds, subs, state) {
var newSubs = A2(_elm_lang$websocket$WebSocket$buildSubDict, subs, _elm_lang$core$Dict$empty);
var cleanup = function (newQueues) {
var rightStep = F3(
function (name, connection, getNewSockets) {
return A2(
_elm_lang$websocket$WebSocket_ops['&>'],
_elm_lang$websocket$WebSocket$closeConnection(connection),
getNewSockets);
});
var bothStep = F4(
function (name, _p17, connection, getNewSockets) {
return A2(
_elm_lang$core$Task$map,
A2(_elm_lang$core$Dict$insert, name, connection),
getNewSockets);
});
var leftStep = F3(
function (name, _p18, getNewSockets) {
return A2(
_elm_lang$core$Task$andThen,
function (newSockets) {
return A2(
_elm_lang$core$Task$andThen,
function (pid) {
return _elm_lang$core$Task$succeed(
A3(
_elm_lang$core$Dict$insert,
name,
A2(_elm_lang$websocket$WebSocket$Opening, 0, pid),
newSockets));
},
A3(_elm_lang$websocket$WebSocket$attemptOpen, router, 0, name));
},
getNewSockets);
});
var newEntries = A2(
_elm_lang$core$Dict$union,
newQueues,
A2(
_elm_lang$core$Dict$map,
F2(
function (k, v) {
return {ctor: '[]'};
}),
newSubs));
var collectNewSockets = A6(
_elm_lang$core$Dict$merge,
leftStep,
bothStep,
rightStep,
newEntries,
state.sockets,
_elm_lang$core$Task$succeed(_elm_lang$core$Dict$empty));
return A2(
_elm_lang$core$Task$andThen,
function (newSockets) {
return _elm_lang$core$Task$succeed(
A3(_elm_lang$websocket$WebSocket$State, newSockets, newQueues, newSubs));
},
collectNewSockets);
};
var sendMessagesGetNewQueues = A3(_elm_lang$websocket$WebSocket$sendMessagesHelp, cmds, state.sockets, state.queues);
return A2(_elm_lang$core$Task$andThen, cleanup, sendMessagesGetNewQueues);
});
var _elm_lang$websocket$WebSocket$onSelfMsg = F3(
function (router, selfMsg, state) {
var _p19 = selfMsg;
switch (_p19.ctor) {
case 'Receive':
var sends = A2(
_elm_lang$core$List$map,
function (tagger) {
return A2(
_elm_lang$core$Platform$sendToApp,
router,
tagger(_p19._1));
},
A2(
_elm_lang$core$Maybe$withDefault,
{ctor: '[]'},
A2(_elm_lang$core$Dict$get, _p19._0, state.subs)));
return A2(
_elm_lang$websocket$WebSocket_ops['&>'],
_elm_lang$core$Task$sequence(sends),
_elm_lang$core$Task$succeed(state));
case 'Die':
var _p21 = _p19._0;
var _p20 = A2(_elm_lang$core$Dict$get, _p21, state.sockets);
if (_p20.ctor === 'Nothing') {
return _elm_lang$core$Task$succeed(state);
} else {
return A2(
_elm_lang$core$Task$andThen,
function (pid) {
return _elm_lang$core$Task$succeed(
A3(
_elm_lang$websocket$WebSocket$updateSocket,
_p21,
A2(_elm_lang$websocket$WebSocket$Opening, 0, pid),
state));
},
A3(_elm_lang$websocket$WebSocket$attemptOpen, router, 0, _p21));
}
case 'GoodOpen':
var _p24 = _p19._1;
var _p23 = _p19._0;
var _p22 = A2(_elm_lang$core$Dict$get, _p23, state.queues);
if (_p22.ctor === 'Nothing') {
return _elm_lang$core$Task$succeed(
A3(
_elm_lang$websocket$WebSocket$updateSocket,
_p23,
_elm_lang$websocket$WebSocket$Connected(_p24),
state));
} else {
return A3(
_elm_lang$core$List$foldl,
F2(
function (msg, task) {
return A2(
_elm_lang$websocket$WebSocket_ops['&>'],
A2(_elm_lang$websocket$WebSocket_LowLevel$send, _p24, msg),
task);
}),
_elm_lang$core$Task$succeed(
A2(
_elm_lang$websocket$WebSocket$removeQueue,
_p23,
A3(
_elm_lang$websocket$WebSocket$updateSocket,
_p23,
_elm_lang$websocket$WebSocket$Connected(_p24),
state))),
_p22._0);
}
default:
var _p27 = _p19._0;
var _p25 = A2(_elm_lang$core$Dict$get, _p27, state.sockets);
if (_p25.ctor === 'Nothing') {
return _elm_lang$core$Task$succeed(state);
} else {
if (_p25._0.ctor === 'Opening') {
var _p26 = _p25._0._0;
return A2(
_elm_lang$core$Task$andThen,
function (pid) {
return _elm_lang$core$Task$succeed(
A3(
_elm_lang$websocket$WebSocket$updateSocket,
_p27,
A2(_elm_lang$websocket$WebSocket$Opening, _p26 + 1, pid),
state));
},
A3(_elm_lang$websocket$WebSocket$attemptOpen, router, _p26 + 1, _p27));
} else {
return _elm_lang$core$Task$succeed(state);
}
}
}
});
_elm_lang$core$Native_Platform.effectManagers['WebSocket'] = {pkg: 'elm-lang/websocket', init: _elm_lang$websocket$WebSocket$init, onEffects: _elm_lang$websocket$WebSocket$onEffects, onSelfMsg: _elm_lang$websocket$WebSocket$onSelfMsg, tag: 'fx', cmdMap: _elm_lang$websocket$WebSocket$cmdMap, subMap: _elm_lang$websocket$WebSocket$subMap};
var _evancz$elm_markdown$Native_Markdown = function() {
@ -33008,6 +33543,9 @@ var _user$project$CoreTypes$PairRoute = {ctor: 'PairRoute'};
var _user$project$CoreTypes$AccountRoute = function (a) {
return {ctor: 'AccountRoute', _0: a};
};
var _user$project$CoreTypes$WebSocketMsg = function (a) {
return {ctor: 'WebSocketMsg', _0: a};
};
var _user$project$CoreTypes$Interval = {ctor: 'Interval'};
var _user$project$CoreTypes$UrlChange = function (a) {
return {ctor: 'UrlChange', _0: a};
@ -34570,11 +35108,20 @@ var _user$project$Maintenance_View$view = function (model) {
};
var _user$project$Main$subscriptions = function (model) {
return A2(
return _elm_lang$core$Platform_Sub$batch(
{
ctor: '::',
_0: A2(
_elm_lang$core$Time$every,
10 * _elm_lang$core$Time$second,
5 * _elm_lang$core$Time$second,
function (_p0) {
return _user$project$CoreTypes$Interval;
}),
_1: {
ctor: '::',
_0: A2(_elm_lang$websocket$WebSocket$listen, 'wss://localhost:8070', _user$project$CoreTypes$WebSocketMsg),
_1: {ctor: '[]'}
}
});
};
var _user$project$Main$statusBar = function (maybeStatus) {
@ -35119,7 +35666,7 @@ var _user$project$Main$update = F2(
});
case 'UrlChange':
return A2(_user$project$Main$urlUpdate, _p12._0, model);
default:
case 'Interval':
var route = A2(
_elm_lang$core$Maybe$withDefault,
_user$project$CoreTypes$NotFoundRoute,
@ -35140,6 +35687,12 @@ var _user$project$Main$update = F2(
_1: {ctor: '[]'}
},
extraCmds));
default:
var _p20 = A2(_elm_lang$core$Debug$log, 'DEBUG100', _p12._0);
return A2(
_elm_lang$core$Platform_Cmd_ops['!'],
model,
{ctor: '[]'});
}
});
var _user$project$Main$main = A2(