Merge origin/main into running Command Center

Keep the deployed tree on conflict; integrate the remote Dockhand/CDC tip.
This commit is contained in:
mo
2026-07-21 23:21:23 +00:00
7 changed files with 169 additions and 48 deletions
@@ -21,6 +21,12 @@ const SOURCE_COLOR: Record<string, string> = {
const SOURCES = ['all', 'postgres', 'mysql', 'mongodb', 'cassandra', 'neo4j']
const OPS = ['all', 'insert', 'update', 'delete']
const TIME_WINDOWS = [
{ label: '15 min', minutes: 15 },
{ label: '1 hour', minutes: 60 },
{ label: '6 hours', minutes: 360 },
{ label: '24 hours', minutes: 1440 },
] as const
function opOf(o: string) {
return OP_STYLE[o] || { label: o.toUpperCase(), cls: 'bg-slate-500/15 text-slate-300 border-slate-500/30', color: '#94a3b8' }
@@ -363,6 +369,23 @@ export function ChangesView({ liveChanges }: { liveChanges: CdcChange[] }) {
</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1 rounded border border-border/60 p-0.5">
{TIME_WINDOWS.map((w) => (
<button
key={w.minutes}
type="button"
onClick={() => setWindowMin(w.minutes)}
className={cn(
'rounded px-2 py-0.5 text-[10px]',
windowMin === w.minutes
? 'bg-docker/20 text-docker'
: 'text-foreground-muted hover:text-foreground',
)}
>
{w.label}
</button>
))}
</div>
<span className={cn('flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium',
connected ? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-300' : 'border-rose-500/40 bg-rose-500/10 text-rose-300')}>
<Radio className={cn('h-3 w-3', connected && 'animate-pulse')} /> {connected ? 'STREAMING' : 'OFFLINE'}
+5 -4
View File
@@ -150,15 +150,16 @@ export async function fetchPresentation(): Promise<PresentationData | null> {
return fetchJson<PresentationData>('/api/presentation', 60000)
}
export async function fetchChanges(opts: { source?: string; op?: string; limit?: number } = {}) {
export async function fetchChanges(opts: { source?: string; op?: string; limit?: number; minutes?: number } = {}) {
const p = new URLSearchParams()
if (opts.source) p.set('source', opts.source)
if (opts.op) p.set('op', opts.op)
p.set('limit', String(opts.limit ?? 150))
const j = await fetchJson<{ changes?: CdcChange[]; connected?: boolean; consumed?: number }>(
p.set('limit', String(opts.limit ?? 200))
if (opts.minutes) p.set('minutes', String(opts.minutes))
const j = await fetchJson<{ changes?: CdcChange[]; connected?: boolean; consumed?: number; last_ts?: string | null }>(
`/api/changes?${p.toString()}`, 8000,
)
return { changes: j?.changes || [], connected: !!j?.connected, consumed: j?.consumed || 0 }
return { changes: j?.changes || [], connected: !!j?.connected, consumed: j?.consumed || 0, last_ts: j?.last_ts }
}
export async function fetchChangeStats(minutes = 15): Promise<CdcStats | null> {
+7
View File
@@ -62,12 +62,19 @@ export type CdcStats = {
ok: boolean
window_minutes: number
total: number
inserts?: number
updates?: number
deletes?: number
rate_per_min?: number
by_source: Record<string, number>
by_op: Record<string, number>
by_table: Record<string, number>
buckets: { t: string; n: number }[]
connected: boolean
consumed: number
buffered?: number
buffer_cap?: number
last_ts?: string | null
}
export type PiiColumn = { name: string; category: string; masked: boolean; policy_locked?: boolean }