feat: active config snapshot, composite StatusDot with tooltip
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 43s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

Part 1 — Config snapshot:
- V8 migration adds resolved_config JSONB to deployments table
- DeploymentExecutor saves the full resolved config at deploy time
- Deployment record includes resolvedConfig for auditability

Part 2 — Composite health StatusDot:
- CatalogController computes composite health from deployment status +
  agent health (green only when RUNNING AND agent live)
- CatalogApp includes healthTooltip (e.g. "Deployment: RUNNING,
  Agents: live (1 connected)")
- StatusDot added to app detail header with deployment status Badge
- StatusDot added to deployment table rows
- Sidebar passes composite health + tooltip through to tree nodes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 08:00:54 +02:00
parent 7b822a787a
commit 2df5e0d7ba
10 changed files with 113 additions and 14 deletions

View File

@@ -29,7 +29,8 @@ export interface CatalogApp {
displayName: string;
managed: boolean;
environmentSlug: string;
health: 'live' | 'stale' | 'dead' | 'offline';
health: 'live' | 'stale' | 'dead' | 'offline' | 'running' | 'error';
healthTooltip: string;
agentCount: number;
routes: CatalogRoute[];
agents: CatalogAgent[];

View File

@@ -403,7 +403,8 @@ function LayoutContent() {
.map((app: any) => ({
id: app.slug,
name: app.displayName || app.slug,
health: (app.health === 'offline' ? 'dead' : app.health) as 'live' | 'stale' | 'dead',
health: (app.health === 'offline' ? 'dead' : app.health) as SidebarApp['health'],
healthTooltip: app.healthTooltip,
exchangeCount: app.exchangeCount,
routes: [...(app.routes || [])]
.sort((a: any, b: any) => cmp(a.routeId, b.routeId))

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import { createElement, type ReactNode } from 'react';
import type { SidebarTreeNode } from '@cameleer/design-system';
/* ------------------------------------------------------------------ */
@@ -15,7 +15,8 @@ export interface SidebarRoute {
export interface SidebarApp {
id: string;
name: string;
health: 'live' | 'stale' | 'dead';
health: 'live' | 'stale' | 'dead' | 'running' | 'error';
healthTooltip?: string;
exchangeCount: number;
routes: SidebarRoute[];
}
@@ -70,7 +71,9 @@ export function buildAppTreeNodes(
return apps.map((app) => ({
id: app.id,
label: app.name,
icon: statusDot(app.health),
icon: app.healthTooltip
? createElement('span', { title: app.healthTooltip }, statusDot(app.health))
: statusDot(app.health),
badge: formatCount(app.exchangeCount),
path: `/exchanges/${app.id}`,
starrable: true,

View File

@@ -10,6 +10,7 @@ import {
SectionHeader,
Select,
Spinner,
StatusDot,
Toggle,
useToast,
} from '@cameleer/design-system';
@@ -58,6 +59,11 @@ const STATUS_COLORS: Record<string, 'success' | 'warning' | 'error' | 'auto' | '
DEGRADED: 'warning', STOPPING: 'auto',
};
const DEPLOY_STATUS_DOT: Record<string, string> = {
RUNNING: 'live', STARTING: 'running', DEGRADED: 'stale',
STOPPING: 'stale', STOPPED: 'dead', FAILED: 'error',
};
function slugify(name: string): string {
return name
.toLowerCase()
@@ -481,6 +487,8 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s
const navigate = useNavigate();
const { data: allApps = [] } = useAllApps();
const app = useMemo(() => allApps.find((a) => a.slug === appSlug), [allApps, appSlug]);
const { data: catalogApps } = useCatalog(selectedEnv);
const catalogEntry = useMemo(() => (catalogApps ?? []).find((c: CatalogApp) => c.slug === appSlug), [catalogApps, appSlug]);
const { data: versions = [] } = useAppVersions(appSlug);
const { data: deployments = [] } = useDeployments(appSlug);
const uploadJar = useUploadJar();
@@ -534,9 +542,19 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s
<div className={styles.container}>
<div className={styles.detailHeader}>
<div>
<h2 className={styles.detailTitle}>{app.displayName}</h2>
<h2 className={styles.detailTitle}>
{catalogEntry && (
<span title={catalogEntry.healthTooltip} style={{ marginRight: 6, verticalAlign: 'middle' }}>
<StatusDot variant={catalogEntry.health === 'offline' ? 'dead' : catalogEntry.health as any} />
</span>
)}
{app.displayName}
</h2>
<div className={styles.detailMeta}>
{app.slug} &middot; <Badge label={env?.displayName ?? '?'} color="auto" />
{catalogEntry?.deployment && (
<> &middot; <Badge label={catalogEntry.deployment.status} color={STATUS_COLORS[catalogEntry.deployment.status as keyof typeof STATUS_COLORS] ?? 'auto'} /></>
)}
</div>
</div>
<div className={styles.detailActions}>
@@ -626,7 +644,10 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
<Badge label={dEnv?.displayName ?? '?'} color={dEnv?.production ? 'error' : 'auto'} />
</td>
<td><Badge label={version ? `v${version.version}` : '?'} color="auto" /></td>
<td><Badge label={d.status} color={STATUS_COLORS[d.status] ?? 'auto'} /></td>
<td style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<StatusDot variant={DEPLOY_STATUS_DOT[d.status] ?? 'dead'} />
<Badge label={d.status} color={STATUS_COLORS[d.status] ?? 'auto'} />
</td>
<td>
{d.replicaStates && d.replicaStates.length > 0 ? (
<span className={styles.cellMeta}>