diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index 72f497d..5c918ea 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -76,6 +76,25 @@ def test_apply_creates_backup_and_replaces_file(tmp_path, monkeypatch): assert report.results[0].detail["cache_invalidated"] is True +def test_source_failure_preserves_existing_file_and_reports_error(tmp_path, monkeypatch): + current = _sp500_rows(450) + output_path = tmp_path / "data" / "sp500_constituents.csv" + _write_constituents(output_path, current) + _disable_cache_invalidation(monkeypatch) + + def failing_source(): + raise RuntimeError("source unavailable") + + runner = _runner(tmp_path, {"sp500_constituents": failing_source}) + + report = runner.run(tasks=["sp500_constituents"], dry_run=False) + + assert report.status == "failed" + assert "source unavailable" in report.errors + assert _read_symbols(output_path) == [r["symbol"] for r in current] + assert not (tmp_path / "data" / "backups" / "constituents").exists() + + def test_validation_failure_preserves_existing_file(tmp_path, monkeypatch): current = _sp500_rows(450) invalid = _sp500_rows(1) @@ -107,6 +126,14 @@ def test_maintenance_page_loads(client): assert b"System Maintenance" in response.data +def test_maintenance_nav_link_registered_in_main_js(client): + response = client.get("/static/js/main.js") + + assert response.status_code == 200 + assert b"/maintenance" in response.data + assert b"Maintenance" in response.data + + def test_maintenance_status_api_loads(client): response = client.get("/api/maintenance/status") diff --git a/web/static/js/main.js b/web/static/js/main.js index 290f9ce..ff9172e 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -3,6 +3,10 @@ document.addEventListener('DOMContentLoaded', () => { console.log('[TWS Robot] UI ready'); + // Ensure optional/operator pages are discoverable even when the server-side + // navbar template has not been expanded yet. + _ensureMaintenanceNavLink(); + // Auto-enrich symbol cells with company names _enrichSymbolNames(); }); @@ -79,6 +83,38 @@ window.renderMarkdown = function renderMarkdown(text) { .replace(/\n/g, '
'); }; +/** + * Add the System Maintenance console to the Monitoring dropdown. + * This keeps the operator-only page discoverable from the web UI. + * @private + */ +function _ensureMaintenanceNavLink() { + const existing = document.querySelector('a[href="/maintenance"]'); + if (existing) return; + + const monitoringToggle = Array.from(document.querySelectorAll('.nav-dropdown-toggle')) + .find(btn => (btn.textContent || '').includes('Monitoring')); + if (!monitoringToggle) return; + + const dropdown = monitoringToggle.closest('.nav-dropdown'); + const menu = dropdown ? dropdown.querySelector('.nav-dropdown-menu') : null; + if (!menu) return; + + const li = document.createElement('li'); + const isMaintenancePage = window.location.pathname === '/maintenance' || + window.location.pathname.startsWith('/maintenance/'); + if (isMaintenancePage) { + li.className = 'active'; + dropdown.classList.add('active'); + } + + const link = document.createElement('a'); + link.href = '/maintenance'; + link.textContent = 'Maintenance'; + li.appendChild(link); + menu.appendChild(li); +} + /** * Fetch company names for all ``[data-symbol]`` elements on the page * and enrich them with a tooltip (title) and a small subtitle showing