haxaml-ui 0.6.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- haxaml_ui-0.6.7/PKG-INFO +54 -0
- haxaml_ui-0.6.7/README.md +31 -0
- haxaml_ui-0.6.7/haxaml_ui/__init__.py +6 -0
- haxaml_ui-0.6.7/haxaml_ui/dashboard.py +517 -0
- haxaml_ui-0.6.7/haxaml_ui.egg-info/PKG-INFO +54 -0
- haxaml_ui-0.6.7/haxaml_ui.egg-info/SOURCES.txt +10 -0
- haxaml_ui-0.6.7/haxaml_ui.egg-info/dependency_links.txt +1 -0
- haxaml_ui-0.6.7/haxaml_ui.egg-info/entry_points.txt +2 -0
- haxaml_ui-0.6.7/haxaml_ui.egg-info/requires.txt +4 -0
- haxaml_ui-0.6.7/haxaml_ui.egg-info/top_level.txt +1 -0
- haxaml_ui-0.6.7/pyproject.toml +45 -0
- haxaml_ui-0.6.7/setup.cfg +4 -0
haxaml_ui-0.6.7/PKG-INFO
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: haxaml-ui
|
|
3
|
+
Version: 0.6.7
|
|
4
|
+
Summary: Local read-only dashboard package for Haxaml.
|
|
5
|
+
Author: Hax
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/haxsysgit/haxaml
|
|
8
|
+
Project-URL: Repository, https://github.com/haxsysgit/haxaml
|
|
9
|
+
Keywords: haxaml,dashboard,ui,developer-tools
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: haxaml<0.7.0,>=0.6.7
|
|
20
|
+
Requires-Dist: jinja2>=3.1
|
|
21
|
+
Requires-Dist: starlette>=0.37
|
|
22
|
+
Requires-Dist: uvicorn>=0.30
|
|
23
|
+
|
|
24
|
+
# haxaml-ui
|
|
25
|
+
|
|
26
|
+
Read-only local dashboard package for Haxaml.
|
|
27
|
+
|
|
28
|
+
This package is the dashboard distribution itself.
|
|
29
|
+
|
|
30
|
+
The human launcher remains:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
haxaml dashboard
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`haxaml[ui]` is only the convenience install selector for the core package.
|
|
37
|
+
|
|
38
|
+
Install directly:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install haxaml-ui
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install through the core extra:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install "haxaml[ui]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Direct package entrypoint is also available:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
haxaml-dashboard
|
|
54
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# haxaml-ui
|
|
2
|
+
|
|
3
|
+
Read-only local dashboard package for Haxaml.
|
|
4
|
+
|
|
5
|
+
This package is the dashboard distribution itself.
|
|
6
|
+
|
|
7
|
+
The human launcher remains:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
haxaml dashboard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`haxaml[ui]` is only the convenience install selector for the core package.
|
|
14
|
+
|
|
15
|
+
Install directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install haxaml-ui
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install through the core extra:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install "haxaml[ui]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Direct package entrypoint is also available:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
haxaml-dashboard
|
|
31
|
+
```
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""Read-only local dashboard for FRAME projects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from html import escape
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import webbrowser
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
import yaml
|
|
11
|
+
from jinja2 import DictLoader, Environment, select_autoescape
|
|
12
|
+
from starlette.applications import Starlette
|
|
13
|
+
from starlette.requests import Request
|
|
14
|
+
from starlette.responses import HTMLResponse, Response
|
|
15
|
+
from starlette.routing import Route
|
|
16
|
+
|
|
17
|
+
from haxaml.frame_model import FrameModel
|
|
18
|
+
from haxaml.map_policy import evaluate_map_complexity, format_map_complexity_summary, map_complexity_issues
|
|
19
|
+
from haxaml.mcp.lifecycle_helpers import _expect_sync_state
|
|
20
|
+
from haxaml.paths import detect_project_root, frame_path
|
|
21
|
+
from haxaml.reconcile import reconcile_derivation
|
|
22
|
+
from haxaml.runner import ExecutionRunner
|
|
23
|
+
from haxaml.runtime_cache import runtime_cache
|
|
24
|
+
from haxaml.state_manager import StateManager
|
|
25
|
+
from haxaml.validator import frame_consistency_report, semantic_validate
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DEFAULT_DASHBOARD_HOST = "127.0.0.1"
|
|
29
|
+
DEFAULT_DASHBOARD_PORT = 8421
|
|
30
|
+
FRAME_PAGE_ORDER = ["facts", "rules", "acts", "expect", "map"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TEMPLATES = {
|
|
34
|
+
"base.html": """
|
|
35
|
+
<!doctype html>
|
|
36
|
+
<html lang="en">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="utf-8">
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
40
|
+
<title>{{ title }}</title>
|
|
41
|
+
<link rel="stylesheet" href="/static/app.css">
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<header class="site-header">
|
|
45
|
+
<div>
|
|
46
|
+
<p class="eyebrow">Haxaml Dashboard</p>
|
|
47
|
+
<h1>{{ heading }}</h1>
|
|
48
|
+
<p class="meta">{{ project_dir }}</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="pill-row">
|
|
51
|
+
<span class="pill">{{ "Read-only" if read_only else "Mutable" }}</span>
|
|
52
|
+
{% if project_name %}<span class="pill accent">{{ project_name }}</span>{% endif %}
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
55
|
+
<nav class="nav">
|
|
56
|
+
<a href="/">Overview</a>
|
|
57
|
+
{% for item in frame_nav %}
|
|
58
|
+
<a href="/frame/{{ item }}">{{ item }}</a>
|
|
59
|
+
{% endfor %}
|
|
60
|
+
<a href="/archive">archive</a>
|
|
61
|
+
</nav>
|
|
62
|
+
<main class="page">
|
|
63
|
+
{{ body | safe }}
|
|
64
|
+
</main>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
67
|
+
""",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
CSS = """
|
|
72
|
+
:root {
|
|
73
|
+
--bg: #f4f0e8;
|
|
74
|
+
--panel: #fffaf2;
|
|
75
|
+
--ink: #1d1b19;
|
|
76
|
+
--muted: #6a645c;
|
|
77
|
+
--line: #d8cfc1;
|
|
78
|
+
--accent: #0d6b57;
|
|
79
|
+
--accent-soft: #d9f0ea;
|
|
80
|
+
--warn: #9b4d19;
|
|
81
|
+
--warn-soft: #f7e1cf;
|
|
82
|
+
--mono: "JetBrains Mono", "SFMono-Regular", monospace;
|
|
83
|
+
--sans: "IBM Plex Sans", "Segoe UI", sans-serif;
|
|
84
|
+
}
|
|
85
|
+
body {
|
|
86
|
+
margin: 0;
|
|
87
|
+
background:
|
|
88
|
+
radial-gradient(circle at top left, rgba(13, 107, 87, 0.12), transparent 34%),
|
|
89
|
+
linear-gradient(180deg, #efe7da 0%, var(--bg) 36%, #f8f5ef 100%);
|
|
90
|
+
color: var(--ink);
|
|
91
|
+
font-family: var(--sans);
|
|
92
|
+
}
|
|
93
|
+
.site-header, .nav, .page {
|
|
94
|
+
max-width: 1100px;
|
|
95
|
+
margin: 0 auto;
|
|
96
|
+
padding-left: 1rem;
|
|
97
|
+
padding-right: 1rem;
|
|
98
|
+
}
|
|
99
|
+
.site-header {
|
|
100
|
+
display: flex;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
gap: 1rem;
|
|
103
|
+
padding-top: 2rem;
|
|
104
|
+
}
|
|
105
|
+
.eyebrow {
|
|
106
|
+
margin: 0 0 .35rem;
|
|
107
|
+
letter-spacing: .08em;
|
|
108
|
+
text-transform: uppercase;
|
|
109
|
+
color: var(--muted);
|
|
110
|
+
font-size: .8rem;
|
|
111
|
+
}
|
|
112
|
+
h1, h2, h3 { margin: 0; }
|
|
113
|
+
.meta, .subtle { color: var(--muted); }
|
|
114
|
+
.pill-row {
|
|
115
|
+
display: flex;
|
|
116
|
+
gap: .5rem;
|
|
117
|
+
align-items: start;
|
|
118
|
+
flex-wrap: wrap;
|
|
119
|
+
}
|
|
120
|
+
.pill {
|
|
121
|
+
border: 1px solid var(--line);
|
|
122
|
+
background: var(--panel);
|
|
123
|
+
border-radius: 999px;
|
|
124
|
+
padding: .45rem .8rem;
|
|
125
|
+
font-size: .9rem;
|
|
126
|
+
}
|
|
127
|
+
.pill.accent {
|
|
128
|
+
border-color: var(--accent);
|
|
129
|
+
background: var(--accent-soft);
|
|
130
|
+
}
|
|
131
|
+
.nav {
|
|
132
|
+
display: flex;
|
|
133
|
+
gap: .6rem;
|
|
134
|
+
flex-wrap: wrap;
|
|
135
|
+
padding-top: 1rem;
|
|
136
|
+
padding-bottom: 1rem;
|
|
137
|
+
}
|
|
138
|
+
.nav a, .button-link {
|
|
139
|
+
color: var(--ink);
|
|
140
|
+
text-decoration: none;
|
|
141
|
+
border: 1px solid var(--line);
|
|
142
|
+
background: rgba(255, 255, 255, .68);
|
|
143
|
+
padding: .55rem .8rem;
|
|
144
|
+
border-radius: 999px;
|
|
145
|
+
}
|
|
146
|
+
.page {
|
|
147
|
+
padding-bottom: 2rem;
|
|
148
|
+
}
|
|
149
|
+
.grid {
|
|
150
|
+
display: grid;
|
|
151
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
152
|
+
gap: 1rem;
|
|
153
|
+
}
|
|
154
|
+
.card, .panel, details {
|
|
155
|
+
background: rgba(255, 250, 242, .88);
|
|
156
|
+
border: 1px solid var(--line);
|
|
157
|
+
border-radius: 18px;
|
|
158
|
+
padding: 1rem;
|
|
159
|
+
box-shadow: 0 10px 30px rgba(54, 43, 29, 0.04);
|
|
160
|
+
}
|
|
161
|
+
.card h3, .panel h2 { margin-bottom: .4rem; }
|
|
162
|
+
.plain-list {
|
|
163
|
+
margin: .6rem 0 0;
|
|
164
|
+
padding-left: 1rem;
|
|
165
|
+
}
|
|
166
|
+
.plain-list li {
|
|
167
|
+
margin: .3rem 0;
|
|
168
|
+
}
|
|
169
|
+
.warning {
|
|
170
|
+
border-color: var(--warn);
|
|
171
|
+
background: var(--warn-soft);
|
|
172
|
+
}
|
|
173
|
+
pre, code {
|
|
174
|
+
font-family: var(--mono);
|
|
175
|
+
}
|
|
176
|
+
pre {
|
|
177
|
+
overflow-x: auto;
|
|
178
|
+
white-space: pre-wrap;
|
|
179
|
+
background: #f7f3ec;
|
|
180
|
+
padding: 1rem;
|
|
181
|
+
border-radius: 12px;
|
|
182
|
+
border: 1px solid #e5dccf;
|
|
183
|
+
}
|
|
184
|
+
.stack {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
gap: 1rem;
|
|
188
|
+
}
|
|
189
|
+
.toolbar {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
gap: .6rem;
|
|
193
|
+
margin-bottom: 1rem;
|
|
194
|
+
}
|
|
195
|
+
.section-summary {
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
}
|
|
198
|
+
@media (max-width: 720px) {
|
|
199
|
+
.site-header {
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
_TEMPLATE_ENV = Environment(
|
|
207
|
+
loader=DictLoader(TEMPLATES),
|
|
208
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def resolve_dashboard_project_dir(project_dir: str = ".") -> Path:
|
|
213
|
+
resolved = detect_project_root(project_dir)
|
|
214
|
+
if resolved is None:
|
|
215
|
+
raise FileNotFoundError(f"No .haxaml directory found from {Path(project_dir).resolve()}")
|
|
216
|
+
return resolved
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def dashboard_url(host: str, port: int) -> str:
|
|
220
|
+
return f"http://{host}:{port}/"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def run_dashboard_server(
|
|
224
|
+
*,
|
|
225
|
+
project_dir: str,
|
|
226
|
+
host: str = DEFAULT_DASHBOARD_HOST,
|
|
227
|
+
port: int = DEFAULT_DASHBOARD_PORT,
|
|
228
|
+
open_browser: bool = True,
|
|
229
|
+
read_only: bool = True,
|
|
230
|
+
) -> str:
|
|
231
|
+
app = create_dashboard_app(project_dir=project_dir, read_only=read_only)
|
|
232
|
+
url = dashboard_url(host, port)
|
|
233
|
+
if open_browser:
|
|
234
|
+
webbrowser.open(url)
|
|
235
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
236
|
+
return url
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main() -> None:
|
|
240
|
+
run_dashboard_server(project_dir=".")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def create_dashboard_app(*, project_dir: str, read_only: bool = True) -> Starlette:
|
|
244
|
+
root = resolve_dashboard_project_dir(project_dir)
|
|
245
|
+
app = Starlette(
|
|
246
|
+
debug=False,
|
|
247
|
+
routes=[
|
|
248
|
+
Route("/", _overview_page),
|
|
249
|
+
Route("/frame/{frame_name}", _frame_page),
|
|
250
|
+
Route("/archive", _archive_page),
|
|
251
|
+
Route("/archive/{kind}/{record_id}", _archive_detail_page),
|
|
252
|
+
Route("/static/app.css", _css_asset),
|
|
253
|
+
],
|
|
254
|
+
)
|
|
255
|
+
app.state.project_dir = str(root)
|
|
256
|
+
app.state.read_only = bool(read_only)
|
|
257
|
+
return app
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _render_page(
|
|
261
|
+
request: Request,
|
|
262
|
+
*,
|
|
263
|
+
title: str,
|
|
264
|
+
heading: str,
|
|
265
|
+
body: str,
|
|
266
|
+
project_name: str = "",
|
|
267
|
+
) -> HTMLResponse:
|
|
268
|
+
template = _TEMPLATE_ENV.get_template("base.html")
|
|
269
|
+
return HTMLResponse(
|
|
270
|
+
template.render(
|
|
271
|
+
title=title,
|
|
272
|
+
heading=heading,
|
|
273
|
+
body=body,
|
|
274
|
+
project_dir=request.app.state.project_dir,
|
|
275
|
+
project_name=project_name,
|
|
276
|
+
read_only=request.app.state.read_only,
|
|
277
|
+
frame_nav=FRAME_PAGE_ORDER,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def _css_asset(_: Request) -> Response:
|
|
283
|
+
return Response(CSS, media_type="text/css")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _render_card(title: str, value: str, detail: str = "", *, warning: bool = False) -> str:
|
|
287
|
+
detail_html = f"<p class='subtle'>{escape(detail)}</p>" if detail else ""
|
|
288
|
+
klass = "card warning" if warning else "card"
|
|
289
|
+
return f"<section class='{klass}'><h3>{escape(title)}</h3><p>{escape(value)}</p>{detail_html}</section>"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _overview_body(project_dir: str) -> tuple[str, str]:
|
|
293
|
+
frame = FrameModel.load(project_dir)
|
|
294
|
+
project_name = str(((frame.facts or {}).get("identity") or {}).get("name", "")).strip()
|
|
295
|
+
runner = ExecutionRunner(project_dir)
|
|
296
|
+
health = runner.get_project_health()
|
|
297
|
+
stats = StateManager(str(frame_path(project_dir, "acts.yaml"))).get_stats() if frame.has_acts() else {}
|
|
298
|
+
archive_index = runtime_cache().get_archive_index(project_dir)
|
|
299
|
+
reconcile = reconcile_derivation(project_dir)
|
|
300
|
+
semantic = semantic_validate(frame)
|
|
301
|
+
consistency = frame_consistency_report(frame)
|
|
302
|
+
map_assessment = evaluate_map_complexity(project_dir)
|
|
303
|
+
map_errors, map_warnings = map_complexity_issues(map_assessment)
|
|
304
|
+
sync_state = _expect_sync_state(frame.acts or {})
|
|
305
|
+
cards = [
|
|
306
|
+
_render_card("Ready", "yes" if health.get("ready") else "no", "Validation and lifecycle readiness", warning=not health.get("ready")),
|
|
307
|
+
_render_card("Context Tokens", str(health.get("context_tokens", 0)), "Current full-context size"),
|
|
308
|
+
_render_card("Archive", f"{len(archive_index.index)} indexed records", "Shallow index loaded from archive"),
|
|
309
|
+
_render_card("Map Policy", format_map_complexity_summary(map_assessment), "Complexity and map expectation", warning=bool(map_errors)),
|
|
310
|
+
_render_card("Runs", str(stats.get("total_runs", 0)), "Hot plus archived"),
|
|
311
|
+
_render_card("Active Task", str(stats.get("active_task", "none")), "Current human-facing state"),
|
|
312
|
+
]
|
|
313
|
+
warnings: list[str] = []
|
|
314
|
+
warnings.extend(str(item) for item in health.get("errors", []))
|
|
315
|
+
warnings.extend(str(item) for item in semantic.blocking)
|
|
316
|
+
warnings.extend(str(item) for item in semantic.warnings)
|
|
317
|
+
warnings.extend(str(item.get("message", "")) for item in consistency.get("findings", []))
|
|
318
|
+
warnings.extend(str(item) for item in map_warnings)
|
|
319
|
+
if sync_state.get("required"):
|
|
320
|
+
warnings.append(
|
|
321
|
+
f"expect.yaml sync pending for run {sync_state.get('pending_run_id') or 'unknown'}."
|
|
322
|
+
)
|
|
323
|
+
warnings = [item for item in warnings if item]
|
|
324
|
+
recent_decisions = (frame.acts or {}).get("decisions", []) if isinstance(frame.acts, dict) else []
|
|
325
|
+
recent_runs = archive_index.index[-5:] if archive_index.index else []
|
|
326
|
+
body = [
|
|
327
|
+
"<section class='grid'>",
|
|
328
|
+
*cards,
|
|
329
|
+
"</section>",
|
|
330
|
+
"<section class='panel stack'>",
|
|
331
|
+
"<div><h2>Signals</h2><p class='subtle'>Overview-first and read-only. Use drilldown pages for full YAML.</p></div>",
|
|
332
|
+
"<ul class='plain-list'>",
|
|
333
|
+
f"<li>Project: {escape(project_name or '(unnamed)')}</li>",
|
|
334
|
+
f"<li>Phase: {escape(str(stats.get('current_phase', 'unknown')))}</li>",
|
|
335
|
+
f"<li>Archive mode: {escape(str(stats.get('archive_mode', 'manual')))}</li>",
|
|
336
|
+
f"<li>Reconcile: {escape(str(reconcile.get('human_summary', 'No reconcile summary')))}</li>",
|
|
337
|
+
"</ul>",
|
|
338
|
+
"</section>",
|
|
339
|
+
]
|
|
340
|
+
if warnings:
|
|
341
|
+
body.extend(
|
|
342
|
+
[
|
|
343
|
+
"<section class='panel warning'>",
|
|
344
|
+
"<h2>Lifecycle And Drift Warnings</h2>",
|
|
345
|
+
"<ul class='plain-list'>",
|
|
346
|
+
*[f"<li>{escape(item)}</li>" for item in warnings[:12]],
|
|
347
|
+
"</ul>",
|
|
348
|
+
"</section>",
|
|
349
|
+
]
|
|
350
|
+
)
|
|
351
|
+
body.extend(
|
|
352
|
+
[
|
|
353
|
+
"<section class='grid'>",
|
|
354
|
+
"<div class='panel'><h2>Recent Decisions</h2><ul class='plain-list'>",
|
|
355
|
+
*[
|
|
356
|
+
f"<li>{escape(str(item.get('decision', '')))}"
|
|
357
|
+
f"{' — ' + escape(str(item.get('reasoning', ''))) if item.get('reasoning') else ''}</li>"
|
|
358
|
+
for item in recent_decisions[-5:]
|
|
359
|
+
if isinstance(item, dict)
|
|
360
|
+
],
|
|
361
|
+
"</ul></div>",
|
|
362
|
+
"<div class='panel'><h2>Archive Summary</h2><ul class='plain-list'>",
|
|
363
|
+
*[
|
|
364
|
+
f"<li><a href='/archive/{escape(str(item.get('kind', '')))}"
|
|
365
|
+
f"/{escape(str(item.get('id', '')))}'>{escape(str(item.get('id', '')))}</a> "
|
|
366
|
+
f"{escape(str(item.get('summary', '')))}</li>"
|
|
367
|
+
for item in recent_runs
|
|
368
|
+
],
|
|
369
|
+
"</ul></div>",
|
|
370
|
+
"</section>",
|
|
371
|
+
]
|
|
372
|
+
)
|
|
373
|
+
return "".join(body), project_name
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def _overview_page(request: Request) -> HTMLResponse:
|
|
377
|
+
body, project_name = _overview_body(request.app.state.project_dir)
|
|
378
|
+
return _render_page(
|
|
379
|
+
request,
|
|
380
|
+
title="Haxaml Dashboard",
|
|
381
|
+
heading="Overview",
|
|
382
|
+
body=body,
|
|
383
|
+
project_name=project_name,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _frame_body(project_dir: str, frame_name: str, q: str = "", view: str = "human") -> tuple[str, str]:
|
|
388
|
+
frame = FrameModel.load(project_dir)
|
|
389
|
+
data = frame.frame_file(frame_name)
|
|
390
|
+
path = frame_path(project_dir, f"{frame_name}.yaml")
|
|
391
|
+
project_name = str((((frame.facts or {}).get("identity")) or {}).get("name", "")).strip()
|
|
392
|
+
filter_text = q.strip().lower()
|
|
393
|
+
if view == "raw":
|
|
394
|
+
raw = yaml.dump(data or {}, default_flow_style=False, sort_keys=False)
|
|
395
|
+
return (
|
|
396
|
+
"<section class='toolbar'>"
|
|
397
|
+
f"<a class='button-link' href='/frame/{frame_name}?view=human'>Human view</a>"
|
|
398
|
+
f"<span class='pill'>{escape(str(path))}</span></section>"
|
|
399
|
+
f"<pre>{escape(raw)}</pre>",
|
|
400
|
+
project_name,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
blocks: list[str] = [
|
|
404
|
+
"<section class='toolbar'>"
|
|
405
|
+
f"<a class='button-link' href='/frame/{frame_name}?view=raw'>Raw YAML</a>"
|
|
406
|
+
f"<span class='pill'>{escape(str(path))}</span>"
|
|
407
|
+
"</section>"
|
|
408
|
+
]
|
|
409
|
+
if not isinstance(data, dict):
|
|
410
|
+
blocks.append(f"<section class='panel warning'><h2>{escape(frame_name)}</h2><p>File is missing.</p></section>")
|
|
411
|
+
return "".join(blocks), project_name
|
|
412
|
+
|
|
413
|
+
for key, value in data.items():
|
|
414
|
+
preview = yaml.dump(value, default_flow_style=False, sort_keys=False).strip()
|
|
415
|
+
haystack = f"{key}\n{preview}".lower()
|
|
416
|
+
if filter_text and filter_text not in haystack:
|
|
417
|
+
continue
|
|
418
|
+
blocks.append(
|
|
419
|
+
"<details open>"
|
|
420
|
+
f"<summary class='section-summary'>{escape(str(key))}</summary>"
|
|
421
|
+
f"<pre>{escape(preview)}</pre>"
|
|
422
|
+
"</details>"
|
|
423
|
+
)
|
|
424
|
+
if len(blocks) == 1:
|
|
425
|
+
blocks.append("<section class='panel'><p>No sections matched this filter.</p></section>")
|
|
426
|
+
return "".join(blocks), project_name
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
async def _frame_page(request: Request) -> HTMLResponse:
|
|
430
|
+
frame_name = request.path_params["frame_name"]
|
|
431
|
+
if frame_name not in FRAME_PAGE_ORDER:
|
|
432
|
+
return HTMLResponse("Not found", status_code=404)
|
|
433
|
+
body, project_name = _frame_body(
|
|
434
|
+
request.app.state.project_dir,
|
|
435
|
+
frame_name,
|
|
436
|
+
q=str(request.query_params.get("q", "")),
|
|
437
|
+
view=str(request.query_params.get("view", "human")),
|
|
438
|
+
)
|
|
439
|
+
return _render_page(
|
|
440
|
+
request,
|
|
441
|
+
title=f"FRAME: {frame_name}",
|
|
442
|
+
heading=f"{frame_name}.yaml",
|
|
443
|
+
body=body,
|
|
444
|
+
project_name=project_name,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _archive_body(project_dir: str, q: str = "") -> tuple[str, str]:
|
|
449
|
+
frame = FrameModel.load(project_dir)
|
|
450
|
+
project_name = str((((frame.facts or {}).get("identity")) or {}).get("name", "")).strip()
|
|
451
|
+
archive_index = runtime_cache().get_archive_index(project_dir)
|
|
452
|
+
if not archive_index.exists:
|
|
453
|
+
return "<section class='panel'><h2>Archive</h2><p>No archive file found.</p></section>", project_name
|
|
454
|
+
filter_text = q.strip().lower()
|
|
455
|
+
items = archive_index.index
|
|
456
|
+
if filter_text:
|
|
457
|
+
items = [
|
|
458
|
+
item for item in items
|
|
459
|
+
if filter_text in yaml.dump(item, default_flow_style=False, sort_keys=False).lower()
|
|
460
|
+
]
|
|
461
|
+
counts = archive_index.metadata.get("counts", {})
|
|
462
|
+
body = [
|
|
463
|
+
"<section class='panel'>",
|
|
464
|
+
"<h2>Archive Overview</h2>",
|
|
465
|
+
"<ul class='plain-list'>",
|
|
466
|
+
f"<li>Runs: {int(counts.get('runs', 0) or 0)}</li>",
|
|
467
|
+
f"<li>Sessions: {int(counts.get('sessions', 0) or 0)}</li>",
|
|
468
|
+
f"<li>Verifications: {int(counts.get('verifications', 0) or 0)}</li>",
|
|
469
|
+
"</ul>",
|
|
470
|
+
"</section>",
|
|
471
|
+
"<section class='panel'><h2>Indexed Records</h2><ul class='plain-list'>",
|
|
472
|
+
]
|
|
473
|
+
for item in reversed(items[-25:]):
|
|
474
|
+
kind = str(item.get("kind", "")).strip()
|
|
475
|
+
record_id = str(item.get("id", "")).strip()
|
|
476
|
+
body.append(
|
|
477
|
+
f"<li><a href='/archive/{escape(kind)}/{escape(record_id)}'>{escape(record_id or kind)}</a> "
|
|
478
|
+
f"{escape(str(item.get('summary', '')))}</li>"
|
|
479
|
+
)
|
|
480
|
+
body.append("</ul></section>")
|
|
481
|
+
return "".join(body), project_name
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
async def _archive_page(request: Request) -> HTMLResponse:
|
|
485
|
+
body, project_name = _archive_body(
|
|
486
|
+
request.app.state.project_dir,
|
|
487
|
+
q=str(request.query_params.get("q", "")),
|
|
488
|
+
)
|
|
489
|
+
return _render_page(
|
|
490
|
+
request,
|
|
491
|
+
title="Archive",
|
|
492
|
+
heading="Archive",
|
|
493
|
+
body=body,
|
|
494
|
+
project_name=project_name,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
async def _archive_detail_page(request: Request) -> HTMLResponse:
|
|
499
|
+
kind = str(request.path_params["kind"])
|
|
500
|
+
record_id = str(request.path_params["record_id"])
|
|
501
|
+
record = runtime_cache().load_archive_record_details(request.app.state.project_dir, kind, record_id)
|
|
502
|
+
if record is None:
|
|
503
|
+
return HTMLResponse("Not found", status_code=404)
|
|
504
|
+
frame = FrameModel.load(request.app.state.project_dir)
|
|
505
|
+
project_name = str((((frame.facts or {}).get("identity")) or {}).get("name", "")).strip()
|
|
506
|
+
body = (
|
|
507
|
+
f"<section class='toolbar'><a class='button-link' href='/archive'>Back to archive</a></section>"
|
|
508
|
+
f"<section class='panel'><h2>{escape(kind)}:{escape(record_id)}</h2>"
|
|
509
|
+
f"<pre>{escape(yaml.dump(record, default_flow_style=False, sort_keys=False))}</pre></section>"
|
|
510
|
+
)
|
|
511
|
+
return _render_page(
|
|
512
|
+
request,
|
|
513
|
+
title=f"Archive {kind}:{record_id}",
|
|
514
|
+
heading=f"Archive {kind}:{record_id}",
|
|
515
|
+
body=body,
|
|
516
|
+
project_name=project_name,
|
|
517
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: haxaml-ui
|
|
3
|
+
Version: 0.6.7
|
|
4
|
+
Summary: Local read-only dashboard package for Haxaml.
|
|
5
|
+
Author: Hax
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/haxsysgit/haxaml
|
|
8
|
+
Project-URL: Repository, https://github.com/haxsysgit/haxaml
|
|
9
|
+
Keywords: haxaml,dashboard,ui,developer-tools
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: haxaml<0.7.0,>=0.6.7
|
|
20
|
+
Requires-Dist: jinja2>=3.1
|
|
21
|
+
Requires-Dist: starlette>=0.37
|
|
22
|
+
Requires-Dist: uvicorn>=0.30
|
|
23
|
+
|
|
24
|
+
# haxaml-ui
|
|
25
|
+
|
|
26
|
+
Read-only local dashboard package for Haxaml.
|
|
27
|
+
|
|
28
|
+
This package is the dashboard distribution itself.
|
|
29
|
+
|
|
30
|
+
The human launcher remains:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
haxaml dashboard
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`haxaml[ui]` is only the convenience install selector for the core package.
|
|
37
|
+
|
|
38
|
+
Install directly:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install haxaml-ui
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install through the core extra:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install "haxaml[ui]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Direct package entrypoint is also available:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
haxaml-dashboard
|
|
54
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
haxaml_ui/__init__.py
|
|
4
|
+
haxaml_ui/dashboard.py
|
|
5
|
+
haxaml_ui.egg-info/PKG-INFO
|
|
6
|
+
haxaml_ui.egg-info/SOURCES.txt
|
|
7
|
+
haxaml_ui.egg-info/dependency_links.txt
|
|
8
|
+
haxaml_ui.egg-info/entry_points.txt
|
|
9
|
+
haxaml_ui.egg-info/requires.txt
|
|
10
|
+
haxaml_ui.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
haxaml_ui
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "haxaml-ui"
|
|
7
|
+
version = "0.6.7"
|
|
8
|
+
description = "Local read-only dashboard package for Haxaml."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Hax" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"haxaml",
|
|
17
|
+
"dashboard",
|
|
18
|
+
"ui",
|
|
19
|
+
"developer-tools"
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Environment :: Web Environment",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Software Development :: Build Tools"
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"haxaml>=0.6.7,<0.7.0",
|
|
32
|
+
"jinja2>=3.1",
|
|
33
|
+
"starlette>=0.37",
|
|
34
|
+
"uvicorn>=0.30"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
haxaml-dashboard = "haxaml_ui.dashboard:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/haxsysgit/haxaml"
|
|
42
|
+
Repository = "https://github.com/haxsysgit/haxaml"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
include = ["haxaml_ui*"]
|