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.
Files changed (70) hide show
  1. scitex/_dev/__init__.py +122 -0
  2. scitex/_dev/_config.py +391 -0
  3. scitex/_dev/_dashboard/__init__.py +11 -0
  4. scitex/_dev/_dashboard/_app.py +89 -0
  5. scitex/_dev/_dashboard/_routes.py +169 -0
  6. scitex/_dev/_dashboard/_scripts.py +301 -0
  7. scitex/_dev/_dashboard/_styles.py +205 -0
  8. scitex/_dev/_dashboard/_templates.py +117 -0
  9. scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
  10. scitex/_dev/_ecosystem.py +109 -0
  11. scitex/_dev/_github.py +360 -0
  12. scitex/_dev/_mcp/__init__.py +11 -0
  13. scitex/_dev/_mcp/handlers.py +182 -0
  14. scitex/_dev/_ssh.py +332 -0
  15. scitex/_dev/_versions.py +272 -0
  16. scitex/_mcp_resources/_cheatsheet.py +1 -1
  17. scitex/_mcp_resources/_modules.py +1 -1
  18. scitex/_mcp_tools/__init__.py +4 -0
  19. scitex/_mcp_tools/dev.py +186 -0
  20. scitex/_mcp_tools/verify.py +256 -0
  21. scitex/audio/_audio_check.py +84 -41
  22. scitex/cli/capture.py +45 -22
  23. scitex/cli/dev.py +494 -0
  24. scitex/cli/main.py +4 -0
  25. scitex/cli/stats.py +48 -20
  26. scitex/cli/verify.py +473 -0
  27. scitex/dev/plt/__init__.py +1 -1
  28. scitex/dev/plt/mpl/get_dir_ax.py +1 -1
  29. scitex/dev/plt/mpl/get_signatures.py +1 -1
  30. scitex/dev/plt/mpl/get_signatures_details.py +1 -1
  31. scitex/io/_load.py +8 -1
  32. scitex/io/_save.py +12 -0
  33. scitex/plt/__init__.py +16 -6
  34. scitex/session/README.md +2 -2
  35. scitex/session/__init__.py +1 -0
  36. scitex/session/_decorator.py +57 -33
  37. scitex/session/_lifecycle/__init__.py +23 -0
  38. scitex/session/_lifecycle/_close.py +225 -0
  39. scitex/session/_lifecycle/_config.py +112 -0
  40. scitex/session/_lifecycle/_matplotlib.py +83 -0
  41. scitex/session/_lifecycle/_start.py +246 -0
  42. scitex/session/_lifecycle/_utils.py +186 -0
  43. scitex/session/_manager.py +40 -3
  44. scitex/session/template.py +1 -1
  45. scitex/template/__init__.py +18 -1
  46. scitex/template/_templates/plt.py +1 -1
  47. scitex/template/_templates/session.py +1 -1
  48. scitex/template/clone_research_minimal.py +111 -0
  49. scitex/verify/README.md +300 -0
  50. scitex/verify/__init__.py +208 -0
  51. scitex/verify/_chain.py +369 -0
  52. scitex/verify/_db.py +600 -0
  53. scitex/verify/_hash.py +187 -0
  54. scitex/verify/_integration.py +127 -0
  55. scitex/verify/_rerun.py +253 -0
  56. scitex/verify/_tracker.py +330 -0
  57. scitex/verify/_visualize.py +44 -0
  58. scitex/verify/_viz/__init__.py +38 -0
  59. scitex/verify/_viz/_colors.py +84 -0
  60. scitex/verify/_viz/_format.py +302 -0
  61. scitex/verify/_viz/_json.py +192 -0
  62. scitex/verify/_viz/_mermaid.py +440 -0
  63. scitex/verify/_viz/_templates.py +246 -0
  64. scitex/verify/_viz/_utils.py +56 -0
  65. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/METADATA +2 -1
  66. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/RECORD +69 -28
  67. scitex/session/_lifecycle.py +0 -827
  68. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/WHEEL +0 -0
  69. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/entry_points.txt +0 -0
  70. {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