aboutsummaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authortoufic ar <contact@toufy.me>2026-05-15 02:24:12 +0300
committertoufic ar <contact@toufy.me>2026-05-15 02:24:12 +0300
commitfda7315f43b02cfae3050ece68a4d7cb26a826b4 (patch)
treedef28890a3c92cd7c2cb53d01f8e76c74fe362e7 /web
parentb36868f1fc3a6df0da4d931f94f3c39f1c50ee59 (diff)
downloadmakeshiftci-fda7315f43b02cfae3050ece68a4d7cb26a826b4.tar.gz
makeshiftci-fda7315f43b02cfae3050ece68a4d7cb26a826b4.zip
restructure webUI
Diffstat (limited to 'web')
-rw-r--r--web/__init__.py166
-rw-r--r--web/favicon.icobin0 -> 302430 bytes
-rw-r--r--web/templates/base.html74
-rw-r--r--web/templates/home.html66
-rw-r--r--web/templates/project.html119
-rw-r--r--web/templates/run.html19
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
new file mode 100644
index 0000000..a4c56d6
--- /dev/null
+++ b/web/favicon.ico
Binary files differ
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: -->