diff --git a/ui/package-lock.json b/ui/package-lock.json
index 5726bb4a..38a158fe 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.0",
"hasInstallScript": true,
"dependencies": {
- "@cameleer/design-system": "^0.1.50",
+ "@cameleer/design-system": "^0.1.52",
"@tanstack/react-query": "^5.90.21",
"js-yaml": "^4.1.1",
"lucide-react": "^1.7.0",
@@ -281,9 +281,9 @@
}
},
"node_modules/@cameleer/design-system": {
- "version": "0.1.51",
- "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.51/design-system-0.1.51.tgz",
- "integrity": "sha512-ppZSiR6ZzzrUbtHTtnwpU4Zr2LPbcbJfAn0Ayh/OzDf9k6kFjn5myJWFlg+VJAZkFQoJA5y76GcKBdJ8nty4Tw==",
+ "version": "0.1.52",
+ "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.52/design-system-0.1.52.tgz",
+ "integrity": "sha512-yhxZodFoqGbucNdjRtnlmTt+SI3csv0+nOf8nvD6hmsOjj0WhaqMjdj+hqPpc6EZu3UVEWjfeX+9d/1B7cyy0A==",
"dependencies": {
"lucide-react": "^1.7.0",
"react": "^19.0.0",
diff --git a/ui/package.json b/ui/package.json
index 0fbf4dfe..93d6ec44 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -15,7 +15,7 @@
"postinstall": "node -e \"const fs=require('fs');fs.mkdirSync('public',{recursive:true});fs.copyFileSync('node_modules/@cameleer/design-system/assets/cameleer-logo.svg','public/favicon.svg')\""
},
"dependencies": {
- "@cameleer/design-system": "^0.1.50",
+ "@cameleer/design-system": "^0.1.52",
"@tanstack/react-query": "^5.90.21",
"js-yaml": "^4.1.1",
"lucide-react": "^1.7.0",
diff --git a/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx b/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx
index 853344e4..ea6a45d6 100644
--- a/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx
+++ b/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx
@@ -1,7 +1,7 @@
import { Badge } from '@cameleer/design-system';
import type { ProcessorNode, ExecutionDetail } from '../types';
import { attributeBadgeColor } from '../../../utils/attribute-color';
-import { formatDurationShort } from '../../../utils/format-utils';
+import { formatDurationShort, statusLabel } from '../../../utils/format-utils';
import styles from '../ExecutionDiagram.module.css';
interface InfoTabProps {
@@ -13,11 +13,14 @@ function formatTime(iso: string | undefined): string {
if (!iso) return '-';
try {
const d = new Date(iso);
+ const y = d.getFullYear();
+ const mo = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
const ms = String(d.getMilliseconds()).padStart(3, '0');
- return `${h}:${m}:${s}.${ms}`;
+ return `${y}-${mo}-${day} ${h}:${m}:${s}.${ms}`;
} catch {
return iso;
}
@@ -66,7 +69,7 @@ export function InfoTab({ processor, executionDetail }: InfoTabProps) {
Status
- {processor.status}
+ {statusLabel(processor.status)}
@@ -96,7 +99,7 @@ export function InfoTab({ processor, executionDetail }: InfoTabProps) {
Status
- {executionDetail.status}
+ {statusLabel(executionDetail.status)}
diff --git a/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx b/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx
index 79b28189..4f6e7022 100644
--- a/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx
+++ b/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx
@@ -2,7 +2,8 @@ import { useState, useMemo } from 'react';
import { useNavigate } from 'react-router';
import { Input, Button, LogViewer } from '@cameleer/design-system';
import type { LogEntry } from '@cameleer/design-system';
-import { useApplicationLogs } from '../../../api/queries/logs';
+import { useLogs } from '../../../api/queries/logs';
+import type { LogEntryResponse } from '../../../api/queries/logs';
import { mapLogLevel } from '../../../utils/agent-utils';
import logStyles from './LogTab.module.css';
import diagramStyles from '../ExecutionDiagram.module.css';
@@ -13,26 +14,33 @@ interface LogTabProps {
processorId: string | null;
}
+function matchesProcessor(e: LogEntryResponse, pid: string): boolean {
+ if (e.message?.includes(pid)) return true;
+ if (e.loggerName?.includes(pid)) return true;
+ if (e.mdc) {
+ for (const v of Object.values(e.mdc)) {
+ if (v === pid) return true;
+ }
+ }
+ return false;
+}
+
export function LogTab({ applicationId, exchangeId, processorId }: LogTabProps) {
const [filter, setFilter] = useState('');
const navigate = useNavigate();
- const { data: logs, isLoading } = useApplicationLogs(
- applicationId,
- undefined,
+ const { data: logPage, isLoading } = useLogs(
{ exchangeId, limit: 500 },
+ { enabled: !!exchangeId },
);
const entries = useMemo(() => {
- if (!logs) return [];
- let items = [...logs];
+ if (!logPage?.data) return [];
+ let items = [...logPage.data];
- // If a processor is selected, filter logs by logger name containing the processor ID
+ // If a processor is selected, filter logs to that processor
if (processorId) {
- items = items.filter((e) =>
- e.message?.includes(processorId) ||
- e.loggerName?.includes(processorId)
- );
+ items = items.filter((e) => matchesProcessor(e, processorId));
}
// Text filter
@@ -50,7 +58,7 @@ export function LogTab({ applicationId, exchangeId, processorId }: LogTabProps)
level: mapLogLevel(e.level),
message: e.message ?? '',
}));
- }, [logs, processorId, filter]);
+ }, [logPage, processorId, filter]);
if (isLoading) {
return Loading logs...
;
diff --git a/ui/src/components/LayoutShell.module.css b/ui/src/components/LayoutShell.module.css
index 4f89366e..aeeee04d 100644
--- a/ui/src/components/LayoutShell.module.css
+++ b/ui/src/components/LayoutShell.module.css
@@ -62,15 +62,57 @@
font-size: 12px;
font-weight: 500;
color: var(--sidebar-muted);
+ opacity: 0.45;
cursor: pointer;
- transition: color 0.12s, background 0.12s;
+ transition: color 0.12s, background 0.12s, opacity 0.12s;
}
.addAppBtn:hover {
+ opacity: 1;
color: var(--amber);
background: rgba(255, 255, 255, 0.06);
}
+.sidebarFilters {
+ display: flex;
+ gap: 6px;
+ padding: 4px 12px 6px;
+}
+
+.filterChip {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: none;
+ border: 1px solid transparent;
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 11px;
+ color: var(--sidebar-muted);
+ cursor: pointer;
+ white-space: nowrap;
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
+ opacity: 0.6;
+}
+
+.filterChip:hover {
+ opacity: 1;
+ color: var(--sidebar-text);
+ border-color: rgba(255, 255, 255, 0.12);
+}
+
+.filterChipActive {
+ opacity: 1;
+ color: var(--amber);
+ border-color: var(--amber);
+ background: rgba(var(--amber-rgb, 245, 158, 11), 0.08);
+}
+
+.filterChipIcon {
+ display: flex;
+ align-items: center;
+}
+
.mainContent {
flex: 1;
display: flex;
diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx
index 99c4f187..b8059c6c 100644
--- a/ui/src/components/LayoutShell.tsx
+++ b/ui/src/components/LayoutShell.tsx
@@ -21,7 +21,7 @@ import {
} from '@cameleer/design-system';
import type { SearchResult, SidebarTreeNode, DropdownItem, ButtonGroupItem, ExchangeStatus } from '@cameleer/design-system';
import sidebarLogo from '@cameleer/design-system/assets/cameleer-logo.svg';
-import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User, Plus } from 'lucide-react';
+import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User, Plus, EyeOff } from 'lucide-react';
import { AboutMeDialog } from './AboutMeDialog';
import css from './LayoutShell.module.css';
import { useQueryClient } from '@tanstack/react-query';
@@ -270,9 +270,9 @@ function StarredList({ items, onNavigate, onRemove }: { items: StarredItem[]; on
/* ------------------------------------------------------------------ */
const STATUS_ITEMS: ButtonGroupItem[] = [
- { value: 'completed', label: 'OK', color: 'var(--success)' },
- { value: 'warning', label: 'Warn', color: 'var(--warning)' },
- { value: 'failed', label: 'Error', color: 'var(--error)' },
+ { value: 'completed', label: 'Completed', color: 'var(--success)' },
+ { value: 'warning', label: 'Warning', color: 'var(--warning)' },
+ { value: 'failed', label: 'Failed', color: 'var(--error)' },
{ value: 'running', label: 'Running', color: 'var(--running)' },
]
@@ -345,6 +345,15 @@ function LayoutContent() {
// --- Sidebar filter -----------------------------------------------
const [filterQuery, setFilterQuery] = useState('');
+ const [hideEmptyRoutes, setHideEmptyRoutes] = useState(() => readCollapsed('sidebar:hideEmptyRoutes', false));
+ const [hideOfflineApps, setHideOfflineApps] = useState(() => readCollapsed('sidebar:hideOfflineApps', false));
+
+ const toggleHideEmptyRoutes = useCallback(() => {
+ setHideEmptyRoutes((prev) => { writeCollapsed('sidebar:hideEmptyRoutes', !prev); return !prev; });
+ }, []);
+ const toggleHideOfflineApps = useCallback(() => {
+ setHideOfflineApps((prev) => { writeCollapsed('sidebar:hideOfflineApps', !prev); return !prev; });
+ }, []);
const setSelectedEnv = useCallback((env: string | undefined) => {
setSelectedEnvRaw(env);
@@ -430,10 +439,27 @@ function LayoutContent() {
}));
}, [catalog]);
+ // --- Apply sidebar filters -----------------------------------------
+ const filteredSidebarApps: SidebarApp[] = useMemo(() => {
+ let apps = sidebarApps;
+ if (hideOfflineApps) {
+ apps = apps.filter((a) => a.health !== 'dead' && a.health !== 'stale');
+ }
+ if (hideEmptyRoutes) {
+ apps = apps
+ .map((a) => ({
+ ...a,
+ routes: a.routes.filter((r) => r.exchangeCount > 0),
+ }))
+ .filter((a) => a.exchangeCount > 0 || a.routes.length > 0);
+ }
+ return apps;
+ }, [sidebarApps, hideOfflineApps, hideEmptyRoutes]);
+
// --- Tree nodes ---------------------------------------------------
const appTreeNodes: SidebarTreeNode[] = useMemo(
- () => buildAppTreeNodes(sidebarApps, makeStatusDot, makeChevron, makeStopIcon, makePauseIcon),
- [sidebarApps],
+ () => buildAppTreeNodes(filteredSidebarApps, makeStatusDot, makeChevron, makeStopIcon, makePauseIcon),
+ [filteredSidebarApps],
);
const adminTreeNodes: SidebarTreeNode[] = useMemo(
@@ -692,9 +718,29 @@ function LayoutContent() {
version={__APP_VERSION__}
/>
+ {/* Sidebar filters */}
+ {!sidebarCollapsed &&
+
+
+
}
+
{/* Applications section */}
- {canControl && (
+ {canControl && !sidebarCollapsed && (
Sampling Rate
- setSamplingRate(e.target.value)} className={styles.inputLg} />
+ setSamplingRate(e.target.value)} className={styles.inputLg} placeholder="1.0" />
Compress Success
@@ -438,6 +459,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
!busy && setRouteControlEnabled(!routeControlEnabled)} disabled={busy} />
{routeControlEnabled ? 'Enabled' : 'Disabled'}
+
)}
@@ -446,23 +468,42 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
Container Resources
+
Runtime Type
+
)}
+
+ {configTab === 'sensitive-keys' && (
+
+
+
+ Agent built-in defaults
+
+
+ {['Authorization', 'Cookie', 'Set-Cookie', 'X-API-Key', 'X-Auth-Token', 'Proxy-Authorization'].map((key) => (
+
+ ))}
+
+
+ {globalKeys.length > 0 && (
+ <>
+
+
+ Global keys (enforced)
+ {globalKeys.length}
+
+
+ {globalKeys.map((key) => (
+
+ ))}
+
+ >
+ )}
+
+
+
+ Application-specific keys
+ {sensitiveKeys.length > 0 && {sensitiveKeys.length}}
+
+
+
+ {sensitiveKeys.map((k, i) => (
+ !busy && setSensitiveKeys(sensitiveKeys.filter((_, idx) => idx !== i))} />
+ ))}
+ {sensitiveKeys.length === 0 && (
+ No app-specific keys — agents use built-in defaults{globalKeys.length > 0 ? ' and global keys' : ''}
+ )}
+
+
+
+ setNewSensitiveKey(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ const v = newSensitiveKey.trim();
+ if (v && !sensitiveKeys.some((k) => k.toLowerCase() === v.toLowerCase())) {
+ setSensitiveKeys([...sensitiveKeys, v]);
+ setNewSensitiveKey('');
+ }
+ }
+ }}
+ placeholder="Add key or glob pattern (e.g. *password*)"
+ disabled={busy}
+ />
+
+
+
+
+
+
+ The final masking configuration is: agent defaults + global keys + app-specific keys.
+ Supports exact header names and glob patterns.
+
+
+
+ )}
);
}
diff --git a/ui/src/pages/Dashboard/Dashboard.tsx b/ui/src/pages/Dashboard/Dashboard.tsx
index 38f9583c..65ec6ba7 100644
--- a/ui/src/pages/Dashboard/Dashboard.tsx
+++ b/ui/src/pages/Dashboard/Dashboard.tsx
@@ -54,12 +54,6 @@ function durationClass(ms: number, status: string): string {
return styles.durBreach
}
-function shortAgentName(name: string): string {
- const parts = name.split('-')
- if (parts.length >= 3) return parts.slice(-2).join('-')
- return name
-}
-
// ─── Table columns ────────────────────────────────────────────────────────────
function buildColumns(hasAttributes: boolean): Column[] {
@@ -77,22 +71,6 @@ function buildColumns(hasAttributes: boolean): Column[] {
),
},
- {
- key: 'routeId',
- header: 'Route',
- sortable: true,
- render: (_: unknown, row: Row) => (
- {row.routeId}
- ),
- },
- {
- key: 'applicationId',
- header: 'Application',
- sortable: true,
- render: (_: unknown, row: Row) => (
- {row.applicationId ?? ''}
- ),
- },
...(hasAttributes ? [{
key: 'attributes' as const,
header: 'Attributes',
@@ -115,20 +93,19 @@ function buildColumns(hasAttributes: boolean): Column[] {
},
}] : []),
{
- key: 'executionId',
- header: 'Exchange ID',
+ key: 'applicationId',
+ header: 'App',
sortable: true,
render: (_: unknown, row: Row) => (
- {
- e.stopPropagation();
- navigator.clipboard.writeText(row.executionId);
- }}
- >
- ...{row.executionId.slice(-8)}
-
+ {row.applicationId ?? ''}
+ ),
+ },
+ {
+ key: 'routeId',
+ header: 'Route',
+ sortable: true,
+ render: (_: unknown, row: Row) => (
+ {row.routeId}
),
},
{
@@ -149,16 +126,6 @@ function buildColumns(hasAttributes: boolean): Column[] {
),
},
- {
- key: 'instanceId',
- header: 'Agent',
- render: (_: unknown, row: Row) => (
-
-
- {shortAgentName(row.instanceId)}
-
- ),
- },
]
}
diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx
index ee9a64ca..fd98a57e 100644
--- a/ui/src/pages/Routes/RouteDetail.tsx
+++ b/ui/src/pages/Routes/RouteDetail.tsx
@@ -40,6 +40,7 @@ import type { TapDefinition } from '../../api/queries/commands';
import type { ExecutionSummary } from '../../api/types';
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
import { buildFlowSegments } from '../../utils/diagram-mapping';
+import { statusLabel } from '../../utils/format-utils';
import styles from './RouteDetail.module.css';
import tableStyles from '../../styles/table-section.module.css';
import rateStyles from '../../styles/rate-colors.module.css';
@@ -658,7 +659,7 @@ export default function RouteDetail() {
const recentExchangeOptions = useMemo(() =>
exchangeRows.slice(0, 20).map(e => ({
value: e.executionId,
- label: `${e.executionId.slice(0, 12)} — ${e.status}`,
+ label: `${e.executionId.slice(0, 12)} — ${statusLabel(e.status)}`,
})),
[exchangeRows],
);
diff --git a/ui/src/utils/format-utils.ts b/ui/src/utils/format-utils.ts
index 8e7a5a84..4d3de0a6 100644
--- a/ui/src/utils/format-utils.ts
+++ b/ui/src/utils/format-utils.ts
@@ -20,12 +20,8 @@ export function formatDurationShort(ms: number | undefined): string {
}
export function statusLabel(s: string): string {
- switch (s) {
- case 'COMPLETED': return 'OK';
- case 'FAILED': return 'ERR';
- case 'RUNNING': return 'RUN';
- default: return s;
- }
+ if (!s) return s;
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
}
export function timeAgo(iso?: string): string {