Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions ui/src/dash/board/board.css
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,55 @@

/* ─── Responsive ─────────────────────────────────────────────────────── */
@media (max-width: 1100px) { .board .lane { width: 270px; } }

@media (max-width: 720px) {
.board .board-top {
padding: 14px 16px 0;
}

.board .board-bar {
flex-wrap: wrap;
gap: 10px;
padding: 10px 12px;
}

.board .board-select {
flex: 1 1 100%;
min-width: 0;
}

.board .board-select .bs-btn {
width: 100%;
min-width: 0;
}

.board .board-select .bs-btn .nm {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.board .board-bar .bb-spacer {
flex: 1 1 auto;
}

.board .filterbar {
gap: 10px;
}

.board .flt,
.board .flt-search,
.board .flt-search .input {
width: 100%;
}

.board .flt-actions {
width: 100%;
flex-wrap: wrap;
}

.board .lanes-scroll {
padding: 4px 16px 0;
}
}
142 changes: 7 additions & 135 deletions ui/src/dash/chrome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

import { useRuntimeRollup, useHealthSystem, failingChecks } from '@/api/hooks/useRuntime'
import { useLogsStream } from '@/api/hooks/useLogs'
import { useSlots, useEndpoints } from '@/api/hooks/useSlots'
import { useSlots } from '@/api/hooks/useSlots'
import { useModels } from '@/api/hooks/useModels'
import { useMemoryEnabled } from '@/api/hooks/useMemory'
import { useUpdateState } from '@/api/hooks/useUpdates'
import { useSidebarAgentRollup, useApprovalList, useApproveApproval, useDenyApproval } from '@/api/hooks/useAgents'
import { useConfigUrls } from '@/api/hooks/useConfigUrls'
import { useHardware } from '@/api/hooks/useHardware'
import { useApprovalList, useApproveApproval, useDenyApproval } from '@/api/hooks/useAgents'
import { useServicesHealth } from '@/api/hooks/useServicesHealth'

const { useState: useStateC, useEffect: useEffectC } = React;
Expand Down Expand Up @@ -137,12 +135,7 @@ const Icons = {
};

// ─── TopBar ───
function TopBar({ route, hostUptime = "14d 02:11", onBell, onCmdK, onMenu, menuOpen = false, approvals = 0 }) {
// Issue #333: hostname from live /api/hardware (useHardware hook) instead of
// the legacy HAL0_DATA seed. Fall back to a neutral "hal0" placeholder
// while the first response is in flight so the layout stays stable.
const hw = useHardware();
const hostName = hw.data?.name || "hal0";
function TopBar({ route, onCmdK, onMenu, menuOpen = false }) {
// Brand-bar version badge — live from /api/updates/state (hal0.current,
// sourced from hal0.__version__) so it never goes stale; the static fallback
// keeps it correct before the first response lands.
Expand Down Expand Up @@ -186,18 +179,9 @@ function TopBar({ route, hostUptime = "14d 02:11", onBell, onCmdK, onMenu, menuO
<kbd>⌘B</kbd>
</button>
<button className="tb-cmdk" onClick={onCmdK}>
{Icons.search}<span>Command palette</span>
{Icons.zap}<span>Quick actions</span>
<kbd>⌘K</kbd>
</button>
<div className="tb-host">
<span className="host-dot" />
<b>{hostName}</b>
<span className="ut">· up {hostUptime}</span>
</div>
<button className="tb-bell" onClick={onBell} aria-label="Agent approvals">
{Icons.bell}
{approvals > 0 && <span className="badge num">{approvals}</span>}
</button>
{/* Mobile-only nav launcher (hidden ≤720px sidebar is gone) → opens NavDrawer. */}
<button
className="tb-menu"
Expand Down Expand Up @@ -298,116 +282,6 @@ function Sidebar({ route, param, onGo }) {
))}
</div>
<div className="sb-spacer" />
{/*
Runtime widget (2026-06-05): the former three stacked status blocks
(SidebarAgentBlock / SidebarEndpointBlock / SidebarStatusBlock) are
consolidated into ONE card so hermes, hal0, the runtime and openwebui read
as a single runtime rollup. hermes + openwebui rows deep-link to their
own dashboards.
*/}
<SidebarRuntimeWidget onGo={onGo} />
</div>
);
}

// ─── Runtime widget (consolidated status card) ───
//
// Single sidebar card that rolls up the four runtime surfaces that used to
// live in three separate blocks:
// - hermes — bundled agent health (useSidebarAgentRollup → /api/agents).
// Row key deep-links to the standalone Hermes dashboard.
// - hal0 — the composite ``hal0`` /v1 upstream surfaced as a synthetic
// entry on /api/slots (useEndpoints filters `_synthetic`);
// NOT a lifecycle slot, so it's read-only here. Model count is
// the chat figure (advertised_models) plus a non-chat modality
// breakdown counted from the real slots by group.
// - runtime — container-slot readiness (useRuntimeRollup → /api/slots).
// - openwebui — the external chat UI link from /api/config/urls.
// Row key deep-links to the OpenWebUI app.
//
// hermes + openwebui link targets are NOT hardcoded — useConfigUrls() reads
// GET /api/config/urls, where the backend derives the reachable host from the
// request (so links work on localhost / LAN IP / hal0.local / a custom
// reverse-proxy domain) and honours the HAL0_{OPENWEBUI,HERMES}_PUBLIC_URL
// env overrides.
function SidebarRuntimeWidget({ onGo }) {
const agent = useSidebarAgentRollup();
const endpoints = useEndpoints().data || [];
const slots = useSlots().data || [];
const urls = useConfigUrls();

// hermes web dashboard link — only when the backend advertises a public
// URL (loopback-only otherwise, so no host:port fallback exists).
const hermesUrl = urls.data?.hermes || "";
const hermesLinkable = urls.data?.hermes_enabled === true && !!hermesUrl;

// ── hal0 endpoint ── the synthetic composite upstream (first/only one).
const ep = endpoints[0];
const chatCount = ep?.advertised_models ?? 0;
const embedCount = slots.filter(s => s.group === "embed").length;
const voiceCount = slots.filter(s => s.group === "voice").length;
const imgCount = slots.filter(s => s.group === "img").length;
const extraParts = [];
if (embedCount > 0) extraParts.push(`${embedCount} embed`);
if (voiceCount > 0) extraParts.push(`${voiceCount} voice`);
if (imgCount > 0) extraParts.push(`${imgCount} img`);
const modelsTitle = [`${chatCount} chat`, ...extraParts].join(" · ");

// ── openwebui ── /api/config/urls is link discovery only. Health lives in
// the footer runtime chip, driven by /api/services/health.
const owuiUrl = urls.data?.openwebui || "";
const owuiLinkable = !!owuiUrl;

return (
<div className="sb-status sb-runtime" data-testid="sidebar-runtime-widget">
<div className="sb-runtime-h">Runtime</div>

{/* hermes — deep-links to the Hermes dashboard when one is published */}
<div className="row" data-testid="runtime-row-hermes">
{hermesLinkable ? (
<a
className="k rt-link"
href={hermesUrl}
target="_blank"
rel="noopener noreferrer"
title="Open the Hermes dashboard"
>hermes ↗</a>
) : (
<span className="k">hermes</span>
)}
<span className="v">{agent.installed ? "agent" : "not installed"}</span>
</div>

{/* hal0 — composite /v1 endpoint (read-only) + model count */}
<div className="row" data-testid="runtime-row-hal0" title={modelsTitle || ep?._synthetic_reason || ""}>
<span className="k">hal0</span>
<span className="v">
<b>{chatCount}</b>
<span className="rt-model-label"> models</span>
{extraParts.length > 0 && (
<span className="rt-extra"> + {extraParts.join(" · ")}</span>
)}
</span>
</div>

{/* openwebui — deep-links to the external chat UI when a URL is known */}
<div className="row" data-testid="runtime-row-openwebui">
{owuiLinkable ? (
<a
className="k rt-link"
href={owuiUrl}
target="_blank"
rel="noopener noreferrer"
title="Open the OpenWebUI chat"
>openwebui ↗</a>
) : (
<span className="k">openwebui</span>
)}
<span className="v">chat</span>
</div>

<div className="ln" />
<div className="nudge" onClick={() => onGo && onGo("logs")}>View runtime logs →</div>
</div>
);
}
Expand Down Expand Up @@ -788,9 +662,8 @@ if (typeof document !== "undefined" && !document.getElementById("hal0-modal-css"
// ─── Bottom tab bar (mobile <720px) ───
// ─── NavDrawer (mobile) ───
// Slide-in drawer that replaces the (display:none) desktop sidebar at
// ≤720px. Mirrors the sidebar: the command-palette launcher (folded in
// from the topbar on mobile), the full nav (useNavItems), and the runtime
// status widget. Opened from the topbar hamburger; closed via backdrop,
// ≤720px. Mirrors the sidebar: the quick-actions launcher (folded in
// from the topbar on mobile) and the full nav (useNavItems). Opened from the topbar hamburger; closed via backdrop,
// the X, Esc, or navigating.
function NavDrawer({ open, route, param, onGo, onClose, onCmdK }) {
const items = useNavItems();
Expand All @@ -814,7 +687,7 @@ function NavDrawer({ open, route, param, onGo, onClose, onCmdK }) {
</button>
</div>
<button className="nav-drawer-cmdk" onClick={onCmdK}>
{Icons.search}<span>Command palette</span><kbd>⌘K</kbd>
{Icons.zap}<span>Quick actions</span><kbd>⌘K</kbd>
</button>
<div className="sb-list">
{items.map(it => (
Expand Down Expand Up @@ -842,7 +715,6 @@ function NavDrawer({ open, route, param, onGo, onClose, onCmdK }) {
))}
</div>
<div className="sb-spacer" />
<SidebarRuntimeWidget onGo={onGo} />
</>)}
</aside>
</>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/dash/command-palette.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ function CommandPaletteInner({ onClose }) {

return (
<div className="cp-backdrop" onMouseDown={(e) => { if (e.target.classList.contains("cp-backdrop")) onClose(); }}>
<div className="cp-shell" role="dialog" aria-label="Command palette">
<div className="cp-shell" role="dialog" aria-label="Quick actions">
<div className="cp-input-row">
<span className="cp-input-ic">{Icons.search}</span>
<input
Expand Down
92 changes: 92 additions & 0 deletions ui/src/dash/connections.css
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,98 @@
/* loading / empty rows inside a pane body */
.conn .cn-empty { padding: 18px 14px; font-family: var(--jbm); font-size: 11.5px; color: var(--fg-4); text-align: center; }

@media (max-width: 720px) {
.conn-eyebrow {
flex-wrap: wrap;
gap: 6px 10px;
}

.cpane-h {
flex-wrap: wrap;
gap: 10px;
padding: 12px 14px;
}

.cpane-titles {
flex: 1 1 calc(100% - 48px);
}

.cpane-title,
.cpane-sub {
overflow: hidden;
text-overflow: ellipsis;
}

.cpane-h .grow {
display: none;
}

.cpane-h .cpill,
.cpane-h .cbtn {
flex: 0 1 auto;
}

.cpane-h .caret {
margin-left: auto;
}

.ep-head {
display: none;
}

.eprow-main {
grid-template-columns: 9px minmax(0, 1fr) auto auto 24px;
grid-template-rows: auto auto auto;
gap: 4px 10px;
padding: 10px 12px;
}

.eprow-main .ep-dot {
grid-column: 1;
grid-row: 1 / 4;
align-self: start;
margin-top: 4px;
}

.eprow-main .ep-name {
grid-column: 2 / 5;
grid-row: 1;
}

.eprow-main .ep-model {
grid-column: 2 / 5;
grid-row: 2;
}

.eprow-main .ep-dev {
grid-column: 2;
grid-row: 3;
text-align: left;
}

.eprow-main .ep-port {
grid-column: 3;
grid-row: 3;
justify-self: end;
}

.eprow-main .ep-tps {
grid-column: 4;
grid-row: 3;
justify-self: end;
}

.eprow-main .ep-caret {
grid-column: 5;
grid-row: 1 / 4;
align-self: center;
}

.ep-cols {
grid-template-columns: 1fr;
}
}

/* ════════════════════════════════════════════════════════════════
MCP — quick-links + expandable server panes with tools
════════════════════════════════════════════════════════════════ */
Expand Down
17 changes: 1 addition & 16 deletions ui/src/dash/dash-grid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -586,14 +586,6 @@ function DashboardOverhaulView({ editing: editingProp, onToggleEdit }) {
const slots = slotsQuery.data ?? [];
const hw = hwQuery.data ?? null;

// Live host identity for the hero greeting (handoff: "Welcome back, halo.
// system steady on <host>"). Prefer the live stats node, fall back to the
// HAL0_DATA seed host name so the hero never shows a bare "on ".
const hostName =
hw?.host?.node ||
(typeof window !== 'undefined' && window.HAL0_DATA?.host?.name) ||
null;

// Reconcile raw layout with live slots
const rawLayout = layoutQuery.data;
const layout = useMemo(() => {
Expand Down Expand Up @@ -630,16 +622,9 @@ function DashboardOverhaulView({ editing: editingProp, onToggleEdit }) {
// padding from the app shell); every route view uses it. Keep it so the
// overhaul board inherits the same chrome as the old DashboardView.
<div className="view dash-overhaul-view">
{/* Hero strip — handoff copy: "Welcome back, halo. system steady on
<host>". `hero-strip` class kept alongside `dash-hero` so shell +
existing hero specs still target it. */}
<div className="dash-hero hero-strip">
<span className="dash-hero-greeting">
Welcome back, halo. system steady{hostName ? ` on ` : ''}
{hostName ? <span className="mono">{hostName}</span> : ''}
</span>
<span className="dash-hero-spacer" />
<span className="dash-hero-meta mono">{heroMeta}</span>
<span className="dash-hero-spacer" />
<button
className={'dash-customize-btn' + (editing ? ' active' : '')}
onClick={toggleEdit}
Expand Down
Loading
Loading