changeset 799:e0e623c01a69

ts build is part of docker now; new web debug console
author drewp@bigasterisk.com
date Fri, 01 Jan 2021 14:17:12 -0800
parents cdc76c84e3e2
children b311e6ca7bbd
files service/mqtt_to_rdf/.devcontainer/devcontainer.json service/mqtt_to_rdf/Dockerfile service/mqtt_to_rdf/mqtt_to_rdf.py service/mqtt_to_rdf/package.json service/mqtt_to_rdf/package.json5 service/mqtt_to_rdf/pnpm-lock.yaml service/mqtt_to_rdf/rollup.config.js service/mqtt_to_rdf/src/index.ts service/mqtt_to_rdf/src/style.styl
diffstat 9 files changed, 320 insertions(+), 159 deletions(-) [+]
line wrap: on
line diff
--- a/service/mqtt_to_rdf/.devcontainer/devcontainer.json	Tue Dec 29 21:05:32 2020 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-{
-  "name": "dev",
-  "context": "..",
-  "dockerFile": "../Dockerfile",
-
-  // Use 'settings' to set *default* container specific settings.json values on container create.
-  // You can edit these settings after create using File > Preferences > Settings > Remote.
-  "settings": {
-    "terminal.integrated.shell.linux": "/bin/bash",
-    "python.pythonPath": "/usr/bin/python3",
-    "python.linting.enabled": true
-    //"python.linting.pylintEnabled": true,
-    //"python.linting.pylintPath": "/usr/local/share/pip-global/bin/pylint"
-  },
-
-  // Use 'appPort' to create a container with published ports. If the port isn't working, be sure
-  // your server accepts connections from all interfaces (0.0.0.0 or '*'), not just localhost.
-  "appPort": [],
-
-  // Add the IDs of extensions you want installed when the container is created in the array below.
-  "extensions": [
-    "ms-python.python",
-    "gregorbiswanger.package-watcher",
-    "hoffs.vscode-versionlens",
-    "esbenp.prettier-vscode"
-  ]
-}
--- a/service/mqtt_to_rdf/Dockerfile	Tue Dec 29 21:05:32 2020 -0800
+++ b/service/mqtt_to_rdf/Dockerfile	Fri Jan 01 14:17:12 2021 -0800
@@ -2,17 +2,33 @@
 
 WORKDIR /opt
 
-COPY requirements.txt ./
-RUN pip3 uninstall --yes enum34
+RUN apt-get update
+RUN apt-get remove -y nodejs
+RUN apt-get install -y wget xz-utils && \
+    wget --output-document=node.tar.xz https://nodejs.org/dist/v14.15.3/node-v14.15.3-linux-x64.tar.xz && \
+    tar xf node.tar.xz && \
+    ln -s node*x64 nodejs
 
-RUN pip3 install --index-url https://projects.bigasterisk.com/ --extra-index-url https://pypi.org/simple -r requirements.txt
-RUN pip3 install -U 'https://github.com/drewp/cyclone/archive/python3.zip?v3'
+ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/workspace/nodejs/bin
+RUN /opt/nodejs/bin/node /opt/nodejs/bin/npm install -g pnpm \
+    && ln -s /opt/nodejs/bin/node /usr/local/bin/node \
+    && ln -s /opt/nodejs/bin/pnpm /usr/local/bin/pnpm
+
+RUN pip3 uninstall --yes enum34
 RUN pip3 install -U attrs
 
-COPY *.py *.html *.css *.js ./
-COPY conf/ ./conf
-COPY build/bundle.js build/
+COPY requirements.txt ./
+RUN pip3 install --index-url https://projects.bigasterisk.com/ --extra-index-url https://pypi.org/simple -r requirements.txt
+RUN pip3 install -U 'https://github.com/drewp/cyclone/archive/python3.zip?v3'
+
+COPY package.json5 pnpm-lock.yaml  ./
+RUN pnpm install
 
-EXPOSE 10018:10018
+COPY tsconfig.json rollup.config.js ./
+COPY src/ ./src
+RUN pnpm build
 
-CMD [ "python3", "./mqtt_to_rdf.py" ]
+COPY *.py *.html ./
+COPY conf/ ./conf
+
+CMD [ "python3", "./mqtt_to_rdf.py", "-v" ]
--- a/service/mqtt_to_rdf/mqtt_to_rdf.py	Tue Dec 29 21:05:32 2020 -0800
+++ b/service/mqtt_to_rdf/mqtt_to_rdf.py	Fri Jan 01 14:17:12 2021 -0800
@@ -1,11 +1,14 @@
 """
 Subscribe to mqtt topics; generate RDF statements.
 """
+import time
 import json
+from logging import debug
 from pathlib import Path
 from typing import Callable, cast
 
 import cyclone.web
+import cyclone.sse
 import prometheus_client
 import rx
 import rx.operators
@@ -21,7 +24,7 @@
 from rdflib.term import Node
 from rx.core import Observable
 from standardservice.logsetup import log, verboseLogging
-from twisted.internet import reactor
+from twisted.internet import reactor, task
 
 from button_events import button_events
 
@@ -30,6 +33,11 @@
 collectors = {}
 
 
+def appendLimit(lst, elem, n=10):
+    del lst[:len(lst) - n + 1]
+    lst.append(elem)
+
+
 def parseDurationLiteral(lit: Literal) -> float:
     if lit.endswith('s'):
         return float(lit.split('s')[0])
@@ -38,33 +46,43 @@
 
 class MqttStatementSource:
 
-    def __init__(self, uri: URIRef, config: Graph, masterGraph: PatchableGraph, mqtt, internalMqtt):
+    def __init__(self, uri: URIRef, config: Graph, masterGraph: PatchableGraph, mqtt, internalMqtt, debugPageData):
         self.uri = uri
         self.config = config
         self.masterGraph = masterGraph
+        self.debugPageData = debugPageData
         self.mqtt = mqtt  # deprecated
         self.internalMqtt = internalMqtt
 
         self.mqttTopic = self.topicFromConfig(self.config)
         log.debug(f'new mqttTopic {self.mqttTopic}')
 
-        statPath = '/subscribed_topic/' + self.mqttTopic.decode('ascii').replace('/', '|')
-        #scales.init(self, statPath)
-        #self._mqttStats = scales.collection(statPath + '/incoming', scales.IntStat('count'), scales.RecentFpsStat('fps'))
-
-        rawBytes = self.subscribeMqtt(self.mqttTopic)
-        rawBytes = self.addFilters(rawBytes)
-        rawBytes = rx.operators.do_action(self.countIncomingMessage)(rawBytes)
-        parsed = self.getParser()(rawBytes)
+        self.debugSub = {
+            'topic': self.mqttTopic.decode('ascii'),
+            'recentMessages': [],
+            'recentParsed': [],
+            'recentConversions': [],
+            'currentMetrics': [],
+            'currentOutputGraph': {
+                't': 1,
+                'n3': "(n3)"
+            },
+        }
+        self.debugPageData['subscribed'].append(self.debugSub)
 
-        g = self.config
-        for conv in g.items(g.value(self.uri, ROOM['conversions'])):
-            parsed = self.conversionStep(conv)(parsed)
+        rawBytes: Observable = self.subscribeMqtt(self.mqttTopic)
+        # rawBytes = rx.operators.do_action(self.countIncomingMessage)(rawBytes)
+        rawBytes.subscribe(on_next=self.countIncomingMessage)
+        # rawBytes = self.addFilters(rawBytes)
+        # parsed = self.getParser()(rawBytes)
 
-        outputQuadsSets = rx.combine_latest(
-            *[self.makeQuads(parsed, plan) for plan in g.objects(self.uri, ROOM['graphStatements'])])
+        # g = self.config
+        # for conv in g.items(g.value(self.uri, ROOM['conversions'])):
+        #     parsed = self.conversionStep(conv)(parsed)
 
-        outputQuadsSets.subscribe_(self.updateQuads)
+        # outputQuadsSets = rx.combine_latest(            *[self.makeQuads(parsed, plan) for plan in g.objects(self.uri, ROOM['graphStatements'])])
+
+        # outputQuadsSets.subscribe_(self.updateQuads)
 
     def addFilters(self, rawBytes):
         jsonEq = self.config.value(self.uri, ROOM['filterPayloadJsonEquals'])
@@ -87,9 +105,13 @@
         mqtt = self.internalMqtt if topic.startswith(b'frontdoorlock') else self.mqtt
         return mqtt.subscribe(topic)
 
-    def countIncomingMessage(self, _):
-        pass  #self._mqttStats.fps.mark()
-        #self._mqttStats.count += 1
+    def countIncomingMessage(self, msg: bytes):
+        self.debugPageData['messagesSeen'] += 1
+
+        appendLimit(self.debugSub['recentMessages'], {
+            't': round(time.time(), 3),
+            'msg': msg.decode('ascii'),
+        })
 
     def getParser(self):
         g = self.config
@@ -199,6 +221,32 @@
         self.write(generate_latest(REGISTRY))
 
 
+class DebugPageData(cyclone.sse.SSEHandler):
+
+    def __init__(self, application, request):
+        cyclone.sse.SSEHandler.__init__(self, application, request)
+        self.lastSent = None
+
+    def watch(self):
+        try:
+            dpd = self.settings.debugPageData
+            js = json.dumps(dpd, sort_keys=True)
+            if js != self.lastSent:
+                print('sending dpd update')
+                self.sendEvent(message=js)
+                self.lastSent = js
+        except Exception:
+            import traceback
+            traceback.print_exc()
+
+    def bind(self):
+        self.loop = task.LoopingCall(self.watch)
+        self.loop.start(1, now=True)
+
+    def unbind(self):
+        self.loop.stop()
+
+
 if __name__ == '__main__':
     arg = docopt("""
     Usage: mqtt_to_rdf.py [options]
@@ -218,14 +266,23 @@
 
     masterGraph = PatchableGraph()
 
+    brokerHost = 'mosquitto-frontdoor.default.svc.cluster.local'
+    brokerPort = 10210
+
+    debugPageData = {
+        # schema in index.ts
+        'server': f'{brokerHost}:{brokerPort}',
+        'messagesSeen': 0,
+        'subscribed': [],
+    }
+
     mqtt = MqttClient(clientId='mqtt_to_rdf', brokerHost='mosquitto-ext.default.svc.cluster.local', brokerPort=1883)  # deprecated
-    internalMqtt = MqttClient(clientId='mqtt_to_rdf',
-                              brokerHost='mosquitto-frontdoor.default.svc.cluster.local',
-                              brokerPort=10210)
+    internalMqtt = MqttClient(clientId='mqtt_to_rdf', brokerHost=brokerHost, brokerPort=brokerPort)
 
     srcs = []
-    for src in config.subjects(RDF.type, ROOM['MqttStatementSource']):
-        srcs.append(MqttStatementSource(src, config, masterGraph, mqtt=mqtt, internalMqtt=internalMqtt))
+    for src in sorted(config.subjects(RDF.type, ROOM['MqttStatementSource'])):
+        srcs.append(
+            MqttStatementSource(src, config, masterGraph, mqtt=mqtt, internalMqtt=internalMqtt, debugPageData=debugPageData))
     log.info(f'set up {len(srcs)} sources')
 
     port = 10018
@@ -244,11 +301,13 @@
                           (r"/graph/mqtt/events", CycloneGraphEventsHandler, {
                               'masterGraph': masterGraph
                           }),
+                          (r'/debugPageData', DebugPageData),
                           (r'/metrics', Metrics),
                       ],
                                               mqtt=mqtt,
                                               internalMqtt=internalMqtt,
                                               masterGraph=masterGraph,
+                                              debugPageData=debugPageData,
                                               debug=arg['-v']),
                       interface='::')
     log.warn('serving on %s', port)
--- a/service/mqtt_to_rdf/package.json	Tue Dec 29 21:05:32 2020 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-{
-  "name": "mqtt_to_rdf",
-  "version": "0.0.1",
-  "scripts": {
-    "build": "rollup -c",
-    "build_forever": "rollup -cw",
-    "test": "jest",
-    "test_forever": "jest --watch"
-  },
-  "dependencies": {
-    "@polymer/polymer": "^3.3.1",
-    "@types/jsonld": "^1.5.0",
-    "jsonld": "^2.0.1",
-    "streamed-graph": "file:/my/proj/streamed-graph",
-    "@types/n3": "^1.1.5",
-    "n3": "link:/my/dl/modified/N3.js",
-    "lit-element": "^2.2.1"
-  },
-  "devDependencies": {
-    "@rollup/plugin-commonjs": "^11.0.1",
-    "@rollup/plugin-node-resolve": "^7.0.0",
-    "@rollup/plugin-replace": "^2.3.0",
-    "@types/jest": "^24.9.0",
-    "@types/rollup-plugin-postcss": "^2.0.0",
-    "add": "^2.0.6",
-    "jest": "^24.9.0",
-    "node-globals": "^0.1.5",
-    "rollup": "^1.29.0",
-    "rollup-plugin-node-builtins": "^2.1.2",
-    "rollup-plugin-postcss": "^2.0.3",
-    "rollup-plugin-terser": "^5.2.0",
-    "rollup-plugin-typescript2": "^0.25.3",
-    "stylus": "^0.54.7",
-    "ts-jest": "^24.3.0",
-    "tslib": "^1.10.0",
-    "typescript": "^3.7.5"
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/mqtt_to_rdf/package.json5	Fri Jan 01 14:17:12 2021 -0800
@@ -0,0 +1,38 @@
+{
+  "name": "mqtt_to_rdf",
+  "version": "0.0.1",
+  "scripts": {
+    "build": "rollup -c",
+    "build_forever": "rollup -cw",
+    "test": "jest",
+    "test_forever": "jest --watch"
+  },
+  "dependencies": {
+    "@polymer/polymer": "^3.3.1",
+    "@types/jsonld": "^1.5.0",
+    "jsonld": "^2.0.1",
+    // "streamed-graph": "file:/my/proj/streamed-graph",
+    // "@types/n3": "^1.1.5",
+    // "n3": "link:/my/dl/modified/N3.js",
+    "lit-element": "^2.2.1"
+  },
+  "devDependencies": {
+    "@rollup/plugin-commonjs": "^11.0.1",
+    "@rollup/plugin-node-resolve": "^7.0.0",
+    "@rollup/plugin-replace": "^2.3.0",
+    "@types/jest": "^24.9.0",
+    "@types/rollup-plugin-postcss": "^2.0.0",
+    "add": "^2.0.6",
+    "jest": "^24.9.0",
+    "node-globals": "^0.1.5",
+    "rollup": "^1.29.0",
+    "rollup-plugin-node-builtins": "^2.1.2",
+    "rollup-plugin-postcss": "^2.0.3",
+    "rollup-plugin-terser": "^5.2.0",
+    "rollup-plugin-typescript2": "^0.25.3",
+    "stylus": "^0.54.7",
+    "ts-jest": "^24.3.0",
+    "tslib": "^1.10.0",
+    "typescript": "^3.7.5"
+  }
+}
--- a/service/mqtt_to_rdf/pnpm-lock.yaml	Tue Dec 29 21:05:32 2020 -0800
+++ b/service/mqtt_to_rdf/pnpm-lock.yaml	Fri Jan 01 14:17:12 2021 -0800
@@ -1,11 +1,8 @@
 dependencies:
   '@polymer/polymer': 3.3.1
   '@types/jsonld': 1.5.0
-  '@types/n3': 1.1.5
   jsonld: 2.0.2
   lit-element: 2.2.1
-  n3: 'link:../../../../../../my/dl/modified/N3.js'
-  streamed-graph: 'link:../../../../../../my/proj/streamed-graph'
 devDependencies:
   '@rollup/plugin-commonjs': 11.0.2_rollup@1.31.0
   '@rollup/plugin-node-resolve': 7.1.1_rollup@1.31.0
@@ -444,26 +441,14 @@
     dev: false
     resolution:
       integrity: sha512-EG2N8JLQ1xDfO6Z/1QRdiUcYX3428CqVRqmY7LyK5or5J1RQ16dpKH6qQ4umVD0vBHU47xHlMeyMbQ6o+6tiYg==
-  /@types/n3/1.1.5:
-    dependencies:
-      '@types/node': 13.7.0
-      '@types/rdf-js': 2.0.11
-    dev: false
-    resolution:
-      integrity: sha512-FaW94FyqTIrPP3ZEiwX745xQhzeoTlNiFsXjxPWsKBd+yvBtIW3ykd9kGnGWI/jz2Rp2iFKto3Tc+IcBL6a6yA==
   /@types/node/13.7.0:
+    dev: true
     resolution:
       integrity: sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==
   /@types/q/1.5.2:
     dev: true
     resolution:
       integrity: sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
-  /@types/rdf-js/2.0.11:
-    dependencies:
-      '@types/node': 13.7.0
-    dev: false
-    resolution:
-      integrity: sha512-GC5MZU2HbL5JnlrLAzoxSqLprqtKwocz0TNVugqM04t1ZeeNFpZRqqBQc9Jhev35hEwdH84siRLaCesxHHYlmA==
   /@types/resolve/0.0.8:
     dependencies:
       '@types/node': 13.7.0
@@ -2927,6 +2912,8 @@
     engines:
       node: '>=4.0'
     optional: true
+    os:
+      - darwin
     requiresBuild: true
     resolution:
       integrity: sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==
@@ -3902,6 +3889,9 @@
       node: '>=6'
     peerDependencies:
       jest-resolve: '*'
+    peerDependenciesMeta:
+      jest-resolve:
+        optional: true
     resolution:
       integrity: sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==
   /jest-regex-util/24.9.0:
@@ -4713,6 +4703,9 @@
     engines:
       node: '>=6'
       npm: ' >=4'
+    os:
+      - darwin
+      - linux
     resolution:
       integrity: sha1-zgW68PKik01AH1X4hwuj9T74CcI=
   /node-int64/0.4.0:
@@ -7092,20 +7085,17 @@
   '@rollup/plugin-replace': ^2.3.0
   '@types/jest': ^24.9.0
   '@types/jsonld': ^1.5.0
-  '@types/n3': ^1.1.5
   '@types/rollup-plugin-postcss': ^2.0.0
   add: ^2.0.6
   jest: ^24.9.0
   jsonld: ^2.0.1
   lit-element: ^2.2.1
-  n3: 'link:/my/dl/modified/N3.js'
   node-globals: ^0.1.5
   rollup: ^1.29.0
   rollup-plugin-node-builtins: ^2.1.2
   rollup-plugin-postcss: ^2.0.3
   rollup-plugin-terser: ^5.2.0
   rollup-plugin-typescript2: ^0.25.3
-  streamed-graph: 'file:/my/proj/streamed-graph'
   stylus: ^0.54.7
   ts-jest: ^24.3.0
   tslib: ^1.10.0
--- a/service/mqtt_to_rdf/rollup.config.js	Tue Dec 29 21:05:32 2020 -0800
+++ b/service/mqtt_to_rdf/rollup.config.js	Fri Jan 01 14:17:12 2021 -0800
@@ -3,6 +3,7 @@
 import resolve from "@rollup/plugin-node-resolve";
 import typescript from "rollup-plugin-typescript2";
 import replace from "@rollup/plugin-replace";
+import postcss from "rollup-plugin-postcss";
 
 const workaround_jsonld_module_system_picker = "process = {version: '1.0.0'}";
 const workaround_some_browser_detector = "global = window";
@@ -39,6 +40,7 @@
     }),
     typescript(),
     commonjs(workaround_jsonld_expand_issue),
+    postcss({ inject: false }),
     replace({ ...replacements, delimiters: ["", ""] }),
   ],
 };
--- a/service/mqtt_to_rdf/src/index.ts	Tue Dec 29 21:05:32 2020 -0800
+++ b/service/mqtt_to_rdf/src/index.ts	Fri Jan 01 14:17:12 2021 -0800
@@ -1,64 +1,168 @@
 // for the web page
 export { DomBind } from "@polymer/polymer/lib/elements/dom-bind.js";
-export { StreamedGraph } from "streamed-graph";
+// export { StreamedGraph } from "streamed-graph";
 
-import { LitElement, property, html, customElement } from "lit-element";
+import {
+  LitElement,
+  property,
+  html,
+  customElement,
+  unsafeCSS,
+} from "lit-element";
 
-import { Literal, N3Store } from "n3";
-import { NamedNode, DataFactory } from "n3";
-const { namedNode, literal } = DataFactory;
+// import { Literal, N3Store } from "n3";
+// import { NamedNode, DataFactory } from "n3";
+// const { namedNode, literal } = DataFactory;
+
+// import { VersionedGraph } from "streamed-graph";
+import style from "./style.styl";
+// import { labelFromUri, graphLiteral, graphUriValue } from "./graph_access";
+
+// const room = "http://projects.bigasterisk.com/room/";
 
-import { VersionedGraph } from "streamed-graph";
-// import style from "./style.styl";
-import { labelFromUri, graphLiteral, graphUriValue } from "./graph_access";
-
-const room = "http://projects.bigasterisk.com/room/";
+// function asString(x: Literal | undefined): string {
+//   if (x && x.value) {
+//     return x.value;
+//   }
+//   return "(unknown)";
+// }
 
-function asString(x: Literal | undefined): string {
-  if (x && x.value) {
-    return x.value;
-  }
-  return "(unknown)";
+interface Msg {
+  t: number;
+  msg: string;
+}
+interface GraphAtTime {
+  t: number;
+  n3: string;
+}
+interface Metric {
+  name: string;
+  labels: { [_: string]: string };
+  value: string;
+}
+interface Subscribed {
+  topic: string;
+  recentMessages: Msg[];
+  recentParsed: GraphAtTime[];
+  recentConversions: GraphAtTime[];
+  currentMetrics: Metric[];
+  currentOutputGraph: GraphAtTime;
+}
+interface PageData {
+  server: string;
+  messagesSeen: number;
+  subscribed: Subscribed[];
 }
 
 @customElement("mqtt-to-rdf-page")
 export class MqttToRdfPage extends LitElement {
-  // static get styles() {
-  //   return [style];
-  // }
+  static get styles() {
+    return unsafeCSS(style);
+  }
 
-  @property({ type: Object })
-  graph!: VersionedGraph;
+  // @property({ type: Object })
+  // graph!: VersionedGraph;
 
   connectedCallback() {
     super.connectedCallback();
-    const sg = this.ownerDocument!.querySelector("streamed-graph");
-    sg?.addEventListener("graph-changed", ((ev: CustomEvent) => {
-      this.graph = ev.detail!.value as VersionedGraph;
-    }) as EventListener);
+    const data = new EventSource("debugPageData");
+    data.addEventListener("message", (ev: { data: string }) => {
+      this.pageData = JSON.parse(ev.data) as PageData;
+      console.log("data update");
+    });
+    //   const sg = this.ownerDocument!.querySelector("streamed-graph");
+    //   sg?.addEventListener("graph-changed", ((ev: CustomEvent) => {
+    //     this.graph = ev.detail!.value as VersionedGraph;
+    //   }) as EventListener);
   }
 
-  static get observers() {
-    return ["onGraphChanged(graph)"];
-  }
+  // static get observers() {
+  //   return ["onGraphChanged(graph)"];
+  // }
+
+  @property({})
+  pageData: PageData = {
+    server: "loading...",
+    messagesSeen: 0,
+    subscribed: [
+      {
+        topic: "top1",
+        recentMessages: [
+          { t: 123456, msg: "one" },
+          { t: 234567, msg: "two" },
+        ],
+        recentParsed: [{ t: 123, n3: ":a :b :c ." }],
+        recentConversions: [],
+        currentMetrics: [],
+        currentOutputGraph: { t: 1, n3: "(n3)" },
+      },
+    ],
+  };
 
   render() {
+    const d = this.pageData;
+    const now = Date.now() / 1000;
+    const ago = (t: number) => html`${Math.round(now - t)}s ago`;
+    const recentMsg = (m: Msg) => html` <div>${ago(m.t)} msg=${m.msg}</div> `;
+    const topicItem = (t: Subscribed, index: number) =>
+      html`<div class="topic" style="grid-column: 1; grid-row: ${index + 2}">
+        ${t.topic} ${t.recentMessages.map(recentMsg)}
+      </div>`;
 
-    return html`
-      <pre>
-       mqtt_to_rdf
-
-      connected to <mqtt server>
+    const parsedMessage = (g: GraphAtTime) =>
+      html` <div class="graph">graph: ${g.n3}</div> `;
+    const parsedMessages = (t: Subscribed, index: number) =>
+      html`
+        <div style="grid-column: 2; grid-row: ${index + 2}">
+          topic=${t.topic} ${t.recentParsed.map(parsedMessage)}
+        </div>
+      `;
 
-      messages received <n from stats page>
+    const metric = (m: Metric) =>
+      html`<div>
+        metrix ${m.name} ${JSON.stringify(m.labels)} = ${m.value}
+      </div>`;
+    const conversions = (t: Subscribed, index: number) =>
+      html`
+        <div style="grid-column: 3; grid-row: ${index + 2}">
+          topic=${t.topic} ${t.recentConversions.map(parsedMessage)}
+        </div>
+      `;
+    const outputMetrics = (t: Subscribed, index: number) =>
+      html`
+        <div style="grid-column: 4; grid-row: ${index + 2}">
+          topic=${t.topic} ${t.currentMetrics.map(metric)}
+        </div>
+      `;
+    const outputGraph = (t: Subscribed, index: number) =>
+      html`
+        <div style="grid-column: 5; grid-row: ${index + 2}">
+          topic=${t.topic} ${parsedMessage(t.currentOutputGraph)}
+        </div>
+      `;
+    return html`
+      <h1>mqtt_to_rdf</h1>
 
-      subscribed topics:
-        ari nightlight temp: 72.2 <graph>
-        ...
+      <section>connected to ${d.server}; messages received ${
+      d.messagesSeen
+    }</section>
+
+      <div class="grid">
+        <div class="hd" style="grid-row: 1; grid-column: 1">subscribed topics</div>
+        ${d.subscribed.map(topicItem)}
 
+        <div class="hd" style="grid-row: 1; grid-column: 2">parsed message: rx stream of Graph</div>
+        ${d.subscribed.map(parsedMessages)}
 
-      </pre>
+        <div class="hd" style="grid-row: 1; grid-column: 3">conversions: rx stream (possible separate times from the previous) of Callable[[Graph], Graph]</div>
+        ${d.subscribed.map(conversions)}
+
+        <div class="hd" style="grid-row: 1; grid-column: 4">output metrics: prom collection according to converted graph</div>
+        ${d.subscribed.map(outputMetrics)}
+
+        <div class="hd" style="grid-row: 1; grid-column: 5">output graph: PatchableGraph</div>
+        ${d.subscribed.map(outputGraph)}
+      </section>
     `;
   }
-
 }
--- a/service/mqtt_to_rdf/src/style.styl	Tue Dec 29 21:05:32 2020 -0800
+++ b/service/mqtt_to_rdf/src/style.styl	Fri Jan 01 14:17:12 2021 -0800
@@ -1,5 +1,22 @@
 
-:host
-  display: flex
-  flex-direction: column
-  padding: 2px 0
+:host {
+  display: flex;
+  flex-direction: column;
+  padding: 2px 0;
+}
+
+.grid {
+  display: grid;
+  grid-auto-columns: minmax(10em, auto);
+  grid-auto-rows: minmax(10em, auto);
+  grid-template-rows: 3em;
+}
+
+.grid * {
+  outline: 1px solid gray;
+  overflow: auto;
+  padding: 3px
+}
+
+.hd 
+  font-weight: bold
\ No newline at end of file