Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/dashboard_refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: PyAutoBrain (shared board presentation)
uses: actions/checkout@v4
with:
repository: ${{ github.repository_owner }}/PyAutoBrain
path: _brain

- name: Log the trigger
env:
PAYLOAD: ${{ toJson(github.event.client_payload) }}
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ bin/pyauto-insight census

The first real producer is autolens_inference. A second-producer fixture tests
interoperability; it is not a claim of a second adopter.

### Shared board presentation

Rendering the HTML board requires a current PyAutoBrain checkout beside this
repo, at `_brain/`, or selected with `PYAUTO_BRAIN`. The banner, logo, responsive
layout and section navigation come from its `board/_theme.py`; CI and dashboard
refresh workflows check it out automatically. Counts and section links remain
owned by this board.
250 changes: 233 additions & 17 deletions dashboard.html

Large diffs are not rendered by default.

24 changes: 19 additions & 5 deletions insight/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pathlib import Path

from insight import ORGAN_ROOT, campaigns, summary
from insight.theme import CSS, JS
from insight.theme import CSS, JS, theme

PAGES_URL = "https://pyautolabs.github.io/PyAutoInsight/"
REPO_URL = "https://github.com/PyAutoLabs/PyAutoInsight"
Expand Down Expand Up @@ -244,18 +244,32 @@ def _detail(s, now=None):

