scitex 2.17.0__py3-none-any.whl → 2.17.4__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 +182 -0
- scitex/_dev/_dashboard/_scripts.py +422 -0
- scitex/_dev/_dashboard/_styles.py +295 -0
- scitex/_dev/_dashboard/_templates.py +130 -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/_rtd.py +122 -0
- scitex/_dev/_ssh.py +362 -0
- scitex/_dev/_versions.py +272 -0
- scitex/_mcp_tools/__init__.py +2 -0
- scitex/_mcp_tools/dev.py +186 -0
- scitex/audio/_audio_check.py +84 -41
- scitex/cli/capture.py +45 -22
- scitex/cli/dev.py +494 -0
- scitex/cli/main.py +2 -0
- scitex/cli/stats.py +48 -20
- scitex/cli/verify.py +33 -36
- scitex/plt/__init__.py +16 -6
- scitex/scholar/_mcp/crossref_handlers.py +45 -7
- scitex/scholar/_mcp/openalex_handlers.py +45 -7
- scitex/scholar/config/default.yaml +2 -0
- scitex/scholar/local_dbs/__init__.py +5 -1
- scitex/scholar/local_dbs/export.py +93 -0
- scitex/scholar/local_dbs/unified.py +505 -0
- scitex/scholar/metadata_engines/ScholarEngine.py +11 -0
- scitex/scholar/metadata_engines/individual/OpenAlexLocalEngine.py +346 -0
- scitex/scholar/metadata_engines/individual/__init__.py +1 -0
- scitex/template/__init__.py +18 -1
- scitex/template/clone_research_minimal.py +111 -0
- scitex/verify/README.md +0 -12
- scitex/verify/__init__.py +0 -4
- scitex/verify/_visualize.py +0 -4
- scitex/verify/_viz/__init__.py +0 -18
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/METADATA +2 -1
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/RECORD +45 -24
- scitex/verify/_viz/_plotly.py +0 -193
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/WHEEL +0 -0
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/entry_points.txt +0 -0
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
@app.route("/api/rtd")
|
|
121
|
+
def api_rtd():
|
|
122
|
+
"""Get Read the Docs build status."""
|
|
123
|
+
try:
|
|
124
|
+
packages = request.args.getlist("package") or None
|
|
125
|
+
versions = request.args.getlist("version") or None
|
|
126
|
+
from .._rtd import check_all_rtd
|
|
127
|
+
|
|
128
|
+
data = check_all_rtd(packages=packages, versions=versions)
|
|
129
|
+
return jsonify(data)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return jsonify({"error": str(e)}), 500
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _get_all_version_data(force_refresh: bool = False) -> dict[str, Any]:
|
|
135
|
+
"""Get all version data from all sources.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
force_refresh : bool
|
|
140
|
+
If True, bypass any caching.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
dict
|
|
145
|
+
Combined version data.
|
|
146
|
+
"""
|
|
147
|
+
from .._config import get_enabled_hosts, get_enabled_remotes, load_config
|
|
148
|
+
from .._github import check_all_remotes
|
|
149
|
+
from .._ssh import check_all_hosts
|
|
150
|
+
from .._versions import list_versions
|
|
151
|
+
|
|
152
|
+
config = load_config()
|
|
153
|
+
|
|
154
|
+
# Get local versions
|
|
155
|
+
packages_data = list_versions()
|
|
156
|
+
|
|
157
|
+
# Get host versions (if any hosts configured)
|
|
158
|
+
hosts_data = {}
|
|
159
|
+
enabled_hosts = get_enabled_hosts(config)
|
|
160
|
+
if enabled_hosts:
|
|
161
|
+
try:
|
|
162
|
+
hosts_data = check_all_hosts(config=config)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
# Get remote versions (if any remotes configured)
|
|
167
|
+
remotes_data = {}
|
|
168
|
+
enabled_remotes = get_enabled_remotes(config)
|
|
169
|
+
if enabled_remotes:
|
|
170
|
+
try:
|
|
171
|
+
remotes_data = check_all_remotes(config=config)
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"packages": packages_data,
|
|
177
|
+
"hosts": hosts_data,
|
|
178
|
+
"remotes": remotes_data,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# EOF
|
|
@@ -0,0 +1,422 @@
|
|
|
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: {}, rtd: {} };
|
|
12
|
+
|
|
13
|
+
async function fetchVersions() {
|
|
14
|
+
showLoading(true);
|
|
15
|
+
cachedData = { packages: {}, hosts: {}, remotes: {}, rtd: {} };
|
|
16
|
+
renderFilters();
|
|
17
|
+
renderData();
|
|
18
|
+
|
|
19
|
+
// Load packages first (fast)
|
|
20
|
+
fetchPackages();
|
|
21
|
+
// Load hosts, remotes, and RTD in parallel (slower)
|
|
22
|
+
fetchHosts();
|
|
23
|
+
fetchRemotes();
|
|
24
|
+
fetchRtd();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchPackages() {
|
|
28
|
+
setSectionLoading('package', true);
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch('/api/packages');
|
|
31
|
+
cachedData.packages = await response.json();
|
|
32
|
+
renderFilters();
|
|
33
|
+
renderData();
|
|
34
|
+
updateTimestamp();
|
|
35
|
+
setSectionUpdated('package');
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Failed to fetch packages:', err);
|
|
38
|
+
} finally {
|
|
39
|
+
showLoading(false);
|
|
40
|
+
setSectionLoading('package', false);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchHosts() {
|
|
45
|
+
setSectionLoading('host', true);
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch('/api/hosts');
|
|
48
|
+
cachedData.hosts = await response.json();
|
|
49
|
+
renderFilters();
|
|
50
|
+
renderData();
|
|
51
|
+
setSectionUpdated('host');
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Failed to fetch hosts:', err);
|
|
54
|
+
cachedData.hosts = { error: err.message };
|
|
55
|
+
} finally {
|
|
56
|
+
setSectionLoading('host', false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function fetchRemotes() {
|
|
61
|
+
setSectionLoading('remote', true);
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch('/api/remotes');
|
|
64
|
+
cachedData.remotes = await response.json();
|
|
65
|
+
renderFilters();
|
|
66
|
+
renderData();
|
|
67
|
+
setSectionUpdated('remote');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Failed to fetch remotes:', err);
|
|
70
|
+
cachedData.remotes = { error: err.message };
|
|
71
|
+
} finally {
|
|
72
|
+
setSectionLoading('remote', false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fetchRtd() {
|
|
77
|
+
setSectionLoading('rtd', true);
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch('/api/rtd');
|
|
80
|
+
cachedData.rtd = await response.json();
|
|
81
|
+
renderFilters();
|
|
82
|
+
renderData();
|
|
83
|
+
setSectionUpdated('rtd');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('Failed to fetch RTD status:', err);
|
|
86
|
+
cachedData.rtd = { error: err.message };
|
|
87
|
+
} finally {
|
|
88
|
+
setSectionLoading('rtd', false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setSectionLoading(section, loading) {
|
|
93
|
+
const el = document.getElementById(section + 'Filters');
|
|
94
|
+
if (el) {
|
|
95
|
+
if (loading) {
|
|
96
|
+
el.classList.add('loading-section');
|
|
97
|
+
} else {
|
|
98
|
+
el.classList.remove('loading-section');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setSectionUpdated(section) {
|
|
104
|
+
const el = document.getElementById(section + 'Filters');
|
|
105
|
+
if (el) {
|
|
106
|
+
el.classList.add('just-updated');
|
|
107
|
+
setTimeout(() => el.classList.remove('just-updated'), 1000);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function updateTimestamp() {
|
|
112
|
+
document.getElementById('lastUpdated').textContent =
|
|
113
|
+
'Last updated: ' + new Date().toLocaleTimeString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Auto-refresh settings
|
|
117
|
+
let autoRefreshInterval = null;
|
|
118
|
+
let autoRefreshSeconds = 0;
|
|
119
|
+
|
|
120
|
+
function toggleAutoRefresh(seconds) {
|
|
121
|
+
if (autoRefreshInterval) {
|
|
122
|
+
clearInterval(autoRefreshInterval);
|
|
123
|
+
autoRefreshInterval = null;
|
|
124
|
+
autoRefreshSeconds = 0;
|
|
125
|
+
document.getElementById('autoRefreshBtn').textContent = 'Auto: Off';
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
autoRefreshSeconds = seconds;
|
|
129
|
+
document.getElementById('autoRefreshBtn').textContent = `Auto: ${seconds}s`;
|
|
130
|
+
autoRefreshInterval = setInterval(() => {
|
|
131
|
+
fetchPackages();
|
|
132
|
+
fetchHosts();
|
|
133
|
+
fetchRemotes();
|
|
134
|
+
}, seconds * 1000);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cycleAutoRefresh() {
|
|
138
|
+
const options = [0, 30, 60, 120];
|
|
139
|
+
const current = options.indexOf(autoRefreshSeconds);
|
|
140
|
+
const next = options[(current + 1) % options.length];
|
|
141
|
+
if (autoRefreshInterval) {
|
|
142
|
+
clearInterval(autoRefreshInterval);
|
|
143
|
+
autoRefreshInterval = null;
|
|
144
|
+
}
|
|
145
|
+
if (next > 0) {
|
|
146
|
+
toggleAutoRefresh(next);
|
|
147
|
+
} else {
|
|
148
|
+
autoRefreshSeconds = 0;
|
|
149
|
+
document.getElementById('autoRefreshBtn').textContent = 'Auto: Off';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function showLoading(show) {
|
|
154
|
+
document.getElementById('loading').classList.toggle('active', show);
|
|
155
|
+
document.getElementById('overlay').classList.toggle('active', show);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderFilters() {
|
|
159
|
+
if (!cachedData) return;
|
|
160
|
+
|
|
161
|
+
const packageFilters = document.getElementById('packageFilters');
|
|
162
|
+
const packages = Object.keys(cachedData.packages || {});
|
|
163
|
+
packageFilters.innerHTML = packages.map(pkg =>
|
|
164
|
+
`<label><input type="checkbox" value="${pkg}" checked onchange="renderData()"> ${pkg}</label>`
|
|
165
|
+
).join('');
|
|
166
|
+
|
|
167
|
+
const hostFilters = document.getElementById('hostFilters');
|
|
168
|
+
const hosts = Object.keys(cachedData.hosts || {});
|
|
169
|
+
if (hosts.length > 0) {
|
|
170
|
+
hostFilters.innerHTML = hosts.map(host =>
|
|
171
|
+
`<label><input type="checkbox" value="${host}" checked onchange="renderData()"> ${host}</label>`
|
|
172
|
+
).join('');
|
|
173
|
+
} else {
|
|
174
|
+
hostFilters.innerHTML = '<span style="color: var(--text-secondary)">No hosts configured</span>';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const remoteFilters = document.getElementById('remoteFilters');
|
|
178
|
+
const remotes = Object.keys(cachedData.remotes || {});
|
|
179
|
+
if (remotes.length > 0) {
|
|
180
|
+
remoteFilters.innerHTML = remotes.map(remote =>
|
|
181
|
+
`<label><input type="checkbox" value="${remote}" checked onchange="renderData()"> ${remote}</label>`
|
|
182
|
+
).join('');
|
|
183
|
+
} else {
|
|
184
|
+
remoteFilters.innerHTML = '<span style="color: var(--text-secondary)">No remotes configured</span>';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rtdFilters = document.getElementById('rtdFilters');
|
|
188
|
+
const rtdVersions = Object.keys(cachedData.rtd || {});
|
|
189
|
+
if (rtdVersions.length > 0) {
|
|
190
|
+
rtdFilters.innerHTML = rtdVersions.map(v =>
|
|
191
|
+
`<label><input type="checkbox" value="${v}" checked onchange="renderData()"> ${v}</label>`
|
|
192
|
+
).join('');
|
|
193
|
+
} else {
|
|
194
|
+
rtdFilters.innerHTML = '<span style="color: var(--text-secondary)">Loading...</span>';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
document.querySelectorAll('#statusFilters input').forEach(input => {
|
|
198
|
+
input.onchange = renderData;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getSelectedFilters() {
|
|
203
|
+
const getChecked = (containerId) =>
|
|
204
|
+
[...document.querySelectorAll(`#${containerId} input:checked`)].map(el => el.value);
|
|
205
|
+
return {
|
|
206
|
+
packages: getChecked('packageFilters'),
|
|
207
|
+
statuses: getChecked('statusFilters'),
|
|
208
|
+
hosts: getChecked('hostFilters'),
|
|
209
|
+
remotes: getChecked('remoteFilters')
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getEffectiveStatus(name, info) {
|
|
214
|
+
// Check RTD status and downgrade if needed
|
|
215
|
+
const rtdData = cachedData.rtd || {};
|
|
216
|
+
let status = info.status;
|
|
217
|
+
if (status === 'ok') {
|
|
218
|
+
const rtdLatest = rtdData['latest'] && rtdData['latest'][name];
|
|
219
|
+
const rtdStable = rtdData['stable'] && rtdData['stable'][name];
|
|
220
|
+
if ((rtdLatest && rtdLatest.status === 'failing') ||
|
|
221
|
+
(rtdStable && rtdStable.status === 'failing') ||
|
|
222
|
+
(rtdLatest && rtdLatest.status === 'not_found')) {
|
|
223
|
+
status = 'mismatch';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return status;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderData() {
|
|
230
|
+
if (!cachedData) return;
|
|
231
|
+
const filters = getSelectedFilters();
|
|
232
|
+
const packages = cachedData.packages || {};
|
|
233
|
+
|
|
234
|
+
const filteredPackages = Object.entries(packages)
|
|
235
|
+
.filter(([name, info]) => {
|
|
236
|
+
if (!filters.packages.includes(name)) return false;
|
|
237
|
+
const effectiveStatus = getEffectiveStatus(name, info);
|
|
238
|
+
if (!filters.statuses.includes(effectiveStatus)) return false;
|
|
239
|
+
return true;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const summary = {
|
|
243
|
+
total: filteredPackages.length,
|
|
244
|
+
ok: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'ok').length,
|
|
245
|
+
unreleased: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'unreleased').length,
|
|
246
|
+
mismatch: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'mismatch').length,
|
|
247
|
+
outdated: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'outdated').length
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
document.getElementById('summary').innerHTML = `
|
|
251
|
+
<div class="summary-card total"><div class="number">${summary.total}</div><div class="label">Total</div></div>
|
|
252
|
+
<div class="summary-card ok"><div class="number">${summary.ok}</div><div class="label">OK</div></div>
|
|
253
|
+
<div class="summary-card unreleased"><div class="number">${summary.unreleased}</div><div class="label">Unreleased</div></div>
|
|
254
|
+
<div class="summary-card mismatch"><div class="number">${summary.mismatch}</div><div class="label">Mismatch</div></div>
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
document.getElementById('packages').innerHTML = filteredPackages.map(([name, info]) => {
|
|
258
|
+
const local = info.local || {};
|
|
259
|
+
const git = info.git || {};
|
|
260
|
+
const remote = info.remote || {};
|
|
261
|
+
const hostData = cachedData.hosts || {};
|
|
262
|
+
const remoteData = cachedData.remotes || {};
|
|
263
|
+
const rtdData = cachedData.rtd || {};
|
|
264
|
+
|
|
265
|
+
const hostVersions = Object.entries(hostData)
|
|
266
|
+
.filter(([h]) => !h.startsWith('_') && filters.hosts.includes(h))
|
|
267
|
+
.map(([hostName, hostInfo]) => ({ name: hostName, ...(hostInfo[name] || {}) }));
|
|
268
|
+
|
|
269
|
+
const remoteVersions = Object.entries(remoteData)
|
|
270
|
+
.filter(([r]) => !r.startsWith('_') && filters.remotes.includes(r))
|
|
271
|
+
.map(([remoteName, remoteInfo]) => ({ name: remoteName, ...(remoteInfo[name] || {}) }));
|
|
272
|
+
|
|
273
|
+
// Get RTD status for this package (latest and stable)
|
|
274
|
+
const rtdStatus = {};
|
|
275
|
+
Object.entries(rtdData).forEach(([version, pkgData]) => {
|
|
276
|
+
if (pkgData[name]) {
|
|
277
|
+
rtdStatus[version] = pkgData[name];
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions, rtdStatus);
|
|
282
|
+
}).join('');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions, rtdStatus) {
|
|
286
|
+
const pypiUrl = `https://pypi.org/project/${name}/`;
|
|
287
|
+
const githubUrl = `https://github.com/ywatanabe1989/${name}`;
|
|
288
|
+
const rtdUrl = `https://${name === 'scitex' ? 'scitex-python' : name}.readthedocs.io/`;
|
|
289
|
+
|
|
290
|
+
// Collect all issues for tooltip
|
|
291
|
+
let allIssues = [...(info.issues || [])];
|
|
292
|
+
|
|
293
|
+
// Re-evaluate status based on RTD
|
|
294
|
+
let effectiveStatus = info.status;
|
|
295
|
+
if (rtdStatus && Object.keys(rtdStatus).length > 0) {
|
|
296
|
+
const rtdLatest = rtdStatus['latest'];
|
|
297
|
+
const rtdStable = rtdStatus['stable'];
|
|
298
|
+
if (rtdLatest && rtdLatest.status === 'failing') {
|
|
299
|
+
allIssues.push('RTD latest build failing');
|
|
300
|
+
if (effectiveStatus === 'ok') effectiveStatus = 'mismatch';
|
|
301
|
+
}
|
|
302
|
+
if (rtdStable && rtdStable.status === 'failing') {
|
|
303
|
+
allIssues.push('RTD stable build failing');
|
|
304
|
+
if (effectiveStatus === 'ok') effectiveStatus = 'mismatch';
|
|
305
|
+
}
|
|
306
|
+
if (rtdLatest && rtdLatest.status === 'not_found') {
|
|
307
|
+
allIssues.push('RTD project not found');
|
|
308
|
+
if (effectiveStatus === 'ok') effectiveStatus = 'mismatch';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Create tooltip text from issues (using for newlines in title attribute)
|
|
313
|
+
const tooltipText = allIssues.length > 0 ? allIssues.join(' ') : '';
|
|
314
|
+
const tooltipAttr = tooltipText ? `title="${tooltipText}"` : '';
|
|
315
|
+
|
|
316
|
+
let html = `
|
|
317
|
+
<div class="package-card collapsed">
|
|
318
|
+
<div class="package-header" onclick="toggleCard(this)">
|
|
319
|
+
<span class="fold-icon">▶</span>
|
|
320
|
+
<a href="${githubUrl}" target="_blank" class="package-name" onclick="event.stopPropagation()">${name}</a>
|
|
321
|
+
<span class="status-badge status-${effectiveStatus}" ${tooltipAttr}>${effectiveStatus}</span>
|
|
322
|
+
<span class="quick-links">
|
|
323
|
+
<a href="${pypiUrl}" target="_blank" title="PyPI" onclick="event.stopPropagation()">📦</a>
|
|
324
|
+
<a href="${githubUrl}" target="_blank" title="GitHub" onclick="event.stopPropagation()">🐙</a>
|
|
325
|
+
<a href="${rtdUrl}" target="_blank" title="Docs" onclick="event.stopPropagation()">📖</a>
|
|
326
|
+
</span>
|
|
327
|
+
</div>
|
|
328
|
+
<div class="package-body">
|
|
329
|
+
<div class="version-grid">
|
|
330
|
+
<div class="version-section">
|
|
331
|
+
<h4>LOCAL</h4>
|
|
332
|
+
<div class="version-item"><span class="key">toml</span><span class="value">${local.pyproject_toml || '-'}</span></div>
|
|
333
|
+
<div class="version-item"><span class="key">installed</span><span class="value">${local.installed || '-'}</span></div>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="version-section">
|
|
336
|
+
<h4>GIT</h4>
|
|
337
|
+
<div class="version-item"><span class="key">tag</span><span class="value">${git.latest_tag || '-'}</span></div>
|
|
338
|
+
<div class="version-item"><span class="key">branch</span><span class="value">${git.branch || '-'}</span></div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="version-section">
|
|
341
|
+
<h4><a href="https://pypi.org/project/${name}/" target="_blank">PYPI</a></h4>
|
|
342
|
+
<div class="version-item"><span class="key">published</span><span class="value">${remote.pypi || '-'}</span></div>
|
|
343
|
+
</div>`;
|
|
344
|
+
|
|
345
|
+
if (hostVersions.length > 0) {
|
|
346
|
+
hostVersions.forEach(h => {
|
|
347
|
+
html += `<div class="version-section"><h4>${h.name.toUpperCase()}</h4>`;
|
|
348
|
+
html += `<div class="version-item"><span class="key">toml</span><span class="value">${h.toml || '-'}</span></div>`;
|
|
349
|
+
html += `<div class="version-item"><span class="key">installed</span><span class="value">${h.installed || h.error || '-'}</span></div>`;
|
|
350
|
+
html += `</div>`;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (remoteVersions.length > 0) {
|
|
355
|
+
html += `<div class="version-section"><h4><a href="${githubUrl}" target="_blank">GITHUB</a></h4>`;
|
|
356
|
+
remoteVersions.forEach(r => {
|
|
357
|
+
html += `<div class="version-item"><span class="key">${r.name}</span><span class="value">${r.latest_tag || r.error || '-'}</span></div>`;
|
|
358
|
+
});
|
|
359
|
+
html += `</div>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (rtdStatus && Object.keys(rtdStatus).length > 0) {
|
|
363
|
+
html += `<div class="version-section"><h4><a href="${rtdUrl}" target="_blank">RTD</a></h4>`;
|
|
364
|
+
Object.entries(rtdStatus).forEach(([version, data]) => {
|
|
365
|
+
const statusClass = data.status === 'passing' ? 'rtd-passing' : (data.status === 'failing' ? 'rtd-failing' : 'rtd-unknown');
|
|
366
|
+
const statusIcon = data.status === 'passing' ? '✓' : (data.status === 'failing' ? '✗' : '?');
|
|
367
|
+
const link = data.url ? `<a href="${data.url}" target="_blank">${statusIcon}</a>` : statusIcon;
|
|
368
|
+
html += `<div class="version-item"><span class="key">${version}</span><span class="value ${statusClass}">${link} ${data.status || '-'}</span></div>`;
|
|
369
|
+
});
|
|
370
|
+
html += `</div>`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
html += `</div>`;
|
|
374
|
+
|
|
375
|
+
if (allIssues.length > 0) {
|
|
376
|
+
html += `<div class="issues"><h4>Issues</h4><ul>`;
|
|
377
|
+
allIssues.forEach(i => { html += `<li>${i}</li>`; });
|
|
378
|
+
html += `</ul></div>`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
html += `</div></div>`;
|
|
382
|
+
return html;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function refreshData() { await fetchVersions(); }
|
|
386
|
+
|
|
387
|
+
function toggleCard(header) {
|
|
388
|
+
const card = header.parentElement;
|
|
389
|
+
card.classList.toggle('collapsed');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function toggleAllCards(expand) {
|
|
393
|
+
document.querySelectorAll('.package-card').forEach(card => {
|
|
394
|
+
if (expand) {
|
|
395
|
+
card.classList.remove('collapsed');
|
|
396
|
+
} else {
|
|
397
|
+
card.classList.add('collapsed');
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function toggleFilters() {
|
|
403
|
+
const filters = document.querySelector('.filters');
|
|
404
|
+
filters.classList.toggle('collapsed');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function exportJSON() {
|
|
408
|
+
if (!cachedData) return;
|
|
409
|
+
const blob = new Blob([JSON.stringify(cachedData, null, 2)], { type: 'application/json' });
|
|
410
|
+
const url = URL.createObjectURL(blob);
|
|
411
|
+
const a = document.createElement('a');
|
|
412
|
+
a.href = url;
|
|
413
|
+
a.download = 'scitex-versions.json';
|
|
414
|
+
a.click();
|
|
415
|
+
URL.revokeObjectURL(url);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fetchVersions();
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# EOF
|