diff --git a/backend/atlas_portal/routes/ai.py b/backend/atlas_portal/routes/ai.py index 4810e9e..8401470 100644 --- a/backend/atlas_portal/routes/ai.py +++ b/backend/atlas_portal/routes/ai.py @@ -22,18 +22,18 @@ def register(app) -> None: payload = request.get_json(silent=True) or {} user_message = (payload.get("message") or "").strip() - profile = (payload.get("profile") or payload.get("mode") or "atlas-quick").strip().lower() + profile = (payload.get("profile") or payload.get("mode") or "atlas-smart").strip().lower() conversation_id = payload.get("conversation_id") if isinstance(payload.get("conversation_id"), str) else "" if not user_message: return jsonify({"error": "message required"}), 400 started = time.time() - mode = "quick" - if profile in {"atlas-smart", "smart"}: - mode = "smart" - elif profile in {"atlas-genius", "genius"}: + mode = "smart" + if profile in {"atlas-genius", "genius"}: mode = "genius" + elif profile in {"atlas-quick", "quick"}: + mode = "quick" reply = _atlasbot_answer(user_message, mode, conversation_id) source = f"atlas-{mode}" if reply: @@ -67,7 +67,7 @@ def register(app) -> None: def ai_info() -> Any: """Return model and placement metadata for the requested AI profile.""" - profile = (request.args.get("profile") or "atlas-quick").strip().lower() + profile = (request.args.get("profile") or "atlas-smart").strip().lower() meta = _discover_ai_meta(profile) return jsonify(meta) diff --git a/backend/tests/test_ai.py b/backend/tests/test_ai.py index d3cb6cd..0618f18 100644 --- a/backend/tests/test_ai.py +++ b/backend/tests/test_ai.py @@ -45,12 +45,28 @@ class AiRouteTests(TestCase): self.assertEqual(data.get("source"), f"atlas-{expected_mode}") self.assertEqual(data.get("reply"), f"{expected_mode}:conv-{profile}") + resp = self.client.post( + "/api/chat", + data=json.dumps( + { + "message": "How is Titan doing?", + "conversation_id": "conv-default", + } + ), + content_type="application/json", + ) + data = resp.get_json() + self.assertEqual(resp.status_code, 200) + self.assertEqual(data.get("source"), "atlas-smart") + self.assertEqual(data.get("reply"), "smart:conv-default") + self.assertEqual( seen, [ ("quick", "conv-atlas-quick"), ("smart", "conv-atlas-smart"), ("genius", "conv-atlas-genius"), + ("smart", "conv-default"), ], ) @@ -63,6 +79,14 @@ class AiRouteTests(TestCase): self.assertEqual(data.get("profile"), "atlas-genius") self.assertEqual(data.get("model"), "genius-model") + with mock.patch.object(ai.settings, "AI_ATLASBOT_MODEL_SMART", "smart-model"): + resp = self.client.get("/api/ai/info") + + data = resp.get_json() + self.assertEqual(resp.status_code, 200) + self.assertEqual(data.get("profile"), "atlas-smart") + self.assertEqual(data.get("model"), "smart-model") + def test_atlasbot_answer_uses_profile_specific_timeout(self): captured: dict[str, object] = {} diff --git a/frontend/src/components/HomeHeroStatus.vue b/frontend/src/components/HomeHeroStatus.vue index 6e4328f..a6f3360 100644 --- a/frontend/src/components/HomeHeroStatus.vue +++ b/frontend/src/components/HomeHeroStatus.vue @@ -27,7 +27,7 @@
-
+
{{ item.label }} @@ -45,6 +45,13 @@ {{ item.value }}
{{ item.value }}
+
+
+ + {{ child.label }} + {{ child.value }} +
+
@@ -230,10 +237,18 @@ function isExternal(href) { background: var(--accent-cyan); } +.status-dot.warn { + background: #f2c94c; +} + .status-dot.bad { background: var(--accent-rose); } +.status-row-copy { + min-width: 0; +} + .status-label { font-weight: 800; color: var(--text-strong); @@ -258,6 +273,45 @@ function isExternal(href) { color: var(--accent-cyan); } +.status-children { + display: grid; + gap: 6px; + margin-top: 10px; +} + +.status-child { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + color: var(--text-muted); + font-size: 13px; +} + +.status-child-dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--text-muted); +} + +.status-child-dot.ok { + background: var(--accent-cyan); +} + +.status-child-dot.warn { + background: #f2c94c; +} + +.status-child-dot.bad { + background: var(--accent-rose); +} + +.status-child-value { + color: var(--text-strong); + font-size: 12px; +} + .status-note { margin: 14px 0 0; } diff --git a/frontend/src/views/AiView.vue b/frontend/src/views/AiView.vue index 739d04a..aa2b903 100644 --- a/frontend/src/views/AiView.vue +++ b/frontend/src/views/AiView.vue @@ -85,11 +85,10 @@ const apiDisplay = apiUrl.host + apiUrl.pathname; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const profiles = [ - { id: "atlas-quick", label: "Atlas Quick" }, { id: "atlas-smart", label: "Atlas Smart" }, { id: "atlas-genius", label: "Atlas Genius" }, ]; -const activeProfile = ref("atlas-quick"); +const activeProfile = ref("atlas-smart"); const profileState = reactive( Object.fromEntries( profiles.map((profile) => [ @@ -244,7 +243,8 @@ function handleKeydown(e) { async function copyCurl() { const target = current.value.meta.endpoint || apiUrl.toString(); - const curl = `curl -X POST ${target} -H 'content-type: application/json' -d '{\"message\":\"hi\"}'`; + const body = JSON.stringify({ message: "hi", profile: activeProfile.value }); + const curl = `curl -X POST ${target} -H 'content-type: application/json' -d '${body}'`; try { await navigator.clipboard.writeText(curl); copied.value = true; diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index c3151a7..6b953fa 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -65,18 +65,19 @@ const mailtoHref = contactMailto(); const statusItems = computed(() => [ { - label: "Atlas platform", + label: "Atlas cluster", labelHref: "#platform", value: statusLabelFrom(contentStatus.value.atlas), state: contentStatus.value.atlas, }, { - label: "Dedicated hosts", + label: "Outpost hosts", value: dedicatedHostsLabel.value, state: dedicatedHostsState.value, + children: dedicatedHostItems.value, }, { - label: "Most active service", + label: "Most requested service", value: activeServiceLabel.value, href: activeServiceHref.value, state: activeServiceState.value, @@ -98,14 +99,27 @@ const activeServiceState = computed(() => (activeService.value?.known ? "ok" : " const dedicatedHosts = computed(() => props.labStatus?.dedicated_hosts || null); const dedicatedHostsState = computed(() => { if (!dedicatedHosts.value?.known) return "unknown"; - return dedicatedHosts.value.up ? "ok" : "bad"; + const total = dedicatedHostTotal(dedicatedHosts.value); + const upCount = Number.isFinite(dedicatedHosts.value.up_count) ? dedicatedHosts.value.up_count : 0; + if (upCount <= 0) return "bad"; + if (upCount < total) return "warn"; + return "ok"; }); const dedicatedHostsLabel = computed(() => { const hosts = dedicatedHosts.value; if (!hosts?.known) return "Waiting for public checks"; - const total = Number.isFinite(hosts.total) ? hosts.total : 2; + const total = dedicatedHostTotal(hosts); const upCount = Number.isFinite(hosts.up_count) ? hosts.up_count : 0; - return `${upCount} of ${total} responding`; + return `${upCount} of ${total} online`; +}); +const dedicatedHostItems = computed(() => { + const hosts = dedicatedHosts.value?.hosts; + if (!Array.isArray(hosts) || !hosts.length) return []; + return hosts.map((host) => ({ + label: dedicatedHostLabel(host.label), + value: host.known ? (host.up ? "Online" : "Offline") : "Unknown", + state: statusState(host), + })); }); const lastCheckedLabel = computed(() => { @@ -135,6 +149,18 @@ function statusLabelFrom(state) { if (state === "bad") return "Needs attention"; return "Unknown"; } + +function dedicatedHostTotal(hosts) { + if (Number.isFinite(hosts?.total)) return hosts.total; + return Array.isArray(hosts?.hosts) && hosts.hosts.length ? hosts.hosts.length : 2; +} + +function dedicatedHostLabel(label = "") { + const normalized = label.toLowerCase(); + if (normalized.includes("database") || normalized.includes("db")) return "Database machine"; + if (normalized.includes("jump") || normalized.includes("theia")) return "Jumphost"; + return label || "Host"; +}