def render_html(views, now=None, campaign_data=None):
data = campaign_data if campaign_data is not None else campaigns.load()
shared = theme()
return (
'<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
"<title>PyAutoInsight dashboard</title><style>"
+ shared.css("insight")
+ CSS
+ "pre{white-space:pre-wrap;overflow-wrap:anywhere}details{margin:1rem 0}dd{margin-left:1rem}</style></head><body>"
'<header class="hero"><h1>PyAuto<span>Insight</span></h1><p>Question. Infer. Understand.</p></header><main>'
+ shared.hero(
"insight",
"Inference dashboard",
navigation=[
{"href": "#campaigns", "label": "Active campaigns"},
{"href": "#evidence", "label": "Inference evidence"},
*(
{
"href": "#" + v.instance.instance,
"label": v.instance.instance.title() + " project",
}
for v in views
),
],
)
+ "<main>"
+ input_marker(views, data)
+ campaigns.render_html(data)
+ "<p>Execution completion is separate from convergence and scientific acceptance. Cortex retains scientific conclusions. Evidence remains in project storage.</p>"
+ '<div class="stats">'
+ "".join(f"<div><b>{n}</b><span>{label}</span></div>" for label, n in counts(views))
+ "</div>"
+ "".join(_detail(s, now) for s in views)
+ "</main><script>"
+ JS
Expand Down
24 changes: 22 additions & 2 deletions insight/theme.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
"""Pulse control-room presentation, shared by copying the proven static design."""
"""Board presentation, using the shared PyAutoBrain banner and navigation."""

CSS = '\n:root{--bg:#ffffff;--fg:#1f2328;--muted:#59636e;--card:#f6f8fa;--line:#d1d9e0;\n--accent:#0b7a4b;--hero1:#062b1c;--hero2:#000000;--glow:#2ee88f;--ok:#1a7f37;--warn:#9a6700;\n--bad:#cf222e}\n@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#0d1117;--fg:#e6edf3;\n--muted:#9198a1;--card:#151b23;--line:#3d444d;--accent:#34d399;--ok:#3fb950;--warn:#d29922;\n--bad:#f85149}}\n:root[data-theme="dark"]{--bg:#0d1117;--fg:#e6edf3;--muted:#9198a1;--card:#151b23;\n--line:#3d444d;--accent:#34d399;--ok:#3fb950;--warn:#d29922;--bad:#f85149}\n*{box-sizing:border-box}\nbody{margin:0;background:var(--bg);color:var(--fg);\nfont:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif}\nmain{max-width:1200px;margin:0 auto;padding:0 16px 48px}\n.hero{background:radial-gradient(circle at 30% 20%,var(--hero1),var(--hero2));color:#fff;\npadding:28px 16px 22px;text-align:center}\n.hero h1{margin:0;font-size:28px;letter-spacing:.5px}.hero h1 span{color:var(--glow)}\n.hero p{margin:6px 0 0;color:#bfe9d3;font-size:13px;letter-spacing:2px;text-transform:uppercase}\nh2,h3{color:var(--accent)}h2{border-bottom:1px solid var(--line);padding-bottom:4px;margin-top:32px}\na{color:var(--accent)}\n.lede{color:var(--muted)}\n.tablewrap{overflow-x:auto}\ntable{border-collapse:collapse;width:100%;font-size:14px}\nth,td{border-bottom:1px solid var(--line);padding:6px 8px;text-align:left;vertical-align:top}\ntd.num{text-align:right;white-space:nowrap}\n.ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)}\n.notions{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px;margin:8px 0}\n.notions div{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 12px}\n.notions b{display:block;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}\n.stats{display:flex;flex-wrap:wrap;gap:12px;margin:16px 0}\n.stats div{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 14px}\n.stats b{display:block;font-size:20px;color:var(--accent)}.stats span{font-size:12px;color:var(--muted)}\nbutton[data-copy]{font:12px ui-monospace,SFMono-Regular,Menlo,monospace;text-align:left;\nbackground:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:6px;\npadding:3px 6px;cursor:pointer;word-break:break-all}\nbutton.copied{border-color:var(--ok)}\n.checkin textarea{display:block;width:100%;font:inherit;font-size:14px;line-height:1.5;background:var(--card);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:12px;margin:8px 0;resize:vertical}\n#copy-checkin{background:var(--accent);color:var(--bg);border:0;border-radius:6px;padding:10px 16px;cursor:pointer;font-weight:600}\n#copy-status{margin-left:12px}\ncode{color:var(--accent);word-break:break-all}\n'
import importlib.util
import os
from pathlib import Path

from insight import ORGAN_ROOT


def theme():
candidates = [Path(os.environ["PYAUTO_BRAIN"])] if os.environ.get("PYAUTO_BRAIN") else []
candidates += [ORGAN_ROOT.parent / "PyAutoBrain", ORGAN_ROOT / "_brain"]
for root in candidates:
path = root / "board/_theme.py"
if path.is_file():
spec = importlib.util.spec_from_file_location("insight_board_theme", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
raise RuntimeError("Shared theme unavailable: set PYAUTO_BRAIN to a PyAutoBrain checkout.")


CSS = "\nmain{padding-bottom:48px}\nh2,h3{color:var(--accent)}h2{border-bottom:1px solid var(--line);padding-bottom:4px;margin-top:32px}\na{color:var(--accent)}\n.lede{color:var(--muted)}\n.tablewrap{overflow-x:auto}\ntable{border-collapse:collapse;width:100%;font-size:14px}\nth,td{border-bottom:1px solid var(--line);padding:6px 8px;text-align:left;vertical-align:top}\ntd.num{text-align:right;white-space:nowrap}\n.ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)}\n.notions{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px;margin:8px 0}\n.notions div{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 12px}\n.notions b{display:block;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}\n.stats{display:flex;flex-wrap:wrap;gap:12px;margin:16px 0}\n.stats div{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 14px}\n.stats b{display:block;font-size:20px;color:var(--accent)}.stats span{font-size:12px;color:var(--muted)}\nbutton[data-copy]{font:12px ui-monospace,SFMono-Regular,Menlo,monospace;text-align:left;\nbackground:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:6px;\npadding:3px 6px;cursor:pointer;word-break:break-all}\nbutton.copied{border-color:var(--ok)}\n.checkin textarea{display:block;width:100%;font:inherit;font-size:14px;line-height:1.5;background:var(--card);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:12px;margin:8px 0;resize:vertical}\n#copy-checkin{background:var(--accent);color:var(--bg);border:0;border-radius:6px;padding:10px 16px;cursor:pointer;font-weight:600}\n#copy-status{margin-left:12px}\ncode{color:var(--accent);word-break:break-all}\n"
JS = "\nvar copyCheckin=document.getElementById('copy-checkin');\nif(copyCheckin){copyCheckin.addEventListener('click',async function(){\n var field=document.getElementById('checkin-prompt'), status=document.getElementById('copy-status');\n try {\n if(navigator.clipboard && window.isSecureContext){await navigator.clipboard.writeText(field.value)}\n else {field.focus();field.select();if(!document.execCommand('copy')){throw new Error('copy unavailable')}}\n status.textContent='Copied';\n } catch(error){field.focus();field.select();status.textContent='Select and copy the prompt above.'}\n});}\n\ndocument.querySelectorAll('button[data-copy]').forEach(function(b){\n b.addEventListener('click',function(){\n var t=b.getAttribute('data-copy');\n var done=function(){b.classList.add('copied');setTimeout(function(){b.classList.remove('copied')},1200)};\n if(navigator.clipboard){navigator.clipboard.writeText(t).then(done,function(){})}\n });\n});\ndocument.querySelectorAll('[data-age-from]').forEach(function(el){\n var t=Date.parse(el.getAttribute('data-age-from'));\n if(!isNaN(t)){el.textContent=' · '+Math.max(0,Math.floor((Date.now()-t)/864e5))+' d today';}\n});\n"
Loading