diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/__init__.py | 166 | ||||
| -rw-r--r-- | web/favicon.ico | bin | 0 -> 302430 bytes | |||
| -rw-r--r-- | web/templates/base.html | 74 | ||||
| -rw-r--r-- | web/templates/home.html | 66 | ||||
| -rw-r--r-- | web/templates/project.html | 119 | ||||
| -rw-r--r-- | web/templates/run.html | 19 |
6 files changed, 444 insertions, 0 deletions
diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..3daef78 --- /dev/null +++ b/web/__init__.py @@ -0,0 +1,166 @@ +import json +import os +import queue +import re +import threading +import time +from typing import Any + +from flask import ( + Flask, + abort, + send_from_directory, + stream_template, +) + +MSCI_HOME = os.environ.get("MSCI_HOME") + + +def get_project(project: str) -> dict[str, Any]: + with open(f"{MSCI_HOME}/projects/{project}.json", "r") as p: + return json.loads(p.read()) + + +def get_run(project: str, run: int) -> str: + with open(f"{MSCI_HOME}/stdout/public/{project}/{run}", "r") as r: + return r.read() + + +def stream_run(project: str, run: int): + while True: + with open(f"{MSCI_HOME}/stdout/public/{project}/{run}", "r") as r: + txt = r.read() + yield txt + if "--MSCI_EXIT_" in txt: + break + time.sleep(0.5) + + +def _projects(q: queue.Queue[tuple[str, dict[str, Any]] | None]): + if os.path.isdir(f"{MSCI_HOME}/projects"): + _projects = os.listdir(f"{MSCI_HOME}/projects") + for _p in _projects: + loaded = get_project(_p.replace(".json", "")) + if not loaded["hidden"]: + q.put((_p.replace(".json", ""), loaded)) + else: + os.mkdir(f"{MSCI_HOME}/projects") + q.put(None) + + +def projects(): + q: queue.Queue[tuple[str, dict[str, Any]] | None] = queue.Queue() + threading.Thread(target=_projects, args=(q,), daemon=True).start() + while True: + pr = q.get() + if pr is None: + break + yield pr + + +def _project_runs(project: str, q: queue.Queue[dict[str, Any] | None]): + pr_stdout = f"{MSCI_HOME}/stdout/public/{project}" + if os.path.isdir(pr_stdout): + _runs = sorted(os.listdir(pr_stdout), key=int) + for _r in _runs: + run_str = get_run(project, int(_r)) + run_date = re.search(r"--MSCI_DATE\((.*?)\)--", run_str) + run_status = ( + True + if "--MSCI_EXIT_SUCCESS--" in run_str + else False if "--MSCI_EXIT_FAILURE--" in run_str else None + ) + run_data = { + "number": int(_r), + "date": run_date.group(1) if run_date else None, + "status": run_status, + } + q.put(run_data) + q.put(None) + + +def project_runs(project: str): + q: queue.Queue[dict[str, Any] | None] = queue.Queue() + threading.Thread( + target=_project_runs, + args=( + project, + q, + ), + daemon=True, + ).start() + while True: + pr = q.get() + if pr is None: + break + yield pr + + +def project_exists(name: str): + return ( + os.path.isfile(f"{MSCI_HOME}/projects/{name}.json") + and get_project(name)["hidden"] == False + ) + + +def run_exists(project: str, run: int): + if project_exists(project): + if not os.path.isfile(f"{MSCI_HOME}/stdout/public/{project}/{run}"): + return False + return True + return False + + +_app = Flask(__name__) +_empty = Flask(__name__) + + +@_empty.route("/", methods=["GET"]) +def ey(): + return "MSCI_HOME not set" + + +@_app.route("/favicon.ico") +def favicon(): + return send_from_directory( + _app.root_path, + "favicon.ico", + mimetype="image/vnd.microsoft.icon", + ) + + +@_app.route("/", methods=["GET"]) +def home(): + return stream_template("home.html", projects=projects()) + + +@_app.route("/<string:project>", methods=["GET"]) +def project(project: str): + if project_exists(project): + return stream_template( + "project.html", + project=get_project(project), + project_path=project, + runlist=project_runs(project), + ) + abort(404) + + +@_app.route("/<string:project>/<int:run>", methods=["GET"]) +def run(project: str, run: int): + if run_exists(project, run): + return stream_template( + "run.html", + project=get_project(project), + project_path=project, + run=stream_run(project, run), + run_number=run, + ) + abort(404) + + +def create_app(): + if not MSCI_HOME: + return _empty + else: + return _app diff --git a/web/favicon.ico b/web/favicon.ico Binary files differnew file mode 100644 index 0000000..a4c56d6 --- /dev/null +++ b/web/favicon.ico diff --git a/web/templates/base.html b/web/templates/base.html new file mode 100644 index 0000000..81d6a59 --- /dev/null +++ b/web/templates/base.html @@ -0,0 +1,74 @@ +<!doctype html> +<html lang="en"> + <head> + {% block head %} + <meta charset="utf-8" /> + <title>makeshiftci{% block title %}{% endblock %}</title> + <link + rel="shortcut icon" + href="/favicon.ico" + /> + {% endblock %} + <style + type="text/css" + media="all" + > + * { + margin: 0; + padding: 0; + } + html, + body { + height: 100%; + } + :root { + --primary: #de3163; + --secondary: #722f37; + --overlay: #f0ead6; + --blue: #0054b4; + --orange: #f94d00; + --green: #228b22; + --red: #ce2029; + --brown: #6f4e37; + @media (prefers-color-scheme: dark) { + --primary: #ffb7c5; + --secondary: #ddadaf; + --overlay: #534b4f; + --blue: #73c2fb; + --orange: #f28500; + --green: #85bb65; + --red: #e66771; + --brown: #c19a6b; + } + .heading { + top: 0; + left: 0; + position: sticky; + padding: 10px; + background-color: var(--overlay); + color: var(--primary); + } + .content-box { + display: flex; + flex-direction: column; + height: 100%; + } + a { + color: var(--primary); + } + color-scheme: light dark; + } + </style> + {% block extrastyle %}{% endblock %} + </head> + <body> + <div class="content-box"> + <div class="heading"> + <h1><a href="/">makeshiftci</a>{% block path %}{% endblock %}</h1> + {% block headingcontent %}{% endblock %} + </div> + {% block content %}{% endblock %} + </div> + </body> +</html> +<!-- vim: set filetype=jinja: --> diff --git a/web/templates/home.html b/web/templates/home.html new file mode 100644 index 0000000..20ef75a --- /dev/null +++ b/web/templates/home.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} {% block extrastyle %} +<style + type="text/css" + media="all" +> + .fullbody { + display: grid; + place-items: stretch; + grid-template-columns: repeat(2, 1fr); + padding: 20px; + gap: 20px; + .projectcard { + display: felx; + background-color: var(--overlay); + padding: 10px; + border-radius: 4px; + width: 90%; + .headlink a { + color: var(--secondary); + font-weight: bolder; + font-style: italic; + text-decoration: none; + } + .urllink a { + color: var(--blue); + text-decoration: underline; + font-style: italic; + } + .cronlink a { + color: var(--orange); + text-decoration: none; + font-weight: bolder; + } + } + } +</style> +{% endblock %} {% block content %} +<div class="fullbody"> + {% for project in projects %} + <div class="projectcard"> + <h2 class="headlink" + ><a href="/{{ project[0] }}">{{ project[1]['name'] }}</a></h2 + > + <hr style="margin: 10px 0" /> + <h4 class="urllink" + >url: + <a + href="{{ project[1]['url'] }}" + target="_blank" + >{{ project[1]['url'] }}</a + ></h4 + > + <h4 class="cronlink" + >cron: + <a + href="https://crontab.guru/#{{ project[1]['cron'] }}" + target="_blank" + >{{ project[1]['cron'] }}</a + ></h4 + > + </div> + {% endfor %} +</div> +{% endblock %} + +<!-- vim: set filetype=jinja: --> diff --git a/web/templates/project.html b/web/templates/project.html new file mode 100644 index 0000000..9b0aa58 --- /dev/null +++ b/web/templates/project.html @@ -0,0 +1,119 @@ +{% extends "base.html" %} {% block title %}/{{ project_path }}{% endblock %} {% +block path %}/<a href="/{{ project_path }}">{{ project_path }}</a>{% endblock %} +{% block extrastyle %} +<style + type="text/css" + media="all" +> + .heading-name { + color: var(--secondary) !important; + font-weight: bolder; + font-style: italic; + text-decoration: none; + } + .heading-info { + display: flex; + flex-direction: row; + place-content: space-between; + .heading-url { + color: var(--blue); + text-decoration: underline; + font-style: italic; + } + .heading-cron { + color: var(--orange); + text-decoration: none; + font-weight: bolder; + } + } + .fullbody { + display: grid; + place-items: stretch; + grid-template-columns: repeat(2, 1fr); + padding: 20px; + gap: 20px; + .runcard { + display: felx; + background-color: var(--overlay); + padding: 10px; + border-radius: 4px; + width: 90%; + .runheader { + display: flex; + flex-direction: row; + place-content: space-between; + .runnumber { + font-weight: bolder; + color: var(--secondary); + } + .rundate { + font-size: 12px; + color: var(--secondary); + } + } + .runstatus { + padding: 4px; + color: var(--overlay); + &.success::before { + content: "success"; + background-color: var(--green); + border-radius: 5px; + padding: 4px; + } + &.failure::before { + content: "failure"; + background-color: var(--red); + border-radius: 5px; + padding: 4px; + } + &.running::before { + content: "running"; + background-color: var(--brown); + border-radius: 5px; + padding: 4px; + } + } + } + } +</style> +{% endblock %} {% block headingcontent %} +<h2 class="heading-name">{{ project.name }}</h2> +<h4 class="heading-info" + ><a + href="{{ project.url }}" + target="_blank" + class="heading-url" + >{{ project.url }}</a + ><a + href="https://crontab.guru/#{{ project.cron }}" + target="_blank" + class="heading-cron" + >{{ project.cron }}</a + ></h4 +> +{% endblock %} {% block content %} + +<div class="fullbody"> + {% for run in runlist %} + <div class="runcard"> + <h3 class="runheader" + ><span + >run + <span class="runnumber" + ><a href="/{{ project_path }}/{{ run.number }}" + >#{{ run.number }}</a + ></span + ></span + ><span class="rundate">{{ run.date }}</span></h3 + > + <hr style="margin: 5px" /> + <h4 + class="runstatus {{ {true: 'success', false: 'failure', none: 'running'}[run.status] + }}" + ></h4> + </div> + {% endfor %} +</div> +{% endblock %} + +<!-- vim: set filetype=jinja: --> diff --git a/web/templates/run.html b/web/templates/run.html new file mode 100644 index 0000000..77f9f90 --- /dev/null +++ b/web/templates/run.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} {% block title %}/{{ project_path }}/{{ run_number +}}{% endblock %} {% block path %}/<a href="/{{ project_path }}" + >{{ project_path }}</a +>/<a href="/{{ project_path }}/{{ run_number }}">{{ run_number }}</a>{% endblock +%} {% block content %} +<pre + id="stdout" + style="white-space: pre-wrap" +></pre> + +{% for n in run %} +<script> + document.getElementById('stdout').textContent = {{ n|tojson }}; + var s = document.currentScript; + s.parentNode.removeChild(s); +</script> +{% endfor %} {% endblock %} + +<!-- vim: set filetype=jinja: --> |
