Renewed branch with main
diff --git a/server/app/plugins/aliases.yml b/server/app/plugins/aliases.yml
new file mode 100644
index 0000000..77ac602
--- /dev/null
+++ b/server/app/plugins/aliases.yml
@@ -0,0 +1,34 @@
+---
+# OFFICER ALIASES
+  assistant secretary: "assistantsecretary"
+  assistant v.p. legal affairs: "assistantvplegalaffairs"
+  board chair: "boardchair"
+  exec. v.p.: "execvp"
+  infrastructure administrator: "infrastructureadministrator"
+  sponsor relations: "sponsorrelations"
+  vice chair: "vicechair"
+  w3c relations: "w3crelations"
+
+# TLP ALIASES
+  brand management: "brand"
+  c++ standard library: "stdcxx"
+  community development: "comdev"
+  conference planning: "concom"
+  conferences: "concom"
+  distributed release audit tool: "drat"
+  diversity and inclusion: "diversity"
+  http server: "httpd"
+  httpserver: "httpd"
+  incubating: "incubator # special for index.html"
+  java community process: "jcp"
+  legal affairs: "legal"
+  logging services: "logging"
+  logo development: "logodev"
+  lucene.net: "lucenenet"
+  open climate workbench: "climate"
+  ocw: "climate # is OCW used?"
+  portable runtime: "apr"
+  quetzalcoatl: "quetz"
+  security team: "security"
+  travel assistance: "tac"
+  wb services: "ws"
diff --git a/server/app/plugins/committees.py b/server/app/plugins/committees.py
new file mode 100644
index 0000000..2e463d4
--- /dev/null
+++ b/server/app/plugins/committees.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""ASF Infrastructure Reporting Dashboard - Committee Info"""
+import asyncio
+
+# from ..lib import config
+# from .. import plugins
+import aiohttp
+import json
+import time
+import yaml
+import getpass
+import svn.remote
+import svn.local
+import svn.exception
+import re
+import datetime
+import sys
+
+NAME_MAP = None
+COMMITTEES = None
+COMMITTEE_INFO_JSON = None
+
+# JSON get committee_info.json
+def get_committee_info():
+    return globals()["COMMITTEE_INFO_JSON"]
+
+
+def get_different_info():
+    return True
+
+
+# Needs to be async
+def load_data():
+    creds = get_creds()
+    client = svn.remote.RemoteClient(
+        "https://svn.apache.org/repos/private/committers/board",
+        creds["usernm"],
+        creds["passwd"],
+    )
+    globals()["NAME_MAP"] = yaml.safe_load(open("aliases.yml", "r"))
+    try:
+        globals()["COMMITTEES"] = re.split(
+            "\n\d\.", client.cat("committee-info.txt").decode("utf-8")
+        )
+    except svn.exception.SvnException as e:
+        sys.stderr.write(
+            "There was an error in the SVN call, please ensure your credentials are correct"
+        )
+        sys.exit(0)
+
+
+class Committee:
+    def __init__(self):
+        self.chairs = []
+        self.roster = {}
+        self.established = None
+        self.paragraph = None
+        self.report_sched = []
+        self.display_name = None
+
+
+# Needs to go away and use system creds instead
+def get_creds():
+    sys.stderr.write("Username: ")
+    return {"usernm": input(), "passwd": getpass.getpass("Password: ")}
+
+
+def set_key(project):
+    if project.lower() in globals()["NAME_MAP"].keys():
+        key = globals()["NAME_MAP"][project.lower()]
+    else:
+        key = project.lower()
+    return key
+
+
+def parse_rosters(data):
+    projects = re.split("\n\* ", data)
+    projects_processed = 0
+    boards_processed = 0
+    committees = {}
+    pmcs = {}
+    roster_counts = {}
+    for project in projects[1:]:
+        proj = Committee()
+        roster = {}
+        key = None
+        ctype = None
+        p = project.split("\n")
+        for l in p:
+            if l and re.match(r"^[A-z]", l):
+                t = [item.strip(")").strip() for item in l.split(" (")]
+                ctype = t[-1].lstrip("(")
+                setattr(proj, "display_name", t[0].capitalize())
+                setattr(proj, "established", t[1])
+                key = set_key(t[0])
+            if l and re.match(r"^\s{4}", l):
+                u = re.split(r"\s{2,}", l.strip("^    "))
+                if len(u) > 2:
+                    roster[u[0]] = {"name": f"{u[1]}", "date": f"{u[2]}"}
+            setattr(proj, "roster", roster)
+            setattr(proj, "roster_count", len(roster))
+        if ctype == "President's Committee" or ctype == "Board Committee":
+            committees[key] = vars(proj)
+            boards_processed += 1
+        else:
+            roster_counts[key] = len(roster.keys())
+            pmcs[key] = vars(proj)
+            projects_processed += 1
+    return projects_processed, pmcs, roster_counts, committees, boards_processed
+
+
+def parse_committees_info():
+    """builds committee-info.json"""
+    (
+        projects_processed,
+        pmcs,
+        roster_counts,
+        committees,
+        boards_processed,
+    ) = parse_rosters(COMMITTEES[3])
+    chairs, boards, prescoms, execs, officers = re.split(r":\n\n", COMMITTEES[1])[1:]
+    pmcs.update(committees)
+    # Parse Chairs
+    for chair in chairs.split("\n")[2:-3]:
+        c = re.split("\s\s+", chair.lstrip())
+        key = set_key(c[0])
+
+        # weird edge cases here:
+        if re.match(r"portable runtime", c[0].lower()):
+            key = "apr"
+
+        if key in pmcs.keys():
+            pmcs[key]["chair"] = {}
+            pmcs[key]["chair"][c[1].split()[0]] = {
+                "name": c[1].split()[1].split("@")[0]
+            }
+        else:
+            continue
+    officers_temp = {}
+
+    # Parse Boards
+    for board in boards.split("\n")[2:-3]:
+        b = re.split("\s\s+", board.lstrip())
+        temp_roster = b[1].strip(">").split(" <")
+        temp_roster.append(temp_roster[1].split("@")[0])
+        committees[set_key(b[0])] = {
+            "display_name": b[0],
+            "paragraph": "Committee",
+            "roster": {f"{temp_roster[2]}": {"name": temp_roster[0]}},
+        }
+
+    # parse Presidential Committees
+    for prescom in prescoms.split("\n")[2:-3]:
+        p = re.split("\s\s+", prescom.lstrip())
+
+    # parse Executive Officers
+    for executive in execs.split("\n")[2:-3]:
+        x = re.split("\s\s+", executive.lstrip())
+        temp_roster = x[1].strip(">").split(" <")
+        temp_roster.append(temp_roster[1].split("@")[0])
+        officers_temp[set_key(x[0])] = {
+            "display_name": x[0],
+            "paragraph": "Executive Officer",
+            "roster": {f"{temp_roster[2]}": {"name": temp_roster[0]}},
+        }
+
+    # parse Additional Officers
+    for officer in officers.split("\n")[2:-9]:
+        o = re.split("\s\s+", officer.lstrip())
+        temp_roster = o[1].strip(">").split(" <")
+        temp_roster.append(temp_roster[1].split("@")[0])
+        officers_temp[set_key(o[0])] = {
+            "display_name": o[0],
+            "paragraph": "Additional Officers",
+            "roster": {f"{temp_roster[2]}": {"name": temp_roster[0]}},
+        }
+
+    data = {
+        "last_updated": str(datetime.datetime.now().isoformat()),
+        "roster_counts": roster_counts,
+        "committees": pmcs,
+        "committee_count": projects_processed + boards_processed,
+        "pmc_count": projects_processed,
+        "officers": officers_temp,
+    }
+
+    globals()["COMMITTEE_INFO"] = data
+
+
+async def scan_loop():
+    while True:
+        await parse_committees_info()
+        await asyncio.sleep(300)
+
+
+load_data()
+parse_committees_info()
+print(globals()["COMMITTEE_INFO"])
+
+# plugins.root.register(scan_loop, slug="json", title="Mail Transport Statistics", icon="bi-envelope-exclamation-fill", private=True)