Implement cutelog for server, and timeline in Panopticon.
This commit is contained in:
parent
aab0df7438
commit
ee49ce2035
11 changed files with 331 additions and 17 deletions
|
@ -21,10 +21,15 @@ import logging
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
from hugvey.voice import VoiceStorage
|
from hugvey.voice import VoiceStorage
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
mainLogger = logging.getLogger("hugvey")
|
mainLogger = logging.getLogger("hugvey")
|
||||||
|
|
||||||
logger = mainLogger.getChild("command")
|
logger = mainLogger.getChild("command")
|
||||||
|
|
||||||
|
eventLogger = logging.getLogger("events")
|
||||||
|
eventLogger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# def exceptionEmitter(a):
|
# def exceptionEmitter(a):
|
||||||
# print(a)
|
# print(a)
|
||||||
# def decorate(func):
|
# def decorate(func):
|
||||||
|
@ -51,6 +56,7 @@ class CentralCommand(object):
|
||||||
self.eventQueue = asyncio.Queue()
|
self.eventQueue = asyncio.Queue()
|
||||||
self.commandQueue = asyncio.Queue()
|
self.commandQueue = asyncio.Queue()
|
||||||
self.isRunning = threading.Event()
|
self.isRunning = threading.Event()
|
||||||
|
self.logQueue = multiprocessing.Queue()
|
||||||
self.hugveys = {}
|
self.hugveys = {}
|
||||||
self.ctx = Context.instance()
|
self.ctx = Context.instance()
|
||||||
self.hugveyLock = asyncio.Lock()
|
self.hugveyLock = asyncio.Lock()
|
||||||
|
@ -58,6 +64,8 @@ class CentralCommand(object):
|
||||||
self.languageFiles = {}
|
self.languageFiles = {}
|
||||||
self.args = args # cli args
|
self.args = args # cli args
|
||||||
|
|
||||||
|
eventLogger.addHandler(logging.handlers.QueueHandler(self.logQueue))
|
||||||
|
|
||||||
def loadConfig(self, filename):
|
def loadConfig(self, filename):
|
||||||
if hasattr(self, 'config'):
|
if hasattr(self, 'config'):
|
||||||
raise Exception("Overriding config not supported yet")
|
raise Exception("Overriding config not supported yet")
|
||||||
|
@ -103,7 +111,8 @@ class CentralCommand(object):
|
||||||
status['language'] = hv.language_code
|
status['language'] = hv.language_code
|
||||||
status['msg'] = hv.story.currentMessage.id if hv.story.currentMessage else None
|
status['msg'] = hv.story.currentMessage.id if hv.story.currentMessage else None
|
||||||
status['finished'] = hv.story.isFinished()
|
status['finished'] = hv.story.isFinished()
|
||||||
status['history'] = hv.story.getLogSummary()
|
status['history'] = {}
|
||||||
|
# status['history'] = hv.story.getLogSummary() # disabled as it is a bit slow. We now have eventLog
|
||||||
status['counts'] = {t: len(a) for t, a in status['history'].items() if t != 'directions' }
|
status['counts'] = {t: len(a) for t, a in status['history'].items() if t != 'directions' }
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
@ -112,6 +121,7 @@ class CentralCommand(object):
|
||||||
status = {
|
status = {
|
||||||
'uptime': time.time() - self.start_time,
|
'uptime': time.time() - self.start_time,
|
||||||
'languages': self.config['languages'],
|
'languages': self.config['languages'],
|
||||||
|
'hugvey_ids': self.hugvey_ids,
|
||||||
'hugveys': [],
|
'hugveys': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,6 +286,7 @@ class HugveyState(object):
|
||||||
self.google = None
|
self.google = None
|
||||||
self.notShuttingDown = True # TODO: allow shutdown of object
|
self.notShuttingDown = True # TODO: allow shutdown of object
|
||||||
self.startMsgId = None
|
self.startMsgId = None
|
||||||
|
self.eventLogger = eventLogger.getChild(f"{self.id}")
|
||||||
|
|
||||||
def getStatus(self):
|
def getStatus(self):
|
||||||
if self.story.isFinished():
|
if self.story.isFinished():
|
||||||
|
@ -324,6 +335,7 @@ class HugveyState(object):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
self.logger.critical(f"Hugvey restart required but not implemented yet")
|
self.logger.critical(f"Hugvey restart required but not implemented yet")
|
||||||
|
self.eventLogger.critical(f"error: {e}")
|
||||||
|
|
||||||
# TODO: restart
|
# TODO: restart
|
||||||
|
|
||||||
|
@ -492,5 +504,6 @@ class HugveyState(object):
|
||||||
self.logger.debug("Start audio stream")
|
self.logger.debug("Start audio stream")
|
||||||
await self.getStreamer().run()
|
await self.getStreamer().run()
|
||||||
self.logger.critical(f"stream has left the building from {self.ip}")
|
self.logger.critical(f"stream has left the building from {self.ip}")
|
||||||
|
self.eventLogger.critical(f"error: stream has left the building from {self.ip}")
|
||||||
# if we end up here, the streamer finished, probably meaning hte hugvey shutdown
|
# if we end up here, the streamer finished, probably meaning hte hugvey shutdown
|
||||||
self.gone()
|
self.gone()
|
||||||
|
|
|
@ -18,12 +18,14 @@ from urllib.parse import urlparse
|
||||||
from hugvey import central_command
|
from hugvey import central_command
|
||||||
from hugvey.voice import VoiceStorage
|
from hugvey.voice import VoiceStorage
|
||||||
|
|
||||||
|
from multiprocessing import Queue
|
||||||
|
import threading
|
||||||
|
|
||||||
mainLogger = logging.getLogger("hugvey")
|
mainLogger = logging.getLogger("hugvey")
|
||||||
logger = mainLogger.getChild("panopticon")
|
logger = mainLogger.getChild("panopticon")
|
||||||
|
|
||||||
web_dir = os.path.join(os.path.split(__file__)[0], '..', 'www')
|
web_dir = os.path.join(os.path.split(__file__)[0], '..', 'www')
|
||||||
|
|
||||||
|
|
||||||
def getWebSocketHandler(central_command):
|
def getWebSocketHandler(central_command):
|
||||||
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
CORS_ORIGINS = ['localhost']
|
CORS_ORIGINS = ['localhost']
|
||||||
|
@ -36,9 +38,10 @@ def getWebSocketHandler(central_command):
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
# the client connected
|
# the client connected
|
||||||
def open(self):
|
def open(self, p = None):
|
||||||
self.connections.add(self)
|
WebSocketHandler.connections.add(self)
|
||||||
logger.info("New client connected")
|
logger.info("New client connected")
|
||||||
|
self.write_message("hello!")
|
||||||
|
|
||||||
# the client sent the message
|
# the client sent the message
|
||||||
def on_message(self, message):
|
def on_message(self, message):
|
||||||
|
@ -68,12 +71,13 @@ def getWebSocketHandler(central_command):
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
def send(self, message):
|
def send(self, message):
|
||||||
|
# Possible useless method: use self.write_message()
|
||||||
j = json.dumps(message)
|
j = json.dumps(message)
|
||||||
[con.write_message(j) for con in self.connections]
|
self.write_message(j)
|
||||||
|
|
||||||
# client disconnected
|
# client disconnected
|
||||||
def on_close(self):
|
def on_close(self):
|
||||||
self.connections.remove(self)
|
WebSocketHandler.connections.remove(self)
|
||||||
logger.info("Client disconnected")
|
logger.info("Client disconnected")
|
||||||
|
|
||||||
def getStatusMsg(self):
|
def getStatusMsg(self):
|
||||||
|
@ -104,6 +108,11 @@ def getWebSocketHandler(central_command):
|
||||||
def msgPlayMsg(self, hv_id, msg_id):
|
def msgPlayMsg(self, hv_id, msg_id):
|
||||||
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'play_msg', 'msg_id': msg_id})
|
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'play_msg', 'msg_id': msg_id})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write_to_clients(wsHandlerClass, msg):
|
||||||
|
for client in wsHandlerClass.connections:
|
||||||
|
client.write_message(msg)
|
||||||
|
|
||||||
return WebSocketHandler
|
return WebSocketHandler
|
||||||
|
|
||||||
class NonCachingStaticFileHandler(tornado.web.StaticFileHandler):
|
class NonCachingStaticFileHandler(tornado.web.StaticFileHandler):
|
||||||
|
@ -196,8 +205,10 @@ class Panopticon(object):
|
||||||
voice_dir = os.path.join(self.config['web']['files_dir'], 'voices')
|
voice_dir = os.path.join(self.config['web']['files_dir'], 'voices')
|
||||||
self.voiceStorage = VoiceStorage(voice_dir, self.config['voice']['token'])
|
self.voiceStorage = VoiceStorage(voice_dir, self.config['voice']['token'])
|
||||||
|
|
||||||
|
self.wsHandler = getWebSocketHandler(self.command)
|
||||||
|
|
||||||
self.application = tornado.web.Application([
|
self.application = tornado.web.Application([
|
||||||
(r"/ws", getWebSocketHandler(self.command)),
|
(r"/ws(.*)", self.wsHandler),
|
||||||
(r"/local/(.*)", NonCachingStaticFileHandler,
|
(r"/local/(.*)", NonCachingStaticFileHandler,
|
||||||
{"path": config['web']['files_dir']}),
|
{"path": config['web']['files_dir']}),
|
||||||
(r"/upload", getUploadHandler(self.command)),
|
(r"/upload", getUploadHandler(self.command)),
|
||||||
|
@ -214,8 +225,31 @@ class Panopticon(object):
|
||||||
asyncio.set_event_loop(evt_loop)
|
asyncio.set_event_loop(evt_loop)
|
||||||
|
|
||||||
self.loop = tornado.ioloop.IOLoop.current()
|
self.loop = tornado.ioloop.IOLoop.current()
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self.broadcastLoggingQueueToWs, kwargs={'wsHandler': self.wsHandler, 'q': self.command.logQueue}, name=f"panopticon/logws")
|
||||||
|
thread.start()
|
||||||
|
|
||||||
logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}")
|
logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}")
|
||||||
self.loop.start()
|
self.loop.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.loop.stop()
|
self.loop.stop()
|
||||||
|
|
||||||
|
def broadcastLoggingQueueToWs(self, wsHandler, q: Queue):
|
||||||
|
while True:
|
||||||
|
record = q.get()
|
||||||
|
# record: logging.LogRecord
|
||||||
|
assert isinstance(record, logging.LogRecord)
|
||||||
|
hugvey_id = record.name.split('.')[1]
|
||||||
|
items = record.msg.split(':', 2)
|
||||||
|
msg = {'action': 'log', 'id':hugvey_id, 'type': items[0]}
|
||||||
|
if len(items) > 1:
|
||||||
|
msg['info'] = items[1]
|
||||||
|
if len(items) > 2:
|
||||||
|
msg['args'] = items[2]
|
||||||
|
|
||||||
|
j = json.dumps(msg)
|
||||||
|
print(j)
|
||||||
|
self.loop.add_callback(wsHandler.write_to_clients, j)
|
||||||
|
|
|
@ -6,6 +6,8 @@ import asyncio
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from .communication import LOG_BS
|
from .communication import LOG_BS
|
||||||
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||||
|
import uuid
|
||||||
|
import shortuuid
|
||||||
|
|
||||||
mainLogger = logging.getLogger("hugvey")
|
mainLogger = logging.getLogger("hugvey")
|
||||||
logger = mainLogger.getChild("narrative")
|
logger = mainLogger.getChild("narrative")
|
||||||
|
@ -42,6 +44,7 @@ class Message(object):
|
||||||
self.params = {}
|
self.params = {}
|
||||||
self.variableValues = {}
|
self.variableValues = {}
|
||||||
self.parseForVariables()
|
self.parseForVariables()
|
||||||
|
self.uuid = None # Have a unique id each time the message is played back.
|
||||||
|
|
||||||
def setStory(self, story):
|
def setStory(self, story):
|
||||||
self.story = story
|
self.story = story
|
||||||
|
@ -80,12 +83,12 @@ class Message(object):
|
||||||
|
|
||||||
self.variableValues[name] = value
|
self.variableValues[name] = value
|
||||||
|
|
||||||
self.story.warn(f"Set variable, now fetch {name}")
|
self.logger.warn(f"Set variable, now fetch {name}")
|
||||||
if not None in self.variableValues.values():
|
if not None in self.variableValues.values():
|
||||||
self.story.warn(f"now fetch indeed {name}")
|
self.logger.warn(f"now fetch indeed {name}")
|
||||||
asyncio.get_event_loop().create_task(self.getAudioFilePath())
|
asyncio.get_event_loop().create_task(self.getAudioFilePath())
|
||||||
# asyncio.get_event_loop().call_soon_threadsafe(self.getAudioFilePath)
|
# asyncio.get_event_loop().call_soon_threadsafe(self.getAudioFilePath)
|
||||||
self.story.warn(f"started {name}")
|
self.logger.warn(f"started {name}")
|
||||||
|
|
||||||
def getText(self):
|
def getText(self):
|
||||||
# sort reverse to avoid replacing the wrong variable
|
# sort reverse to avoid replacing the wrong variable
|
||||||
|
@ -697,6 +700,7 @@ class Story(object):
|
||||||
if e['msgId'] == self.currentMessage.id:
|
if e['msgId'] == self.currentMessage.id:
|
||||||
#TODO: migrate value to Messagage instead of Story
|
#TODO: migrate value to Messagage instead of Story
|
||||||
self.lastMsgFinishTime = self.timer.getElapsed()
|
self.lastMsgFinishTime = self.timer.getElapsed()
|
||||||
|
self.hugvey.eventLogger.info(f"message: {self.currentMessage.id} {self.currentMessage.uuid} done")
|
||||||
|
|
||||||
# 2019-02-22 temporary disable listening while playing audio:
|
# 2019-02-22 temporary disable listening while playing audio:
|
||||||
# if self.hugvey.google is not None:
|
# if self.hugvey.google is not None:
|
||||||
|
@ -735,9 +739,11 @@ class Story(object):
|
||||||
|
|
||||||
utterance = self.currentReply.getActiveUtterance(self.timer.getElapsed())
|
utterance = self.currentReply.getActiveUtterance(self.timer.getElapsed())
|
||||||
utterance.setText(e['transcript'])
|
utterance.setText(e['transcript'])
|
||||||
|
self.hugvey.eventLogger.info("speaking: content {} \"{}\"".format(id(utterance), e['transcript']))
|
||||||
|
|
||||||
if e['is_final']:
|
if e['is_final']:
|
||||||
utterance.setFinished(self.timer.getElapsed())
|
utterance.setFinished(self.timer.getElapsed())
|
||||||
|
self.hugvey.eventLogger.info("speaking: stop {}".format(id(utterance)))
|
||||||
|
|
||||||
|
|
||||||
async def _processDirections(self, directions):
|
async def _processDirections(self, directions):
|
||||||
|
@ -747,6 +753,8 @@ class Story(object):
|
||||||
if condition.isMet(self):
|
if condition.isMet(self):
|
||||||
self.logger.info("Condition is met: {0}, going to {1}".format(
|
self.logger.info("Condition is met: {0}, going to {1}".format(
|
||||||
condition.id, direction.msgTo.id))
|
condition.id, direction.msgTo.id))
|
||||||
|
self.hugvey.eventLogger.info("condition: {0}".format(condition.id))
|
||||||
|
self.hugvey.eventLogger.info("direction: {0}".format(direction.id))
|
||||||
direction.setMetCondition(condition)
|
direction.setMetCondition(condition)
|
||||||
self.addToLog(condition)
|
self.addToLog(condition)
|
||||||
self.addToLog(direction)
|
self.addToLog(direction)
|
||||||
|
@ -809,12 +817,14 @@ class Story(object):
|
||||||
"""
|
"""
|
||||||
if self.currentMessage and not self.lastMsgFinishTime:
|
if self.currentMessage and not self.lastMsgFinishTime:
|
||||||
self.logger.info("Interrupt playback {}".format(self.currentMessage.id))
|
self.logger.info("Interrupt playback {}".format(self.currentMessage.id))
|
||||||
|
self.hugvey.eventLogger.info("interrupt")
|
||||||
# message is playing
|
# message is playing
|
||||||
self.hugvey.sendCommand({
|
self.hugvey.sendCommand({
|
||||||
'action': 'stop',
|
'action': 'stop',
|
||||||
'id': self.currentMessage.id,
|
'id': self.currentMessage.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
message.uuid = shortuuid.uuid()
|
||||||
self.currentMessage = message
|
self.currentMessage = message
|
||||||
self.lastMsgTime = time.time()
|
self.lastMsgTime = time.time()
|
||||||
self.lastMsgFinishTime = None # to be filled in by the event
|
self.lastMsgFinishTime = None # to be filled in by the event
|
||||||
|
@ -830,6 +840,8 @@ class Story(object):
|
||||||
self.logger.info("Current message: ({0}) \"{1}\"".format(
|
self.logger.info("Current message: ({0}) \"{1}\"".format(
|
||||||
message.id, message.text))
|
message.id, message.text))
|
||||||
self.addToLog(message)
|
self.addToLog(message)
|
||||||
|
self.hugvey.eventLogger.info(f"message: {message.id} {message.uuid} start \"{message.text}\"")
|
||||||
|
|
||||||
# TODO: prep events & timer etc.
|
# TODO: prep events & timer etc.
|
||||||
# TODO: preload file paths if no variables are set, or once these are loaded
|
# TODO: preload file paths if no variables are set, or once these are loaded
|
||||||
self.hugvey.sendCommand({
|
self.hugvey.sendCommand({
|
||||||
|
@ -859,6 +871,7 @@ class Story(object):
|
||||||
|
|
||||||
async def run(self, customStartMsgId = None):
|
async def run(self, customStartMsgId = None):
|
||||||
self.logger.info("Starting story")
|
self.logger.info("Starting story")
|
||||||
|
self.hugvey.eventLogger.info("story: start")
|
||||||
self.timer.reset()
|
self.timer.reset()
|
||||||
self.isRunning = True
|
self.isRunning = True
|
||||||
if customStartMsgId is not None:
|
if customStartMsgId is not None:
|
||||||
|
@ -876,6 +889,7 @@ class Story(object):
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
self.logger.info(f"Finished story for {self.hugvey.id}")
|
self.logger.info(f"Finished story for {self.hugvey.id}")
|
||||||
|
self.hugvey.eventLogger.info("story: finished")
|
||||||
self.hugvey.pause()
|
self.hugvey.pause()
|
||||||
self.finish_time = time.time()
|
self.finish_time = time.time()
|
||||||
self.timer.pause()
|
self.timer.pause()
|
||||||
|
|
|
@ -24,7 +24,12 @@ class VoiceStorage(object):
|
||||||
self.token = token
|
self.token = token
|
||||||
|
|
||||||
def getId(self, text):
|
def getId(self, text):
|
||||||
return sha1(text.encode()).hexdigest()
|
"""
|
||||||
|
Get a unique id based on text and the voice token.
|
||||||
|
|
||||||
|
So changing the voice or text triggers a re-download.
|
||||||
|
"""
|
||||||
|
return sha1((self.token + ':' + text).encode()).hexdigest()
|
||||||
|
|
||||||
def getFilename(self, text, isVariable=False):
|
def getFilename(self, text, isVariable=False):
|
||||||
subdir = 'static' if not isVariable else 'variable'
|
subdir = 'static' if not isVariable else 'variable'
|
||||||
|
|
|
@ -7,3 +7,4 @@ requests-threads
|
||||||
fabric
|
fabric
|
||||||
cutelog
|
cutelog
|
||||||
tornado
|
tornado
|
||||||
|
shortuuid
|
|
@ -248,3 +248,27 @@ img.icon {
|
||||||
display: block; }
|
display: block; }
|
||||||
.divToggle + div {
|
.divToggle + div {
|
||||||
display: none; }
|
display: none; }
|
||||||
|
|
||||||
|
body.showTimeline #toggleTimeline {
|
||||||
|
background-color: pink; }
|
||||||
|
|
||||||
|
#timeline {
|
||||||
|
position: absolute;
|
||||||
|
left: 380px;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #eee;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0; }
|
||||||
|
body.showTimeline #timeline {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto; }
|
||||||
|
#timeline .vis-item.message {
|
||||||
|
background: lightgray; }
|
||||||
|
#timeline .vis-item.message.vis-range {
|
||||||
|
background-color: darkgray;
|
||||||
|
border-color: green; }
|
||||||
|
#timeline .vis-item.speech {
|
||||||
|
background-color: greenyellow;
|
||||||
|
border-color: green; }
|
||||||
|
|
1
www/css/vis.min.css
vendored
Normal file
1
www/css/vis.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -7,11 +7,13 @@
|
||||||
<script src="/js/moment.min.js"></script>
|
<script src="/js/moment.min.js"></script>
|
||||||
<script src="/js/d3.v5.min.js"></script>
|
<script src="/js/d3.v5.min.js"></script>
|
||||||
<script src="/js/crel.min.js"></script>
|
<script src="/js/crel.min.js"></script>
|
||||||
|
<script src="/js/vis.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="/css/vis.min.css"></link>
|
||||||
<link rel="stylesheet" href="/css/styles.css"></link>
|
<link rel="stylesheet" href="/css/styles.css"></link>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class='showTimeline'>
|
||||||
<div id="interface" class='showStatus'>
|
<div id="interface" class='showStatus'>
|
||||||
<div id='status'>
|
<div id='status'>
|
||||||
<div id='overview'>
|
<div id='overview'>
|
||||||
|
@ -26,6 +28,8 @@
|
||||||
@click="loadNarrative(lang.code, lang.file)"><span
|
@click="loadNarrative(lang.code, lang.file)"><span
|
||||||
:class="['flag-icon', lang.code]"></span> {{lang.code}}</li>
|
:class="['flag-icon', lang.code]"></span> {{lang.code}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div class='btn' id='toggleTimeline'>Timeline</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='hugvey' v-for="hv in hugveys"
|
<div class='hugvey' v-for="hv in hugveys"
|
||||||
:class="[{'hugvey--on': hv.status != 'off'},'hugvey--' + hv.status]"
|
:class="[{'hugvey--on': hv.status != 'off'},'hugvey--' + hv.status]"
|
||||||
|
@ -83,6 +87,7 @@
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div id='timeline'></div>
|
||||||
</div>
|
</div>
|
||||||
<script type='application/javascript' src="/js/hugvey_console.js"></script>
|
<script type='application/javascript' src="/js/hugvey_console.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -52,11 +52,25 @@ class Panopticon {
|
||||||
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
|
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
|
||||||
this.graph = new Graph();
|
this.graph = new Graph();
|
||||||
|
|
||||||
|
this.eventDataSet = new vis.DataSet([
|
||||||
|
{content: '.', start: new Date(), type: 'point', group: 1}
|
||||||
|
]);
|
||||||
|
this.timeline = false;
|
||||||
|
|
||||||
|
document.getElementById('toggleTimeline').addEventListener('click', function(){
|
||||||
|
document.body.classList.toggle('showTimeline');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
this.socket.addEventListener( 'open', ( e ) => {
|
this.socket.addEventListener( 'open', ( e ) => {
|
||||||
this.send( { action: 'init' } );
|
this.send( { action: 'init' } );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// request close before unloading
|
||||||
|
window.addEventListener('beforeunload', function(){
|
||||||
|
panopticon.socket.close();
|
||||||
|
});
|
||||||
|
|
||||||
this.socket.addEventListener( 'close', function( e ) {
|
this.socket.addEventListener( 'close', function( e ) {
|
||||||
console.log( 'Closed connection' );
|
console.log( 'Closed connection' );
|
||||||
} );
|
} );
|
||||||
|
@ -71,10 +85,11 @@ class Panopticon {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.debug(msg);
|
||||||
|
|
||||||
switch ( msg['action'] ) {
|
switch ( msg['action'] ) {
|
||||||
|
|
||||||
case 'status':
|
case 'status':
|
||||||
console.debug(msg);
|
|
||||||
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
|
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
|
||||||
this.hugveys.languages = msg['languages'];
|
this.hugveys.languages = msg['languages'];
|
||||||
this.languages = msg['languages'];
|
this.languages = msg['languages'];
|
||||||
|
@ -82,7 +97,108 @@ class Panopticon {
|
||||||
if(this.hugveys.selectedId) {
|
if(this.hugveys.selectedId) {
|
||||||
this.updateSelectedHugvey();
|
this.updateSelectedHugvey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!this.timeline) {
|
||||||
|
console.log('init timeline');
|
||||||
|
let groups = [];
|
||||||
|
for(let hid of msg['hugvey_ids']) {
|
||||||
|
groups.push({id: parseInt(hid), content: 'Hugvey #'+hid});
|
||||||
|
this.eventDataSet.add({content: 'initiate', start: new Date(), type: 'point', group: parseInt(hid)})
|
||||||
|
}
|
||||||
|
let dataGroups = new vis.DataSet(groups);
|
||||||
|
let options = {
|
||||||
|
// 'rollingMode': {'follow': true, 'offset': .8 }
|
||||||
|
};
|
||||||
|
console.log('groups', dataGroups, groups, options);
|
||||||
|
|
||||||
|
this.timeline = new vis.Timeline(document.getElementById('timeline'), this.eventDataSet, dataGroups, options);
|
||||||
|
//
|
||||||
|
|
||||||
|
let startDate = new Date();
|
||||||
|
startDate.setMinutes(startDate.getMinutes()-1);
|
||||||
|
let endDate = new Date();
|
||||||
|
endDate.setMinutes(endDate.getMinutes()+20);
|
||||||
|
setTimeout(function(){
|
||||||
|
panopticon.timeline.setWindow(startDate, endDate);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
console.log(startDate, endDate);
|
||||||
|
setInterval(function(){panopticon.timeline.moveTo(new Date());}, 1000);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'log':
|
||||||
|
let hv_id = parseInt(msg['id']);
|
||||||
|
if(this.timeline) {
|
||||||
|
// {'action': 'log', 'id':hugvey_id, 'type': items[0], 'info', 'args'}
|
||||||
|
let d, parts;
|
||||||
|
switch(msg['type']){
|
||||||
|
case 'message':
|
||||||
|
// info: en-njsm8bwbd "Are you up for a conversation?"
|
||||||
|
parts = msg['info'].trim().split(' ');
|
||||||
|
let msgId = parts.shift();
|
||||||
|
let msgUuid = parts.shift();
|
||||||
|
let msgEvent = parts.shift();
|
||||||
|
let msgContent = parts.join(' ');
|
||||||
|
let mId = 'm-'+msgUuid+'-'+hv_id;
|
||||||
|
d = this.eventDataSet.get(mId);
|
||||||
|
console.log(msgId, msgEvent, msgContent);
|
||||||
|
if(d !== null && msgEvent == 'done'){
|
||||||
|
d['end'] = new Date();
|
||||||
|
this.eventDataSet.update(d);
|
||||||
|
console.log('update', d);
|
||||||
|
} else {
|
||||||
|
this.eventDataSet.add({id: mId, content: msgContent, title: `${msgContent} (${msgId})`, start: new Date(), group: hv_id, 'className': 'message'});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'speaking':
|
||||||
|
// start/content/end
|
||||||
|
parts = msg['info'].trim().split(' ');
|
||||||
|
let info = parts.shift();
|
||||||
|
let id = parts.shift();
|
||||||
|
let content = parts.join(' ');
|
||||||
|
let scId = 'sc-'+id+'-'+hv_id;
|
||||||
|
|
||||||
|
if(info.startsWith('start')){
|
||||||
|
this.eventDataSet.add({content: info, start: new Date(), type: 'point', group: hv_id, 'className': 'speech'});
|
||||||
|
}
|
||||||
|
if(info.startsWith('content')){
|
||||||
|
d = this.eventDataSet.get(scId);
|
||||||
|
if(d !== null){
|
||||||
|
console.log('alter');
|
||||||
|
d['content'] = content;
|
||||||
|
d['end']= new Date();
|
||||||
|
d['title'] = content;
|
||||||
|
this.eventDataSet.update(d);
|
||||||
|
} else {
|
||||||
|
console.log('add');
|
||||||
|
this.eventDataSet.add({id: scId, content: content, title: content, start: new Date(), group: hv_id, 'className': 'speech'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(info.startsWith('end')){
|
||||||
|
d = this.eventDataSet.get(scId);
|
||||||
|
if(d !== null){
|
||||||
|
d['end'] = new Date();
|
||||||
|
this.eventDataSet.update(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'story':
|
||||||
|
// 'info': 'start'/'finished'
|
||||||
|
this.eventDataSet.add({content: msg['type'] +': ' + msg['info'] + ': ' + msg['args'], start: new Date(), type: 'point', group: hv_id, 'className': 'story'});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'condition':
|
||||||
|
case 'direction':
|
||||||
|
// don't draw these :-0
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.eventDataSet.add({content: msg['type'] +': ' + msg['info'] + ': ' + msg['args'], start: new Date(), type: 'point', group: hv_id});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
@ -1630,4 +1746,25 @@ class Graph {
|
||||||
return distances;
|
return distances;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
//class Timeline {
|
||||||
|
// constructor(el, hugvey_ids) {
|
||||||
|
// this.el = el;
|
||||||
|
// this.logbook = []
|
||||||
|
// this.hugvey_ids = hugvey_ids;
|
||||||
|
//
|
||||||
|
// this.el.innerHTML = "";
|
||||||
|
// for(id of this.hugvey_ids) {
|
||||||
|
// this.el.appendChild(crel(
|
||||||
|
// 'div', {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// log(msg) {
|
||||||
|
//// {"action": "log", "id": "3", "type": "story", "info": " start"}
|
||||||
|
// console.log('log!', msg);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
47
www/js/vis.min.js
vendored
Normal file
47
www/js/vis.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -423,4 +423,37 @@ img.icon{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#toggleTimeline {
|
||||||
|
body.showTimeline & {
|
||||||
|
background-color:pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#timeline{
|
||||||
|
position: absolute;
|
||||||
|
left: 380px;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #eee;
|
||||||
|
// display:none;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
body.showTimeline &{
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vis-item.message {
|
||||||
|
background: lightgray;
|
||||||
|
&.vis-range{
|
||||||
|
background-color: darkgray;
|
||||||
|
border-color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vis-item.speech {
|
||||||
|
background-color: greenyellow;
|
||||||
|
border-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue