changeset 18:6f38aa08408d

starting over: make a web page that draws a streamed graph from collector, with plans for services to scrape the data that collector will subscribe to
author drewp@bigasterisk.com
date Sun, 09 Jan 2022 00:21:41 -0800
parents a4778c56cc03
children 5751ef191454
files .hgignore Dockerfile deploy.yaml hg_status.py index.html package.json patch_cyclone_sse.py pnpm-lock.yaml pydeps repo_github_status.py repo_local_status.py repo_sync.py requirements.txt root-hgrc skaffold.yaml tsconfig.json view/index.ts vite.config.ts
diffstat 18 files changed, 948 insertions(+), 316 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sun Dec 12 22:32:25 2021 -0800
+++ b/.hgignore	Sun Jan 09 00:21:41 2022 -0800
@@ -1,4 +1,5 @@
 dot-ssh
 __pycache__
 config.yaml
-config.json
\ No newline at end of file
+config.json
+node_modules
--- a/Dockerfile	Sun Dec 12 22:32:25 2021 -0800
+++ b/Dockerfile	Sun Jan 09 00:21:41 2022 -0800
@@ -5,7 +5,10 @@
 RUN apt-get install -y python3 libpython3-dev python3-openssl python3-cffi-backend
 RUN python3 -m pip install mercurial
 
-RUN python3 -m pip install -U pip
+RUN npm install -g npm@8.3.0 
+RUN npm install -g pnpm
+RUN pnpm set registry "https://bigasterisk.com/js/"
+
 COPY requirements.txt ./
 RUN python3 -m pip install --index-url https://projects.bigasterisk.com/ --extra-index-url https://pypi.org/simple -r requirements.txt
 RUN python3 -m pip install -U 'https://github.com/drewp/cyclone/archive/python3.zip?v3'
@@ -13,9 +16,13 @@
 RUN python3 -m pip install 'https://foss.heptapod.net/mercurial/hg-git/-/archive/branch/default/hg-git-branch-default.zip'
 RUN groupadd --gid 1000 drewp && useradd --uid 501 --gid 1000 drewp
 
+COPY package.json .
+RUN pnpm install
+
 COPY root-hgrc /root/.hgrc
-COPY config.yaml hg_status.py index.html ./
 COPY dot-ssh/* /root/.ssh/
+COPY config.yaml package.json pnpm-lock.yaml tsconfig.json vite.config.ts repo*.py patch_cyclone_sse.py index.html ./
+COPY view/ ./view
 #USER drewp
 
-CMD ["python3", "hg_status.py", "-v"]
+CMD ["python3", "repo_local_status.py", "-v"]
--- a/deploy.yaml	Sun Dec 12 22:32:25 2021 -0800
+++ b/deploy.yaml	Sun Jan 09 00:21:41 2022 -0800
@@ -13,24 +13,33 @@
         app: reposync
     spec:
       containers:
-        - name: reposync
+        - name: view
           image: bang5:5000/reposync_image
-          imagePullPolicy: "Always"
-          securityContext: {capabilities: {add: [SYS_PTRACE]}}
+          command:
+            - pnpx
+            - vite
+            - --mode=dev
           ports:
-          - containerPort: 10001
-          volumeMounts:
-            - {name: my, mountPath: /my}
+          - containerPort: 3000
+        # - name: reposync
+        #   image: bang5:5000/reposync_image
+        #   imagePullPolicy: "Always"
+        #   securityContext: {capabilities: {add: [SYS_PTRACE]}}
+        #   ports:
+        #   - containerPort: 10001
+        #   volumeMounts:
+        #     - {name: my, mountPath: /my}
+
       volumes:
         - { name: my, persistentVolumeClaim: { claimName: my } }
       affinity:
         nodeAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
             nodeSelectorTerms:
-            - matchExpressions:
-              - key: "kubernetes.io/hostname"
-                operator: In
-                values: ["bang"]
+              - matchExpressions:
+                  - key: "kubernetes.io/hostname"
+                    operator: In
+                    values: ["bang"]
 ---
 apiVersion: v1
 kind: Service
@@ -38,8 +47,6 @@
   name: reposync
 spec:
   ports:
-  - {port: 80, targetPort: 10001, name: http}
+    - { port: 80, targetPort: 3000, name: http }
   selector:
     app: reposync
-
-    
--- a/hg_status.py	Sun Dec 12 22:32:25 2021 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,217 +0,0 @@
-import datetime
-import json
-import time
-import traceback
-from dataclasses import dataclass, field
-from pathlib import Path
-from typing import Dict, Optional, Tuple
-
-import cyclone.httpserver
-import cyclone.sse
-import cyclone.web
-import docopt
-import treq
-import tzlocal
-from cycloneerr import PrettyErrorHandler
-from dateutil.parser import parse
-from dateutil.tz import tzlocal
-from prometheus_client.exposition import generate_latest
-from prometheus_client.registry import REGISTRY
-from ruamel.yaml import YAML
-from standardservice.logsetup import log, verboseLogging
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.utils import _UnexpectedErrorOutput, getProcessOutput
-
-githubOwner = 'drewp'
-
-
-@inlineCallbacks
-def runHg(cwd, args):
-    if args[0] not in ['push']:
-        args.extend(['-T', 'json'])
-    j = yield getProcessOutput('/usr/local/bin/hg', args, path=cwd)
-    returnValue(json.loads(j) if j else None)
-
-
-@dataclass
-class Repo:
-    path: Path
-    github: bool
-    _cache: Dict[str, Tuple[float, object]] = field(default_factory=dict)
-
-    def _isStale(self, group) -> Optional[object]:
-        now = time.time()
-        if group not in self._cache:
-            return True
-        if now > self._cache[group][0] + 86400:
-            return True
-        print('fresh')
-        return False
-
-    def _save(self, group, obj):
-        now = time.time()
-        self._cache[group] = (now, obj)
-
-    def _get(self, group):
-        print('get')
-        return self._cache[group][1]
-
-    @inlineCallbacks
-    def getStatus(self):
-        if self._isStale('status'):
-            try:
-                statusResp = yield runHg(self.path, ['status'])
-            except Exception as e:
-                status = {'error': repr(e)}
-            else:
-                unknowns = len([row for row in statusResp if row['status'] == '?'])
-                status = {'unknown': unknowns, 'changed': len(statusResp) - unknowns}
-            self._save('status', status)
-        returnValue(self._get('status'))
-
-    @inlineCallbacks
-    def getLatestHgCommit(self):
-        if self._isStale('log'):
-            rows = yield runHg(self.path, ['log', '--limit', '1'])
-            commit = rows[0]
-            sec = commit['date'][0]
-            t = datetime.datetime.fromtimestamp(sec, tzlocal())
-            self._save('log', {'email': commit['user'], 't': t.isoformat(), 'message': commit['desc']})
-        returnValue(self._get('log'))
-
-    @inlineCallbacks
-    def getLatestGithubCommit(self):
-        if self._isStale('github'):
-            resp = yield treq.get(f'https://api.github.com/repos/{githubOwner}/{self.path.name}/commits?per_page=1',
-                                  timeout=5,
-                                  headers={
-                                      'User-agent': 'reposync by github.com/drewp',
-                                      'Accept': 'application/vnd.github.v3+json'
-                                  })
-            ret = yield treq.json_content(resp)
-            commit = ret[0]['commit']
-            t = parse(commit['committer']['date']).astimezone(tzlocal()).isoformat()
-            self._save('github', {'email': commit['committer']['email'], 't': t, 'message': commit['message']})
-        returnValue(self._get('github'))
-
-    @inlineCallbacks
-    def clearGithubMaster(self):
-        '''bang(pts/13):/tmp/reset% git init
-Initialized empty Git repository in /tmp/reset/.git/
-then github set current to a new branch called 'clearing' with https://developer.github.com/v3/repos/#update-a-repository
-bang(pts/13):/tmp/reset% git remote add origin git@github.com:drewp/href.git
-bang(pts/13):/tmp/reset% git push origin :master
-To github.com:drewp/href.git
- - [deleted]         master
-maybe --set-upstream origin
-bang(pts/13):/tmp/reset% git remote set-branches origin master
-?
-then push
-then github setdefault to master
-then github delete clearing
-'''
-
-    @inlineCallbacks
-    def pushToGithub(self):
-        if not self.github:
-            raise ValueError
-        yield runHg(self.path, ['bookmark', '--rev', 'default', 'master'])
-        out = yield runHg(self.path, ['push', f'git+ssh://git@github.com/{githubOwner}/{self.path.name}.git'])
-        print(f'out fompushh {out}')
-
-
-class GithubSync(PrettyErrorHandler, cyclone.web.RequestHandler):
-
-    @inlineCallbacks
-    def post(self):
-        try:
-            path = self.get_argument('repo')
-            repo = [r for r in self.settings.repos if str(r.path) == path][0]
-            yield repo.pushToGithub()
-        except Exception:
-            traceback.print_exc()
-            raise
-
-
-class Statuses(cyclone.sse.SSEHandler):
-
-    def update(self, key, data):
-        self.sendEvent(json.dumps({'key': key, 'update': data}).encode('utf8'))
-
-    def bind(self):
-        self.toProcess = self.settings.repos[:]
-        reactor.callLater(0, self.runOne)
-
-    @inlineCallbacks
-    def runOne(self):
-        if not self.toProcess:
-            print('done')
-            return
-        repo = self.toProcess.pop(0)
-
-        try:
-            update = {'path': str(repo.path), 'github': repo.github, 'status': (yield repo.getStatus()), 'hgLatest': (yield repo.getLatestHgCommit())}
-            if repo.github:
-                update['githubLatest'] = (yield repo.getLatestGithubCommit())
-            self.update(str(repo.path), update)
-        except Exception:
-            log.warn(f'not reporting on {repo}')
-            traceback.print_exc()
-        reactor.callLater(0, self.runOne)
-
-
-class Metrics(cyclone.web.RequestHandler):
-
-    def get(self):
-        self.add_header('content-type', 'text/plain')
-        self.write(generate_latest(REGISTRY))
-
-
-def main():
-    args = docopt.docopt('''
-Usage:
-  hg_status.py [options]
-
-Options:
-  -v, --verbose  more logging
-''')
-    verboseLogging(args['--verbose'])
-
-    # import sys
-    # sys.path.append('/usr/lib/python3/dist-packages')
-    # import OpenSSL
-
-    yaml = YAML(typ='safe')
-    config = yaml.load(open('config.yaml'))
-    repos = [Repo(Path(row['dir']), row['github']) for row in config['hg_repos']]
-
-    class Application(cyclone.web.Application):
-
-        def __init__(self):
-            handlers = [
-                (r"/()", cyclone.web.StaticFileHandler, {
-                    'path': '.',
-                    'default_filename': 'index.html'
-                }),
-                (r'/build/(bundle\.js)', cyclone.web.StaticFileHandler, {
-                    'path': './build/'
-                }),
-                (r'/status/events', Statuses),
-                (r'/githubSync', GithubSync),
-                (r'/metrics', Metrics),
-            ]
-            cyclone.web.Application.__init__(
-                self,
-                handlers,
-                repos=repos,
-                debug=args['--verbose'],
-                template_path='.',
-            )
-
-    reactor.listenTCP(10001, Application())
-    reactor.run()
-
-
-if __name__ == '__main__':
-    main()
--- a/index.html	Sun Dec 12 22:32:25 2021 -0800
+++ b/index.html	Sun Jan 09 00:21:41 2022 -0800
@@ -1,93 +1,24 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
   <head>
-    <meta charset="utf8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>reposync</title>
     <style>
       body {
         background: #1f1f1f;
         color: white;
-        font-family: monospace;
-      }
-      th {
-        text-align: left;
-        white-space: nowrap;
-      }
-      td.github {
-        white-space: nowrap;
-        opacity: 0.5;
-      }
-      td.github.on {
-        opacity: 1;
-      }
-      span.check {
-        font-size: 200%;
       }
     </style>
+    <script type="module" src="https://bigasterisk.com/lib/bigast/v1/loginBar.js"></script>
+    <script type="module" src="/view/index.ts"></script>
   </head>
   <body>
-    <h1>repo statuses</h1>
-
-    <script type="module" src="/loginBar.js">
-    </script>
-
-    <script type="module">
-      import Litedom from "https://bigasterisk.com/lib/litedom/0.12.1/litedom.es.js";
-      const repos = [{ dir: "dir1", summary: "ok" }];
-      Litedom({
-        tagName: "reposync-top",
-        template: `<div id="top">
-                      <table>
-                       <tr :for="repo in this.repos">
-                        <th>{repo.path}</th>
-                        <td> {repo.status.error || ''}
-                          <span :if="repo.status.changed==0 && repo.status.unknown==0">
-                            clean
-                          </span>
-                          <span :else>
-                            changed {repo.status.changed}, unknown {repo.status.unknown}
-                          </span>
-                        </td>
-                        <td :class="github: true; on: repo.github">
-                          <span class="check" :if="repo.github">☑</span>
-                          <span class="check" :else>☐</span> github
-                          <span :if="repo.github">
-                          <table style="display: inline-block">
-                            <tr><td>github latest</td><td>{repo.githubLatest.t}</td></tr>
-                            <tr><td>home latest</td><td>{repo.hgLatest.t}</td></tr>
-                            </table>
-                          </span>
-                        </td>
-                        </tr>
-                      </table>
-                   </div>`,
-        data: { repos: [] },
-        created() {
-          const statuses = new EventSource("status/events");
-          statuses.addEventListener("message", (ev) => {
-            const update = JSON.parse(ev.data);
-            update.update.repoDir = update.key;
-
-            var found = false;
-            this.data.repos.forEach((r) => {
-              if (r.repoDir == update.key) {
-                found = true;
-                Object.assign(r, update.update);
-              }
-            });
-            if (!found) {
-              this.data.repos.push(update.update);
-              this.data.repos.sort((a, b) => (a.repoDir > b.repoDir ? 1 : -1));
-            }
-          });
-        },
-      });
-    </script>
-
-    <reposync-top></reposync-top>
+    <reposync-page></reposync-page>
     <bigast-loginbar></bigast-loginbar>
     <pre>
+      notes: 
+
       repo1 clean                     synced to github   github up to date  [sync now]
             dirty [msg____] [commit]
 
@@ -110,5 +41,33 @@
       Here, keys are repo paths.
     
     </pre>
+
   </body>
 </html>
+<!--
+  before streamed-graph version:
+  template: `<div id="top">
+       <table>
+        <tr :for="repo in this.repos">
+         <th>{repo.path}</th>
+         <td> {repo.status.error || ''}
+           <span :if="repo.status.changed==0 && repo.status.unknown==0">
+             clean
+           </span>
+           <span :else>
+             changed {repo.status.changed}, unknown {repo.status.unknown}
+           </span>
+         </td>
+         <td :class="github: true; on: repo.github">
+           <span class="check" :if="repo.github">☑</span>
+           <span class="check" :else>☐</span> github
+           <span :if="repo.github">
+           <table style="display: inline-block">
+             <tr><td>github latest</td><td>{repo.githubLatest.t}</td></tr>
+             <tr><td>home latest</td><td>{repo.hgLatest.t}</td></tr>
+             </table>
+           </span>
+         </td>
+         </tr>
+       </table>
+-->
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/package.json	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,19 @@
+{
+  "name": "reposync",
+  "version": "1.0.0",
+  "description": "Mirrors a directory of darcs repos onto github.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "lit": "^2.1.1",
+    "streamed-graph": "^0.0.4",
+    "vite": "^2.7.10"
+  },
+  "devDependencies": {
+    "@types/node": "^17.0.8"
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/patch_cyclone_sse.py	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,21 @@
+def patchCycloneSse():
+    import cyclone.sse
+    from cyclone import escape
+
+    def sendEvent(self, message, event=None, eid=None, retry=None):
+        if isinstance(message, dict):
+            message = escape.json_encode(message)
+        if isinstance(message, str):
+            message = message.encode("utf-8")
+        assert isinstance(message, bytes)
+
+        if eid:
+            self.transport.write(b"id: %s\n" % eid)
+        if event:
+            self.transport.write(b"event: %s\n" % event)
+        if retry:
+            self.transport.write(b"retry: %s\n" % retry)
+
+        self.transport.write(b"data: %s\n\n" % message)
+
+    cyclone.sse.SSEHandler.sendEvent = sendEvent
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pnpm-lock.yaml	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,486 @@
+lockfileVersion: 5.3
+
+specifiers:
+  '@types/node': ^17.0.8
+  lit: ^2.1.1
+  streamed-graph: ^0.0.4
+  vite: ^2.7.10
+
+dependencies:
+  lit: 2.1.1
+  streamed-graph: 0.0.4
+
+devDependencies:
+  '@types/node': 17.0.8
+  vite: 2.7.10
+
+packages:
+
+  /@digitalbazaar/http-client/1.2.0:
+    resolution: {integrity: sha512-W9KQQ5pUJcaR0I4c2HPJC0a7kRbZApIorZgPnEDwMBgj16iQzutGLrCXYaZOmxqVLVNqqlQ4aUJh+HBQZy4W6Q==}
+    engines: {node: '>=10.0.0'}
+    dependencies:
+      esm: 3.2.25
+      ky: 0.25.1
+      ky-universal: 0.8.2_ky@0.25.1
+    transitivePeerDependencies:
+      - domexception
+      - web-streams-polyfill
+    dev: false
+
+  /@lit/reactive-element/1.1.1:
+    resolution: {integrity: sha512-B2JdRMwCGv+VpIRj3CYVQBx3muPDeE8y+HPgWqzrAHsO5/40BpwDFZeplIV790BaTqDVUDvZOKMSbuFM9zWC0w==}
+    dev: false
+
+  /@rdfjs/types/1.0.1:
+    resolution: {integrity: sha512-YxVkH0XrCNG3MWeZxfg596GFe+oorTVusmNxRP6ZHTsGczZ8AGvG3UchRNkg3Fy4MyysI7vBAA5YZbESL+VmHQ==}
+    dependencies:
+      '@types/node': 17.0.8
+    dev: false
+
+  /@types/jsonld/1.5.6:
+    resolution: {integrity: sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg==}
+    dev: false
+
+  /@types/n3/1.10.4:
+    resolution: {integrity: sha512-FfRTwcbXcScVHuAjIASveRWL6Fi6fPALl1Ge8tMESYLqU7R42LJvtdBpUi+f9YK0oQPqIN+zFFgMDFJfLMx0bg==}
+    dependencies:
+      '@types/node': 17.0.8
+      rdf-js: 4.0.2
+    dev: false
+
+  /@types/node/17.0.8:
+    resolution: {integrity: sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==}
+
+  /@types/trusted-types/2.0.2:
+    resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
+    dev: false
+
+  /abort-controller/3.0.0:
+    resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+    engines: {node: '>=6.5'}
+    dependencies:
+      event-target-shim: 5.0.1
+    dev: false
+
+  /canonicalize/1.0.8:
+    resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==}
+    dev: false
+
+  /data-uri-to-buffer/3.0.1:
+    resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
+    engines: {node: '>= 6'}
+    dev: false
+
+  /esbuild-android-arm64/0.13.15:
+    resolution: {integrity: sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==}
+    cpu: [arm64]
+    os: [android]
+    dev: true
+    optional: true
+
+  /esbuild-darwin-64/0.13.15:
+    resolution: {integrity: sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==}
+    cpu: [x64]
+    os: [darwin]
+    dev: true
+    optional: true
+
+  /esbuild-darwin-arm64/0.13.15:
+    resolution: {integrity: sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==}
+    cpu: [arm64]
+    os: [darwin]
+    dev: true
+    optional: true
+
+  /esbuild-freebsd-64/0.13.15:
+    resolution: {integrity: sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==}
+    cpu: [x64]
+    os: [freebsd]
+    dev: true
+    optional: true
+
+  /esbuild-freebsd-arm64/0.13.15:
+    resolution: {integrity: sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==}
+    cpu: [arm64]
+    os: [freebsd]
+    dev: true
+    optional: true
+
+  /esbuild-linux-32/0.13.15:
+    resolution: {integrity: sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==}
+    cpu: [ia32]
+    os: [linux]
+    dev: true
+    optional: true
+
+  /esbuild-linux-64/0.13.15:
+    resolution: {integrity: sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==}
+    cpu: [x64]
+    os: [linux]
+    dev: true
+    optional: true
+
+  /esbuild-linux-arm/0.13.15:
+    resolution: {integrity: sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==}
+    cpu: [arm]
+    os: [linux]
+    dev: true
+    optional: true
+
+  /esbuild-linux-arm64/0.13.15:
+    resolution: {integrity: sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==}
+    cpu: [arm64]
+    os: [linux]
+    dev: true
+    optional: true
+
+  /esbuild-linux-mips64le/0.13.15:
+    resolution: {integrity: sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==}
+    cpu: [mips64el]
+    os: [linux]
+    dev: true
+    optional: true
+
+  /esbuild-linux-ppc64le/0.13.15:
+    resolution: {integrity: sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==}
+    cpu: [ppc64]
+    os: [linux]
+    dev: true
+    optional: true
+
+  /esbuild-netbsd-64/0.13.15:
+    resolution: {integrity: sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==}
+    cpu: [x64]
+    os: [netbsd]
+    dev: true
+    optional: true
+
+  /esbuild-openbsd-64/0.13.15:
+    resolution: {integrity: sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==}
+    cpu: [x64]
+    os: [openbsd]
+    dev: true
+    optional: true
+
+  /esbuild-sunos-64/0.13.15:
+    resolution: {integrity: sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==}
+    cpu: [x64]
+    os: [sunos]
+    dev: true
+    optional: true
+
+  /esbuild-windows-32/0.13.15:
+    resolution: {integrity: sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==}
+    cpu: [ia32]
+    os: [win32]
+    dev: true
+    optional: true
+
+  /esbuild-windows-64/0.13.15:
+    resolution: {integrity: sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==}
+    cpu: [x64]
+    os: [win32]
+    dev: true
+    optional: true
+
+  /esbuild-windows-arm64/0.13.15:
+    resolution: {integrity: sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==}
+    cpu: [arm64]
+    os: [win32]
+    dev: true
+    optional: true
+
+  /esbuild/0.13.15:
+    resolution: {integrity: sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==}
+    hasBin: true
+    requiresBuild: true
+    optionalDependencies:
+      esbuild-android-arm64: 0.13.15
+      esbuild-darwin-64: 0.13.15
+      esbuild-darwin-arm64: 0.13.15
+      esbuild-freebsd-64: 0.13.15
+      esbuild-freebsd-arm64: 0.13.15
+      esbuild-linux-32: 0.13.15
+      esbuild-linux-64: 0.13.15
+      esbuild-linux-arm: 0.13.15
+      esbuild-linux-arm64: 0.13.15
+      esbuild-linux-mips64le: 0.13.15
+      esbuild-linux-ppc64le: 0.13.15
+      esbuild-netbsd-64: 0.13.15
+      esbuild-openbsd-64: 0.13.15
+      esbuild-sunos-64: 0.13.15
+      esbuild-windows-32: 0.13.15
+      esbuild-windows-64: 0.13.15
+      esbuild-windows-arm64: 0.13.15
+    dev: true
+
+  /esm/3.2.25:
+    resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
+    engines: {node: '>=6'}
+    dev: false
+
+  /event-target-shim/5.0.1:
+    resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+    engines: {node: '>=6'}
+    dev: false
+
+  /fetch-blob/2.1.2:
+    resolution: {integrity: sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==}
+    engines: {node: ^10.17.0 || >=12.3.0}
+    peerDependencies:
+      domexception: '*'
+    peerDependenciesMeta:
+      domexception:
+        optional: true
+    dev: false
+
+  /fsevents/2.3.2:
+    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+    dev: true
+    optional: true
+
+  /function-bind/1.1.1:
+    resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
+    dev: true
+
+  /has/1.0.3:
+    resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
+    engines: {node: '>= 0.4.0'}
+    dependencies:
+      function-bind: 1.1.1
+    dev: true
+
+  /immutable/4.0.0:
+    resolution: {integrity: sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==}
+    dev: false
+
+  /inherits/2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+    dev: false
+
+  /is-core-module/2.8.1:
+    resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==}
+    dependencies:
+      has: 1.0.3
+    dev: true
+
+  /jsonld/5.2.0:
+    resolution: {integrity: sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw==}
+    engines: {node: '>=12'}
+    dependencies:
+      '@digitalbazaar/http-client': 1.2.0
+      canonicalize: 1.0.8
+      lru-cache: 6.0.0
+      rdf-canonize: 3.0.0
+    transitivePeerDependencies:
+      - domexception
+      - web-streams-polyfill
+    dev: false
+
+  /ky-universal/0.8.2_ky@0.25.1:
+    resolution: {integrity: sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ==}
+    engines: {node: '>=10.17'}
+    peerDependencies:
+      ky: '>=0.17.0'
+      web-streams-polyfill: '>=2.0.0'
+    peerDependenciesMeta:
+      web-streams-polyfill:
+        optional: true
+    dependencies:
+      abort-controller: 3.0.0
+      ky: 0.25.1
+      node-fetch: 3.0.0-beta.9
+    transitivePeerDependencies:
+      - domexception
+    dev: false
+
+  /ky/0.25.1:
+    resolution: {integrity: sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA==}
+    engines: {node: '>=10'}
+    dev: false
+
+  /lit-element/3.1.1:
+    resolution: {integrity: sha512-14ClnMAU8EXnzC+M2/KDd3SFmNUn1QUw1+GxWkEMwGV3iaH8ObunMlO5svzvaWlkSV0WlxJCi40NGnDVJ2XZKQ==}
+    dependencies:
+      '@lit/reactive-element': 1.1.1
+      lit-html: 2.1.1
+    dev: false
+
+  /lit-html/2.1.1:
+    resolution: {integrity: sha512-E4BImK6lopAYanJpvcGaAG8kQFF1ccIulPu2BRNZI7acFB6i4ujjjsnaPVFT1j/4lD9r8GKih0Y8d7/LH8SeyQ==}
+    dependencies:
+      '@types/trusted-types': 2.0.2
+    dev: false
+
+  /lit/2.1.1:
+    resolution: {integrity: sha512-yqDqf36IhXwOxIQSFqCMgpfvDCRdxLCLZl7m/+tO5C9W/OBHUj17qZpiMBT35v97QMVKcKEi1KZ3hZRyTwBNsQ==}
+    dependencies:
+      '@lit/reactive-element': 1.1.1
+      lit-element: 3.1.1
+      lit-html: 2.1.1
+    dev: false
+
+  /lru-cache/6.0.0:
+    resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+    engines: {node: '>=10'}
+    dependencies:
+      yallist: 4.0.0
+    dev: false
+
+  /n3/1.12.2:
+    resolution: {integrity: sha512-vY1HBEraMPWQFLEK6sn67DeGMqTuwXnlEYpZ8gTVukKQSz2f44d+t+ZcmwEt8c99FlVAbpmMb/435Q8t0OC+7w==}
+    engines: {node: '>=8.0'}
+    dependencies:
+      queue-microtask: 1.2.3
+      readable-stream: 3.6.0
+    dev: false
+
+  /nanoid/3.1.30:
+    resolution: {integrity: sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+    dev: true
+
+  /node-fetch/3.0.0-beta.9:
+    resolution: {integrity: sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==}
+    engines: {node: ^10.17 || >=12.3}
+    dependencies:
+      data-uri-to-buffer: 3.0.1
+      fetch-blob: 2.1.2
+    transitivePeerDependencies:
+      - domexception
+    dev: false
+
+  /path-parse/1.0.7:
+    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+    dev: true
+
+  /picocolors/1.0.0:
+    resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
+    dev: true
+
+  /postcss/8.4.5:
+    resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==}
+    engines: {node: ^10 || ^12 || >=14}
+    dependencies:
+      nanoid: 3.1.30
+      picocolors: 1.0.0
+      source-map-js: 1.0.1
+    dev: true
+
+  /queue-microtask/1.2.3:
+    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+    dev: false
+
+  /rdf-canonize/3.0.0:
+    resolution: {integrity: sha512-LXRkhab1QaPJnhUIt1gtXXKswQCZ9zpflsSZFczG7mCLAkMvVjdqCGk9VXCUss0aOUeEyV2jtFxGcdX8DSkj9w==}
+    engines: {node: '>=12'}
+    dependencies:
+      setimmediate: 1.0.5
+    dev: false
+
+  /rdf-js/4.0.2:
+    resolution: {integrity: sha512-ApvlFa/WsQh8LpPK/6hctQwG06Z9ztQQGWVtrcrf9L6+sejHNXLPOqL+w7q3hF+iL0C4sv3AX1PUtGkLNzyZ0Q==}
+    dependencies:
+      '@rdfjs/types': 1.0.1
+    dev: false
+
+  /readable-stream/3.6.0:
+    resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
+    engines: {node: '>= 6'}
+    dependencies:
+      inherits: 2.0.4
+      string_decoder: 1.3.0
+      util-deprecate: 1.0.2
+    dev: false
+
+  /resolve/1.21.0:
+    resolution: {integrity: sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==}
+    hasBin: true
+    dependencies:
+      is-core-module: 2.8.1
+      path-parse: 1.0.7
+      supports-preserve-symlinks-flag: 1.0.0
+    dev: true
+
+  /rollup/2.63.0:
+    resolution: {integrity: sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==}
+    engines: {node: '>=10.0.0'}
+    hasBin: true
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
+  /safe-buffer/5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+    dev: false
+
+  /setimmediate/1.0.5:
+    resolution: {integrity: sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=}
+    dev: false
+
+  /source-map-js/1.0.1:
+    resolution: {integrity: sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /streamed-graph/0.0.4:
+    resolution: {integrity: sha512-uatILnTW0GKmSf21MoiRelrrw1D8rFYajIXmWBSW90jINMFlzdYA03gWFeB4IFKFs/RTeqbgOr1OPQOQwJ6Lgw==}
+    dependencies:
+      '@types/jsonld': 1.5.6
+      '@types/n3': 1.10.4
+      immutable: 4.0.0
+      jsonld: 5.2.0
+      lit: 2.1.1
+      n3: 1.12.2
+      rdf-js: 4.0.2
+    transitivePeerDependencies:
+      - domexception
+      - web-streams-polyfill
+    dev: false
+
+  /string_decoder/1.3.0:
+    resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+    dependencies:
+      safe-buffer: 5.2.1
+    dev: false
+
+  /supports-preserve-symlinks-flag/1.0.0:
+    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+    engines: {node: '>= 0.4'}
+    dev: true
+
+  /util-deprecate/1.0.2:
+    resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
+    dev: false
+
+  /vite/2.7.10:
+    resolution: {integrity: sha512-KEY96ntXUid1/xJihJbgmLZx7QSC2D4Tui0FdS0Old5OokYzFclcofhtxtjDdGOk/fFpPbHv9yw88+rB93Tb8w==}
+    engines: {node: '>=12.2.0'}
+    hasBin: true
+    peerDependencies:
+      less: '*'
+      sass: '*'
+      stylus: '*'
+    peerDependenciesMeta:
+      less:
+        optional: true
+      sass:
+        optional: true
+      stylus:
+        optional: true
+    dependencies:
+      esbuild: 0.13.15
+      postcss: 8.4.5
+      resolve: 1.21.0
+      rollup: 2.63.0
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
+  /yallist/4.0.0:
+    resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+    dev: false
--- a/pydeps	Sun Dec 12 22:32:25 2021 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-PyGithub==1.14.2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/repo_github_status.py	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,3 @@
+"""
+repos from config.yaml that are at github -> rdf data
+"""
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/repo_local_status.py	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,223 @@
+"""
+configured hg dirs and settings -> rdf graph
+"""
+import datetime
+import json
+import time
+import traceback
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Dict, Optional, Tuple
+
+import cyclone.httpserver
+import cyclone.sse
+import cyclone.web
+import docopt
+import treq
+import tzlocal
+from cycloneerr import PrettyErrorHandler
+from dateutil.parser import parse
+from dateutil.tz import tzlocal
+from prometheus_client.exposition import generate_latest
+from prometheus_client.registry import REGISTRY
+from ruamel.yaml import YAML
+from standardservice.logsetup import log, verboseLogging
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.utils import _UnexpectedErrorOutput, getProcessOutput
+
+from patch_cyclone_sse import patchCycloneSse
+patchCycloneSse()
+
+githubOwner = 'drewp'
+
+
+@inlineCallbacks
+def runHg(cwd, args):
+    if args[0] not in ['push']:
+        args.extend(['-T', 'json'])
+    j = yield getProcessOutput('/usr/local/bin/hg', args, path=cwd)
+    returnValue(json.loads(j) if j else None)
+
+
+@dataclass
+class Repo:
+    path: Path
+    github: bool
+    _cache: Dict[str, Tuple[float, object]] = field(default_factory=dict)
+
+    def _isStale(self, group) -> Optional[object]:
+        now = time.time()
+        if group not in self._cache:
+            return True
+        if now > self._cache[group][0] + 86400:
+            return True
+        print('fresh')
+        return False
+
+    def _save(self, group, obj):
+        now = time.time()
+        self._cache[group] = (now, obj)
+
+    def _get(self, group):
+        print('get')
+        return self._cache[group][1]
+
+    @inlineCallbacks
+    def getStatus(self):
+        if self._isStale('status'):
+            try:
+                statusResp = yield runHg(self.path, ['status'])
+            except Exception as e:
+                status = {'error': repr(e)}
+            else:
+                unknowns = len([row for row in statusResp if row['status'] == '?'])
+                status = {'unknown': unknowns, 'changed': len(statusResp) - unknowns}
+            self._save('status', status)
+        returnValue(self._get('status'))
+
+    @inlineCallbacks
+    def getLatestHgCommit(self):
+        if self._isStale('log'):
+            rows = yield runHg(self.path, ['log', '--limit', '1'])
+            commit = rows[0]
+            sec = commit['date'][0]
+            t = datetime.datetime.fromtimestamp(sec, tzlocal())
+            self._save('log', {'email': commit['user'], 't': t.isoformat(), 'message': commit['desc']})
+        returnValue(self._get('log'))
+
+    @inlineCallbacks
+    def getLatestGithubCommit(self):
+        if self._isStale('github'):
+            resp = yield treq.get(f'https://api.github.com/repos/{githubOwner}/{self.path.name}/commits?per_page=1',
+                                  timeout=5,
+                                  headers={
+                                      'User-agent': 'reposync by github.com/drewp',
+                                      'Accept': 'application/vnd.github.v3+json'
+                                  })
+            ret = yield treq.json_content(resp)
+            commit = ret[0]['commit']
+            t = parse(commit['committer']['date']).astimezone(tzlocal()).isoformat()
+            self._save('github', {'email': commit['committer']['email'], 't': t, 'message': commit['message']})
+        returnValue(self._get('github'))
+
+    @inlineCallbacks
+    def clearGithubMaster(self):
+        '''bang(pts/13):/tmp/reset% git init
+Initialized empty Git repository in /tmp/reset/.git/
+then github set current to a new branch called 'clearing' with https://developer.github.com/v3/repos/#update-a-repository
+bang(pts/13):/tmp/reset% git remote add origin git@github.com:drewp/href.git
+bang(pts/13):/tmp/reset% git push origin :master
+To github.com:drewp/href.git
+ - [deleted]         master
+maybe --set-upstream origin
+bang(pts/13):/tmp/reset% git remote set-branches origin master
+?
+then push
+then github setdefault to master
+then github delete clearing
+'''
+
+    @inlineCallbacks
+    def pushToGithub(self):
+        if not self.github:
+            raise ValueError
+        yield runHg(self.path, ['bookmark', '--rev', 'default', 'master'])
+        out = yield runHg(self.path, ['push', f'git+ssh://git@github.com/{githubOwner}/{self.path.name}.git'])
+        print(f'out fompushh {out}')
+
+
+class GithubSync(PrettyErrorHandler, cyclone.web.RequestHandler):
+
+    @inlineCallbacks
+    def post(self):
+        try:
+            path = self.get_argument('repo')
+            repo = [r for r in self.settings.repos if str(r.path) == path][0]
+            yield repo.pushToGithub()
+        except Exception:
+            traceback.print_exc()
+            raise
+
+
+class Statuses(cyclone.sse.SSEHandler):
+
+    def update(self, key, data):
+        self.sendEvent(json.dumps({'key': key, 'update': data}).encode('utf8'))
+
+    def bind(self):
+        self.toProcess = self.settings.repos[:]
+        reactor.callLater(0, self.runOne)
+
+    @inlineCallbacks
+    def runOne(self):
+        if not self.toProcess:
+            print('done')
+            return
+        repo = self.toProcess.pop(0)
+
+        try:
+            update = {'path': str(repo.path), 'github': repo.github, 'status': (yield repo.getStatus()), 'hgLatest': (yield repo.getLatestHgCommit())}
+            if repo.github:
+                update['githubLatest'] = (yield repo.getLatestGithubCommit())
+            self.update(str(repo.path), update)
+        except Exception:
+            log.warn(f'not reporting on {repo}')
+            traceback.print_exc()
+        reactor.callLater(0, self.runOne)
+
+
+class Metrics(cyclone.web.RequestHandler):
+
+    def get(self):
+        self.add_header('content-type', 'text/plain')
+        self.write(generate_latest(REGISTRY))
+
+
+def main():
+    args = docopt.docopt('''
+Usage:
+  hg_status.py [options]
+
+Options:
+  -v, --verbose  more logging
+''')
+    verboseLogging(args['--verbose'])
+
+    # import sys
+    # sys.path.append('/usr/lib/python3/dist-packages')
+    # import OpenSSL
+
+    yaml = YAML(typ='safe')
+    config = yaml.load(open('config.yaml'))
+    repos = [Repo(Path(row['dir']), row['github']) for row in config['hg_repos']]
+
+    class Application(cyclone.web.Application):
+
+        def __init__(self):
+            handlers = [
+                (r"/()", cyclone.web.StaticFileHandler, {
+                    'path': '.',
+                    'default_filename': 'index.html'
+                }),
+                (r'/build/(bundle\.js)', cyclone.web.StaticFileHandler, {
+                    'path': './build/'
+                }),
+                (r'/status/events', Statuses),
+                (r'/githubSync', GithubSync),
+                (r'/metrics', Metrics),
+            ]
+            cyclone.web.Application.__init__(
+                self,
+                handlers,
+                repos=repos,
+                debug=args['--verbose'],
+                template_path='.',
+            )
+
+    reactor.listenTCP(10001, Application())
+    reactor.run()
+
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/repo_sync.py	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,3 @@
+"""
+service to sync repos from local to github
+"""
--- a/requirements.txt	Sun Dec 12 22:32:25 2021 -0800
+++ b/requirements.txt	Sun Jan 09 00:21:41 2022 -0800
@@ -4,11 +4,12 @@
 prometheus_client==0.12.0
 pyopenssl
 python-dateutil==2.8.2
-requests==2.26.0
-ruamel.yaml==0.17.17
+requests==2.27.1
+ruamel.yaml==0.17.20
 treq==21.5.0
 twisted[tls]
 tzlocal==4.1
+# PyGithub==1.14.2
 
 cycloneerr==0.4.0
 standardservice==0.6.0
--- a/root-hgrc	Sun Dec 12 22:32:25 2021 -0800
+++ b/root-hgrc	Sun Jan 09 00:21:41 2022 -0800
@@ -3,4 +3,4 @@
 
 [extensions]
 hgext.bookmarks =
-hggit = /usr/local/lib/python3.8/dist-packages/hggit
\ No newline at end of file
+hggit = /usr/local/lib/python3.9/dist-packages/hggit
\ No newline at end of file
--- a/skaffold.yaml	Sun Dec 12 22:32:25 2021 -0800
+++ b/skaffold.yaml	Sun Jan 09 00:21:41 2022 -0800
@@ -9,9 +9,10 @@
       timezone: "Local"
   artifacts:
   - image: bang5:5000/reposync_image
-    sync: # files that could be patched sans python restart
+    sync: # files that could be patched and vite would pick them up
       infer:
       - index.html
+      - "view/*.ts"
 deploy:
   kubectl:
     manifests:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tsconfig.json	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,71 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+    /* Basic Options */
