scitex 2.16.2__py3-none-any.whl → 2.17.3__py3-none-any.whl
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.
- scitex/_dev/__init__.py +122 -0
- scitex/_dev/_config.py +391 -0
- scitex/_dev/_dashboard/__init__.py +11 -0
- scitex/_dev/_dashboard/_app.py +89 -0
- scitex/_dev/_dashboard/_routes.py +169 -0
- scitex/_dev/_dashboard/_scripts.py +301 -0
- scitex/_dev/_dashboard/_styles.py +205 -0
- scitex/_dev/_dashboard/_templates.py +117 -0
- scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
- scitex/_dev/_ecosystem.py +109 -0
- scitex/_dev/_github.py +360 -0
- scitex/_dev/_mcp/__init__.py +11 -0
- scitex/_dev/_mcp/handlers.py +182 -0
- scitex/_dev/_ssh.py +332 -0
- scitex/_dev/_versions.py +272 -0
- scitex/_mcp_resources/_cheatsheet.py +1 -1
- scitex/_mcp_resources/_modules.py +1 -1
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/dev.py +186 -0
- scitex/_mcp_tools/verify.py +256 -0
- scitex/audio/_audio_check.py +84 -41
- scitex/cli/capture.py +45 -22
- scitex/cli/dev.py +494 -0
- scitex/cli/main.py +4 -0
- scitex/cli/stats.py +48 -20
- scitex/cli/verify.py +473 -0
- scitex/dev/plt/__init__.py +1 -1
- scitex/dev/plt/mpl/get_dir_ax.py +1 -1
- scitex/dev/plt/mpl/get_signatures.py +1 -1
- scitex/dev/plt/mpl/get_signatures_details.py +1 -1
- scitex/io/_load.py +8 -1
- scitex/io/_save.py +12 -0
- scitex/plt/__init__.py +16 -6
- scitex/session/README.md +2 -2
- scitex/session/__init__.py +1 -0
- scitex/session/_decorator.py +57 -33
- scitex/session/_lifecycle/__init__.py +23 -0
- scitex/session/_lifecycle/_close.py +225 -0
- scitex/session/_lifecycle/_config.py +112 -0
- scitex/session/_lifecycle/_matplotlib.py +83 -0
- scitex/session/_lifecycle/_start.py +246 -0
- scitex/session/_lifecycle/_utils.py +186 -0
- scitex/session/_manager.py +40 -3
- scitex/session/template.py +1 -1
- scitex/template/__init__.py +18 -1
- scitex/template/_templates/plt.py +1 -1
- scitex/template/_templates/session.py +1 -1
- scitex/template/clone_research_minimal.py +111 -0
- scitex/verify/README.md +300 -0
- scitex/verify/__init__.py +208 -0
- scitex/verify/_chain.py +369 -0
- scitex/verify/_db.py +600 -0
- scitex/verify/_hash.py +187 -0
- scitex/verify/_integration.py +127 -0
- scitex/verify/_rerun.py +253 -0
- scitex/verify/_tracker.py +330 -0
- scitex/verify/_visualize.py +44 -0
- scitex/verify/_viz/__init__.py +38 -0
- scitex/verify/_viz/_colors.py +84 -0
- scitex/verify/_viz/_format.py +302 -0
- scitex/verify/_viz/_json.py +192 -0
- scitex/verify/_viz/_mermaid.py +440 -0
- scitex/verify/_viz/_templates.py +246 -0
- scitex/verify/_viz/_utils.py +56 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/METADATA +2 -1
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/RECORD +69 -28
- scitex/session/_lifecycle.py +0 -827
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/WHEEL +0 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-02-02
|
|
3
|
+
# File: scitex/_dev/_dashboard/_routes.py
|
|
4
|
+
|
|
5
|
+
"""Flask routes for the dashboard."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from flask import Flask
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_routes(app: Flask) -> None:
|
|
16
|
+
"""Register dashboard routes with Flask app."""
|
|
17
|
+
from flask import jsonify, request
|
|
18
|
+
|
|
19
|
+
from ._templates import get_dashboard_html, get_error_html
|
|
20
|
+
|
|
21
|
+
@app.route("/")
|
|
22
|
+
def index():
|
|
23
|
+
"""Serve the main dashboard page."""
|
|
24
|
+
try:
|
|
25
|
+
return get_dashboard_html()
|
|
26
|
+
except Exception as e:
|
|
27
|
+
return get_error_html(str(e)), 500
|
|
28
|
+
|
|
29
|
+
@app.route("/api/versions")
|
|
30
|
+
def api_versions():
|
|
31
|
+
"""Get version data as JSON."""
|
|
32
|
+
try:
|
|
33
|
+
data = _get_all_version_data()
|
|
34
|
+
return jsonify(data)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return jsonify({"error": str(e)}), 500
|
|
37
|
+
|
|
38
|
+
@app.route("/api/packages")
|
|
39
|
+
def api_packages():
|
|
40
|
+
"""Get local package versions only (fast)."""
|
|
41
|
+
try:
|
|
42
|
+
from .._versions import list_versions
|
|
43
|
+
|
|
44
|
+
return jsonify(list_versions())
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return jsonify({"error": str(e)}), 500
|
|
47
|
+
|
|
48
|
+
@app.route("/api/config")
|
|
49
|
+
def api_config():
|
|
50
|
+
"""Get current configuration."""
|
|
51
|
+
try:
|
|
52
|
+
from .._config import get_config_path, load_config
|
|
53
|
+
|
|
54
|
+
config = load_config()
|
|
55
|
+
return jsonify(
|
|
56
|
+
{
|
|
57
|
+
"config_path": str(get_config_path()),
|
|
58
|
+
"packages": [
|
|
59
|
+
{
|
|
60
|
+
"name": p.name,
|
|
61
|
+
"local_path": p.local_path,
|
|
62
|
+
"pypi_name": p.pypi_name,
|
|
63
|
+
}
|
|
64
|
+
for p in config.packages
|
|
65
|
+
],
|
|
66
|
+
"hosts": [
|
|
67
|
+
{
|
|
68
|
+
"name": h.name,
|
|
69
|
+
"hostname": h.hostname,
|
|
70
|
+
"role": h.role,
|
|
71
|
+
"enabled": h.enabled,
|
|
72
|
+
}
|
|
73
|
+
for h in config.hosts
|
|
74
|
+
],
|
|
75
|
+
"github_remotes": [
|
|
76
|
+
{"name": r.name, "org": r.org, "enabled": r.enabled}
|
|
77
|
+
for r in config.github_remotes
|
|
78
|
+
],
|
|
79
|
+
"branches": config.branches,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return jsonify({"error": str(e)}), 500
|
|
84
|
+
|
|
85
|
+
@app.route("/api/refresh", methods=["POST"])
|
|
86
|
+
def api_refresh():
|
|
87
|
+
"""Trigger a data refresh."""
|
|
88
|
+
try:
|
|
89
|
+
data = _get_all_version_data(force_refresh=True)
|
|
90
|
+
return jsonify({"status": "ok", "data": data})
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return jsonify({"error": str(e)}), 500
|
|
93
|
+
|
|
94
|
+
@app.route("/api/hosts")
|
|
95
|
+
def api_hosts():
|
|
96
|
+
"""Get host version data."""
|
|
97
|
+
try:
|
|
98
|
+
packages = request.args.getlist("package") or None
|
|
99
|
+
hosts = request.args.getlist("host") or None
|
|
100
|
+
from .._ssh import check_all_hosts
|
|
101
|
+
|
|
102
|
+
data = check_all_hosts(packages=packages, hosts=hosts)
|
|
103
|
+
return jsonify(data)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return jsonify({"error": str(e)}), 500
|
|
106
|
+
|
|
107
|
+
@app.route("/api/remotes")
|
|
108
|
+
def api_remotes():
|
|
109
|
+
"""Get GitHub remote version data."""
|
|
110
|
+
try:
|
|
111
|
+
packages = request.args.getlist("package") or None
|
|
112
|
+
remotes = request.args.getlist("remote") or None
|
|
113
|
+
from .._github import check_all_remotes
|
|
114
|
+
|
|
115
|
+
data = check_all_remotes(packages=packages, remotes=remotes)
|
|
116
|
+
return jsonify(data)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return jsonify({"error": str(e)}), 500
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_all_version_data(force_refresh: bool = False) -> dict[str, Any]:
|
|
122
|
+
"""Get all version data from all sources.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
force_refresh : bool
|
|
127
|
+
If True, bypass any caching.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
dict
|
|
132
|
+
Combined version data.
|
|
133
|
+
"""
|
|
134
|
+
from .._config import get_enabled_hosts, get_enabled_remotes, load_config
|
|
135
|
+
from .._github import check_all_remotes
|
|
136
|
+
from .._ssh import check_all_hosts
|
|
137
|
+
from .._versions import list_versions
|
|
138
|
+
|
|
139
|
+
config = load_config()
|
|
140
|
+
|
|
141
|
+
# Get local versions
|
|
142
|
+
packages_data = list_versions()
|
|
143
|
+
|
|
144
|
+
# Get host versions (if any hosts configured)
|
|
145
|
+
hosts_data = {}
|
|
146
|
+
enabled_hosts = get_enabled_hosts(config)
|
|
147
|
+
if enabled_hosts:
|
|
148
|
+
try:
|
|
149
|
+
hosts_data = check_all_hosts(config=config)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# Get remote versions (if any remotes configured)
|
|
154
|
+
remotes_data = {}
|
|
155
|
+
enabled_remotes = get_enabled_remotes(config)
|
|
156
|
+
if enabled_remotes:
|
|
157
|
+
try:
|
|
158
|
+
remotes_data = check_all_remotes(config=config)
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"packages": packages_data,
|
|
164
|
+
"hosts": hosts_data,
|
|
165
|
+
"remotes": remotes_data,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# EOF
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-02-02
|
|
3
|
+
# File: scitex/_dev/_dashboard/_scripts.py
|
|
4
|
+
|
|
5
|
+
"""JavaScript for the dashboard."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_javascript() -> str:
|
|
9
|
+
"""Return dashboard JavaScript."""
|
|
10
|
+
return """
|
|
11
|
+
let cachedData = { packages: {}, hosts: {}, remotes: {} };
|
|
12
|
+
|
|
13
|
+
async function fetchVersions() {
|
|
14
|
+
showLoading(true);
|
|
15
|
+
cachedData = { packages: {}, hosts: {}, remotes: {} };
|
|
16
|
+
renderFilters();
|
|
17
|
+
renderData();
|
|
18
|
+
|
|
19
|
+
// Load packages first (fast)
|
|
20
|
+
fetchPackages();
|
|
21
|
+
// Load hosts and remotes in parallel (slower)
|
|
22
|
+
fetchHosts();
|
|
23
|
+
fetchRemotes();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fetchPackages() {
|
|
27
|
+
setSectionLoading('package', true);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch('/api/packages');
|
|
30
|
+
cachedData.packages = await response.json();
|
|
31
|
+
renderFilters();
|
|
32
|
+
renderData();
|
|
33
|
+
updateTimestamp();
|
|
34
|
+
setSectionUpdated('package');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('Failed to fetch packages:', err);
|
|
37
|
+
} finally {
|
|
38
|
+
showLoading(false);
|
|
39
|
+
setSectionLoading('package', false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchHosts() {
|
|
44
|
+
setSectionLoading('host', true);
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch('/api/hosts');
|
|
47
|
+
cachedData.hosts = await response.json();
|
|
48
|
+
renderFilters();
|
|
49
|
+
renderData();
|
|
50
|
+
setSectionUpdated('host');
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error('Failed to fetch hosts:', err);
|
|
53
|
+
cachedData.hosts = { error: err.message };
|
|
54
|
+
} finally {
|
|
55
|
+
setSectionLoading('host', false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchRemotes() {
|
|
60
|
+
setSectionLoading('remote', true);
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch('/api/remotes');
|
|
63
|
+
cachedData.remotes = await response.json();
|
|
64
|
+
renderFilters();
|
|
65
|
+
renderData();
|
|
66
|
+
setSectionUpdated('remote');
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error('Failed to fetch remotes:', err);
|
|
69
|
+
cachedData.remotes = { error: err.message };
|
|
70
|
+
} finally {
|
|
71
|
+
setSectionLoading('remote', false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setSectionLoading(section, loading) {
|
|
76
|
+
const el = document.getElementById(section + 'Filters');
|
|
77
|
+
if (el) {
|
|
78
|
+
if (loading) {
|
|
79
|
+
el.classList.add('loading-section');
|
|
80
|
+
} else {
|
|
81
|
+
el.classList.remove('loading-section');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setSectionUpdated(section) {
|
|
87
|
+
const el = document.getElementById(section + 'Filters');
|
|
88
|
+
if (el) {
|
|
89
|
+
el.classList.add('just-updated');
|
|
90
|
+
setTimeout(() => el.classList.remove('just-updated'), 1000);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateTimestamp() {
|
|
95
|
+
document.getElementById('lastUpdated').textContent =
|
|
96
|
+
'Last updated: ' + new Date().toLocaleTimeString();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Auto-refresh settings
|
|
100
|
+
let autoRefreshInterval = null;
|
|
101
|
+
let autoRefreshSeconds = 0;
|
|
102
|
+
|
|
103
|
+
function toggleAutoRefresh(seconds) {
|
|
104
|
+
if (autoRefreshInterval) {
|
|
105
|
+
clearInterval(autoRefreshInterval);
|
|
106
|
+
autoRefreshInterval = null;
|
|
107
|
+
autoRefreshSeconds = 0;
|
|
108
|
+
document.getElementById('autoRefreshBtn').textContent = 'Auto: Off';
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
autoRefreshSeconds = seconds;
|
|
112
|
+
document.getElementById('autoRefreshBtn').textContent = `Auto: ${seconds}s`;
|
|
113
|
+
autoRefreshInterval = setInterval(() => {
|
|
114
|
+
fetchPackages();
|
|
115
|
+
fetchHosts();
|
|
116
|
+
fetchRemotes();
|
|
117
|
+
}, seconds * 1000);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cycleAutoRefresh() {
|
|
121
|
+
const options = [0, 30, 60, 120];
|
|
122
|
+
const current = options.indexOf(autoRefreshSeconds);
|
|
123
|
+
const next = options[(current + 1) % options.length];
|
|
124
|
+
if (autoRefreshInterval) {
|
|
125
|
+
clearInterval(autoRefreshInterval);
|
|
126
|
+
autoRefreshInterval = null;
|
|
127
|
+
}
|
|
128
|
+
if (next > 0) {
|
|
129
|
+
toggleAutoRefresh(next);
|
|
130
|
+
} else {
|
|
131
|
+
autoRefreshSeconds = 0;
|
|
132
|
+
document.getElementById('autoRefreshBtn').textContent = 'Auto: Off';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function showLoading(show) {
|
|
137
|
+
document.getElementById('loading').classList.toggle('active', show);
|
|
138
|
+
document.getElementById('overlay').classList.toggle('active', show);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderFilters() {
|
|
142
|
+
if (!cachedData) return;
|
|
143
|
+
|
|
144
|
+
const packageFilters = document.getElementById('packageFilters');
|
|
145
|
+
const packages = Object.keys(cachedData.packages || {});
|
|
146
|
+
packageFilters.innerHTML = packages.map(pkg =>
|
|
147
|
+
`<label><input type="checkbox" value="${pkg}" checked onchange="renderData()"> ${pkg}</label>`
|
|
148
|
+
).join('');
|
|
149
|
+
|
|
150
|
+
const hostFilters = document.getElementById('hostFilters');
|
|
151
|
+
const hosts = Object.keys(cachedData.hosts || {});
|
|
152
|
+
if (hosts.length > 0) {
|
|
153
|
+
hostFilters.innerHTML = hosts.map(host =>
|
|
154
|
+
`<label><input type="checkbox" value="${host}" checked onchange="renderData()"> ${host}</label>`
|
|
155
|
+
).join('');
|
|
156
|
+
} else {
|
|
157
|
+
hostFilters.innerHTML = '<span style="color: var(--text-secondary)">No hosts configured</span>';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const remoteFilters = document.getElementById('remoteFilters');
|
|
161
|
+
const remotes = Object.keys(cachedData.remotes || {});
|
|
162
|
+
if (remotes.length > 0) {
|
|
163
|
+
remoteFilters.innerHTML = remotes.map(remote =>
|
|
164
|
+
`<label><input type="checkbox" value="${remote}" checked onchange="renderData()"> ${remote}</label>`
|
|
165
|
+
).join('');
|
|
166
|
+
} else {
|
|
167
|
+
remoteFilters.innerHTML = '<span style="color: var(--text-secondary)">No remotes configured</span>';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
document.querySelectorAll('#statusFilters input').forEach(input => {
|
|
171
|
+
input.onchange = renderData;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getSelectedFilters() {
|
|
176
|
+
const getChecked = (containerId) =>
|
|
177
|
+
[...document.querySelectorAll(`#${containerId} input:checked`)].map(el => el.value);
|
|
178
|
+
return {
|
|
179
|
+
packages: getChecked('packageFilters'),
|
|
180
|
+
statuses: getChecked('statusFilters'),
|
|
181
|
+
hosts: getChecked('hostFilters'),
|
|
182
|
+
remotes: getChecked('remoteFilters')
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderData() {
|
|
187
|
+
if (!cachedData) return;
|
|
188
|
+
const filters = getSelectedFilters();
|
|
189
|
+
const packages = cachedData.packages || {};
|
|
190
|
+
|
|
191
|
+
const filteredPackages = Object.entries(packages)
|
|
192
|
+
.filter(([name, info]) => {
|
|
193
|
+
if (!filters.packages.includes(name)) return false;
|
|
194
|
+
if (!filters.statuses.includes(info.status)) return false;
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const summary = {
|
|
199
|
+
total: filteredPackages.length,
|
|
200
|
+
ok: filteredPackages.filter(([, i]) => i.status === 'ok').length,
|
|
201
|
+
unreleased: filteredPackages.filter(([, i]) => i.status === 'unreleased').length,
|
|
202
|
+
mismatch: filteredPackages.filter(([, i]) => i.status === 'mismatch').length,
|
|
203
|
+
outdated: filteredPackages.filter(([, i]) => i.status === 'outdated').length
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
document.getElementById('summary').innerHTML = `
|
|
207
|
+
<div class="summary-card total"><div class="number">${summary.total}</div><div class="label">Total</div></div>
|
|
208
|
+
<div class="summary-card ok"><div class="number">${summary.ok}</div><div class="label">OK</div></div>
|
|
209
|
+
<div class="summary-card unreleased"><div class="number">${summary.unreleased}</div><div class="label">Unreleased</div></div>
|
|
210
|
+
<div class="summary-card mismatch"><div class="number">${summary.mismatch}</div><div class="label">Mismatch</div></div>
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
document.getElementById('packages').innerHTML = filteredPackages.map(([name, info]) => {
|
|
214
|
+
const local = info.local || {};
|
|
215
|
+
const git = info.git || {};
|
|
216
|
+
const remote = info.remote || {};
|
|
217
|
+
const hostData = cachedData.hosts || {};
|
|
218
|
+
const remoteData = cachedData.remotes || {};
|
|
219
|
+
|
|
220
|
+
const hostVersions = Object.entries(hostData)
|
|
221
|
+
.filter(([h]) => !h.startsWith('_') && filters.hosts.includes(h))
|
|
222
|
+
.map(([hostName, hostInfo]) => ({ name: hostName, ...(hostInfo[name] || {}) }));
|
|
223
|
+
|
|
224
|
+
const remoteVersions = Object.entries(remoteData)
|
|
225
|
+
.filter(([r]) => !r.startsWith('_') && filters.remotes.includes(r))
|
|
226
|
+
.map(([remoteName, remoteInfo]) => ({ name: remoteName, ...(remoteInfo[name] || {}) }));
|
|
227
|
+
|
|
228
|
+
return renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions);
|
|
229
|
+
}).join('');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions) {
|
|
233
|
+
let html = `
|
|
234
|
+
<div class="package-card">
|
|
235
|
+
<div class="package-header">
|
|
236
|
+
<span class="package-name">${name}</span>
|
|
237
|
+
<span class="status-badge status-${info.status}">${info.status}</span>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="package-body">
|
|
240
|
+
<div class="version-grid">
|
|
241
|
+
<div class="version-section">
|
|
242
|
+
<h4>Local</h4>
|
|
243
|
+
<div class="version-item"><span class="key">toml</span><span class="value">${local.pyproject_toml || '-'}</span></div>
|
|
244
|
+
<div class="version-item"><span class="key">installed</span><span class="value">${local.installed || '-'}</span></div>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="version-section">
|
|
247
|
+
<h4>Git</h4>
|
|
248
|
+
<div class="version-item"><span class="key">tag</span><span class="value">${git.latest_tag || '-'}</span></div>
|
|
249
|
+
<div class="version-item"><span class="key">branch</span><span class="value">${git.branch || '-'}</span></div>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="version-section">
|
|
252
|
+
<h4>PyPI</h4>
|
|
253
|
+
<div class="version-item"><span class="key">published</span><span class="value">${remote.pypi || '-'}</span></div>
|
|
254
|
+
</div>`;
|
|
255
|
+
|
|
256
|
+
if (hostVersions.length > 0) {
|
|
257
|
+
html += `<div class="version-section"><h4>Hosts</h4>`;
|
|
258
|
+
hostVersions.forEach(h => {
|
|
259
|
+
html += `<div class="version-item"><span class="key">${h.name}</span><span class="value">${h.installed || h.error || '-'}</span></div>`;
|
|
260
|
+
});
|
|
261
|
+
html += `</div>`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (remoteVersions.length > 0) {
|
|
265
|
+
html += `<div class="version-section"><h4>GitHub</h4>`;
|
|
266
|
+
remoteVersions.forEach(r => {
|
|
267
|
+
html += `<div class="version-item"><span class="key">${r.name}</span><span class="value">${r.latest_tag || r.error || '-'}</span></div>`;
|
|
268
|
+
});
|
|
269
|
+
html += `</div>`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
html += `</div>`;
|
|
273
|
+
|
|
274
|
+
if (info.issues && info.issues.length > 0) {
|
|
275
|
+
html += `<div class="issues"><h4>Issues</h4><ul>`;
|
|
276
|
+
info.issues.forEach(i => { html += `<li>${i}</li>`; });
|
|
277
|
+
html += `</ul></div>`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
html += `</div></div>`;
|
|
281
|
+
return html;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function refreshData() { await fetchVersions(); }
|
|
285
|
+
|
|
286
|
+
function exportJSON() {
|
|
287
|
+
if (!cachedData) return;
|
|
288
|
+
const blob = new Blob([JSON.stringify(cachedData, null, 2)], { type: 'application/json' });
|
|
289
|
+
const url = URL.createObjectURL(blob);
|
|
290
|
+
const a = document.createElement('a');
|
|
291
|
+
a.href = url;
|
|
292
|
+
a.download = 'scitex-versions.json';
|
|
293
|
+
a.click();
|
|
294
|
+
URL.revokeObjectURL(url);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fetchVersions();
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# EOF
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-02-02
|
|
3
|
+
# File: scitex/_dev/_dashboard/_styles.py
|
|
4
|
+
|
|
5
|
+
"""CSS styles for the dashboard."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_css() -> str:
|
|
9
|
+
"""Return dashboard CSS."""
|
|
10
|
+
return """
|
|
11
|
+
:root {
|
|
12
|
+
--bg-primary: #1a1a2e;
|
|
13
|
+
--bg-secondary: #16213e;
|
|
14
|
+
--bg-card: #0f3460;
|
|
15
|
+
--text-primary: #eee;
|
|
16
|
+
--text-secondary: #aaa;
|
|
17
|
+
--accent: #e94560;
|
|
18
|
+
--success: #4ade80;
|
|
19
|
+
--warning: #fbbf24;
|
|
20
|
+
--error: #f87171;
|
|
21
|
+
--info: #60a5fa;
|
|
22
|
+
}
|
|
23
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
24
|
+
body {
|
|
25
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
26
|
+
background: var(--bg-primary);
|
|
27
|
+
color: var(--text-primary);
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
padding: 20px;
|
|
30
|
+
}
|
|
31
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
32
|
+
header {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
align-items: center;
|
|
36
|
+
margin-bottom: 30px;
|
|
37
|
+
padding: 20px;
|
|
38
|
+
background: var(--bg-secondary);
|
|
39
|
+
border-radius: 10px;
|
|
40
|
+
}
|
|
41
|
+
h1 { font-size: 1.8rem; color: var(--accent); }
|
|
42
|
+
.actions { display: flex; gap: 10px; }
|
|
43
|
+
button {
|
|
44
|
+
padding: 10px 20px;
|
|
45
|
+
border: none;
|
|
46
|
+
border-radius: 5px;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
font-size: 0.9rem;
|
|
49
|
+
transition: all 0.3s ease;
|
|
50
|
+
}
|
|
51
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
52
|
+
.btn-primary:hover { background: #c9184a; }
|
|
53
|
+
.btn-secondary { background: var(--bg-card); color: var(--text-primary); }
|
|
54
|
+
.btn-secondary:hover { background: #1a4d80; }
|
|
55
|
+
.filters {
|
|
56
|
+
display: grid;
|
|
57
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
58
|
+
gap: 20px;
|
|
59
|
+
margin-bottom: 30px;
|
|
60
|
+
padding: 20px;
|
|
61
|
+
background: var(--bg-secondary);
|
|
62
|
+
border-radius: 10px;
|
|
63
|
+
}
|
|
64
|
+
.filter-group { display: flex; flex-direction: column; gap: 10px; }
|
|
65
|
+
.filter-group h3 {
|
|
66
|
+
font-size: 0.9rem;
|
|
67
|
+
color: var(--text-secondary);
|
|
68
|
+
text-transform: uppercase;
|
|
69
|
+
letter-spacing: 1px;
|
|
70
|
+
}
|
|
71
|
+
.filter-options { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
72
|
+
.filter-options label {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: 5px;
|
|
76
|
+
padding: 5px 10px;
|
|
77
|
+
background: var(--bg-card);
|
|
78
|
+
border-radius: 5px;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
font-size: 0.85rem;
|
|
81
|
+
transition: all 0.2s ease;
|
|
82
|
+
}
|
|
83
|
+
.filter-options label:hover { background: #1a4d80; }
|
|
84
|
+
.filter-options input[type="checkbox"],
|
|
85
|
+
.filter-options input[type="radio"] { accent-color: var(--accent); }
|
|
86
|
+
.summary {
|
|
87
|
+
display: grid;
|
|
88
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
89
|
+
gap: 15px;
|
|
90
|
+
margin-bottom: 30px;
|
|
91
|
+
}
|
|
92
|
+
.summary-card {
|
|
93
|
+
padding: 20px;
|
|
94
|
+
background: var(--bg-secondary);
|
|
95
|
+
border-radius: 10px;
|
|
96
|
+
text-align: center;
|
|
97
|
+
}
|
|
98
|
+
.summary-card .number { font-size: 2rem; font-weight: bold; }
|
|
99
|
+
.summary-card .label { font-size: 0.85rem; color: var(--text-secondary); margin-top: 5px; }
|
|
100
|
+
.summary-card.ok .number { color: var(--success); }
|
|
101
|
+
.summary-card.unreleased .number { color: var(--warning); }
|
|
102
|
+
.summary-card.mismatch .number { color: var(--error); }
|
|
103
|
+
.summary-card.total .number { color: var(--info); }
|
|
104
|
+
.packages { display: grid; gap: 20px; }
|
|
105
|
+
.package-card { background: var(--bg-secondary); border-radius: 10px; overflow: hidden; }
|
|
106
|
+
.package-header {
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
align-items: center;
|
|
110
|
+
padding: 15px 20px;
|
|
111
|
+
background: var(--bg-card);
|
|
112
|
+
}
|
|
113
|
+
.package-name { font-size: 1.1rem; font-weight: bold; }
|
|
114
|
+
.status-badge {
|
|
115
|
+
padding: 5px 12px;
|
|
116
|
+
border-radius: 15px;
|
|
117
|
+
font-size: 0.75rem;
|
|
118
|
+
font-weight: bold;
|
|
119
|
+
text-transform: uppercase;
|
|
120
|
+
}
|
|
121
|
+
.status-ok { background: rgba(74, 222, 128, 0.2); color: var(--success); }
|
|
122
|
+
.status-unreleased { background: rgba(251, 191, 36, 0.2); color: var(--warning); }
|
|
123
|
+
.status-mismatch { background: rgba(248, 113, 113, 0.2); color: var(--error); }
|
|
124
|
+
.status-outdated { background: rgba(167, 139, 250, 0.2); color: #a78bfa; }
|
|
125
|
+
.status-unavailable { background: rgba(156, 163, 175, 0.2); color: #9ca3af; }
|
|
126
|
+
.status-error { background: rgba(248, 113, 113, 0.2); color: var(--error); }
|
|
127
|
+
.package-body { padding: 20px; }
|
|
128
|
+
.version-grid {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
131
|
+
gap: 15px;
|
|
132
|
+
}
|
|
133
|
+
.version-section { padding: 10px; background: var(--bg-primary); border-radius: 5px; }
|
|
134
|
+
.version-section h4 {
|
|
135
|
+
font-size: 0.8rem;
|
|
136
|
+
color: var(--text-secondary);
|
|
137
|
+
margin-bottom: 8px;
|
|
138
|
+
text-transform: uppercase;
|
|
139
|
+
}
|
|
140
|
+
.version-item { display: flex; justify-content: space-between; padding: 5px 0; font-size: 0.9rem; }
|
|
141
|
+
.version-item .key { color: var(--text-secondary); }
|
|
142
|
+
.version-item .value { font-family: monospace; }
|
|
143
|
+
.issues {
|
|
144
|
+
margin-top: 15px;
|
|
145
|
+
padding: 10px;
|
|
146
|
+
background: rgba(248, 113, 113, 0.1);
|
|
147
|
+
border-radius: 5px;
|
|
148
|
+
border-left: 3px solid var(--error);
|
|
149
|
+
}
|
|
150
|
+
.issues h4 { font-size: 0.8rem; color: var(--error); margin-bottom: 5px; }
|
|
151
|
+
.issues ul { list-style: none; font-size: 0.85rem; color: var(--text-secondary); }
|
|
152
|
+
.issues li::before { content: "!"; margin-right: 8px; color: var(--error); }
|
|
153
|
+
.loading {
|
|
154
|
+
display: none;
|
|
155
|
+
position: fixed;
|
|
156
|
+
top: 50%;
|
|
157
|
+
left: 50%;
|
|
158
|
+
transform: translate(-50%, -50%);
|
|
159
|
+
background: var(--bg-secondary);
|
|
160
|
+
padding: 30px;
|
|
161
|
+
border-radius: 10px;
|
|
162
|
+
text-align: center;
|
|
163
|
+
z-index: 1000;
|
|
164
|
+
}
|
|
165
|
+
.loading.active { display: block; }
|
|
166
|
+
.spinner {
|
|
167
|
+
border: 4px solid var(--bg-card);
|
|
168
|
+
border-top: 4px solid var(--accent);
|
|
169
|
+
border-radius: 50%;
|
|
170
|
+
width: 40px;
|
|
171
|
+
height: 40px;
|
|
172
|
+
animation: spin 1s linear infinite;
|
|
173
|
+
margin: 0 auto 15px;
|
|
174
|
+
}
|
|
175
|
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
176
|
+
.overlay {
|
|
177
|
+
display: none;
|
|
178
|
+
position: fixed;
|
|
179
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
180
|
+
background: rgba(0, 0, 0, 0.5);
|
|
181
|
+
z-index: 999;
|
|
182
|
+
}
|
|
183
|
+
.overlay.active { display: block; }
|
|
184
|
+
.last-updated { font-size: 0.8rem; color: var(--text-secondary); }
|
|
185
|
+
.loading-section { opacity: 0.5; position: relative; }
|
|
186
|
+
.loading-section::after {
|
|
187
|
+
content: "⟳";
|
|
188
|
+
position: absolute;
|
|
189
|
+
right: 5px;
|
|
190
|
+
top: -20px;
|
|
191
|
+
font-size: 1rem;
|
|
192
|
+
animation: spin 1s linear infinite;
|
|
193
|
+
color: var(--accent);
|
|
194
|
+
}
|
|
195
|
+
.just-updated {
|
|
196
|
+
animation: flash-green 0.5s ease;
|
|
197
|
+
}
|
|
198
|
+
@keyframes flash-green {
|
|
199
|
+
0% { background: rgba(74, 222, 128, 0.3); }
|
|
200
|
+
100% { background: transparent; }
|
|
201
|
+
}
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# EOF
|