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";
+}