+    // "incremental": true,                         /* Enable incremental compilation */
+    "target": "es2020",                             /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
+    "module": "es2020",                             /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+    // "lib": [],                                   /* Specify library files to be included in the compilation. */
+    // "allowJs": true,                             /* Allow javascript files to be compiled. */
+    // "checkJs": true,                             /* Report errors in .js files. */
+    // "jsx": "preserve",                           /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
+    // "declaration": true,                         /* Generates corresponding '.d.ts' file. */
+    // "declarationMap": true,                      /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    // "sourceMap": true,                           /* Generates corresponding '.map' file. */
+    // "outFile": "./",                             /* Concatenate and emit output to single file. */
+    // "outDir": "./",                              /* Redirect output structure to the directory. */
+    // "rootDir": "./",                             /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                           /* Enable project compilation */
+    // "tsBuildInfoFile": "./",                     /* Specify file to store incremental compilation information */
+    // "removeComments": true,                      /* Do not emit comments to output. */
+    // "noEmit": true,                              /* Do not emit outputs. */
+    // "importHelpers": true,                       /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,                  /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,                     /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+    /* Strict Type-Checking Options */
+    "strict": true,                                 /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                       /* Raise error on expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,                    /* Enable strict null checks. */
+    // "strictFunctionTypes": true,                 /* Enable strict checking of function types. */
+    // "strictBindCallApply": true,                 /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    // "strictPropertyInitialization": true,        /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                      /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                        /* Parse in strict mode and emit "use strict" for each source file. */
+
+    /* Additional Checks */
+    // "noUnusedLocals": true,                      /* Report errors on unused locals. */
+    // "noUnusedParameters": true,                  /* Report errors on unused parameters. */
+    // "noImplicitReturns": true,                   /* Report error when not all code paths in function return a value. */
+    // "noFallthroughCasesInSwitch": true,          /* Report errors for fallthrough cases in switch statement. */
+    // "noUncheckedIndexedAccess": true,            /* Include 'undefined' in index signature results */
+    // "noPropertyAccessFromIndexSignature": true,  /* Require undeclared properties from index signatures to use element accesses. */
+
+    /* Module Resolution Options */
+    "moduleResolution": "node",                  /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    // "baseUrl": "./",                             /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                                 /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                              /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                             /* List of folders to include type definitions from. */
+    // "types": [],                                 /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,        /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true,                        /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    // "preserveSymlinks": true,                    /* Do not resolve the real path of symlinks. */
+    // "allowUmdGlobalAccess": true,                /* Allow accessing UMD globals from modules. */
+
+    /* Source Map Options */
+    // "sourceRoot": "",                            /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                               /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,                     /* Emit a single file with source maps instead of having a separate file. */
+    // "inlineSources": true,                       /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+    /* Experimental Options */
+    "experimentalDecorators": true,              /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,               /* Enables experimental support for emitting type metadata for decorators. */
+
+    /* Advanced Options */
+    "skipLibCheck": true,                           /* Skip type checking of declaration files. */
+    "forceConsistentCasingInFileNames": true        /* Disallow inconsistently-cased references to the same file. */
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/view/index.ts	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,37 @@
+import { LitElement, html, css } from "lit";
+import { customElement, property } from "lit/decorators.js";
+export { StreamedGraph } from "streamed-graph";
+
+@customElement("reposync-page")
+export class ReposyncPage extends LitElement {
+  static styles = [
+    css`
+      :host {
+        display: block;
+      }
+
+      th {
+        text-align: left;
+        white-space: nowrap;
+      }
+      td.github {
+        white-space: nowrap;
+        opacity: 0.5;
+      }
+      td.github.on {
+        opacity: 1;
+      }
+      span.check {
+        font-size: 200%;
+      }
+    `,
+  ];
+
+
+  render() {
+    return html`
+      <h1>repo statuses yay</h1>
+      <streamed-graph url="/collector/graph/home" expanded="true"></streamed-graph>
+    `;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vite.config.ts	Sun Jan 09 00:21:41 2022 -0800
@@ -0,0 +1,11 @@
+import { defineConfig } from "vite";
+
+export default defineConfig({
+  base: "https://bigasterisk.com/reposync/",
+  server: {
+    host: "0.0.0.0",
+    hmr: {
+      port: 443,
+    },
+  },
+});