feat(observability): event-triggers + log-scan-rules UI + i18n
Operator-facing surfaces for the two backend features:
- /event-triggers — list (filter summary, status pill),
/event-triggers/new (form with regex validation), and
/event-triggers/[id] (edit + Send-test + delete) with
CONFIGURED secret badge + clear-to-rotate flow, ConfirmDialog
for delete, aria-live regions on async result slots.
- /log-scan-rules — list with scope filter chips and stats panel
(active tails, RATE-LIMITED, COOLED DOWN, COMPILE ERRORS),
/log-scan-rules/new (with EntityPicker for workload scope and
inline RegexTestBox), /log-scan-rules/[id] (edit + server-side
/test + delete + live RegexTestBox panel).
- web/src/lib/components/RegexTestBox.svelte — reusable
client-side regex test with sample input + captures display.
- web/src/lib/api.ts — typed wrappers for EventTrigger and
LogScanRule CRUD + /test + getLogScanStats +
getEffectiveLogScanRules.
- web/src/routes/+layout.svelte — nav entries for both surfaces.
- web/src/lib/i18n/{en,ru}.json — ~90 keys under observability.*,
triggers.*, logscan.* namespaces; Russian translations cover
every key.
Design + a11y polish per a frontend-design review pass: all
boolean inputs use ToggleSwitch, all destructive actions use
ConfirmDialog with confirmVariant="danger" / onconfirm /
oncancel, hand-rolled .btn-primary replaced with global
forge-btn classes, hex colors replaced with var(--*) tokens,
role="alert" on error banners, aria-invalid + aria-describedby
on invalid-regex inputs, aria-busy on async forms, mobile
breakpoints (hide-md columns, .row.three collapsing 3→2→1,
.table-wrap overflow-x).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1078,6 +1078,141 @@ export function setWorkloadAppID(id: string, appID: string): Promise<Workload> {
|
||||
return patch<Workload>(`/api/workloads/${id}/app`, { app_id: appID });
|
||||
}
|
||||
|
||||
export function createPluginWorkload(body: import('./types').PluginWorkloadInput): Promise<Workload> {
|
||||
return post<Workload>('/api/workloads', body);
|
||||
}
|
||||
|
||||
export function updatePluginWorkload(id: string, body: import('./types').PluginWorkloadInput): Promise<Workload> {
|
||||
return put<Workload>(`/api/workloads/${id}/plugin`, body);
|
||||
}
|
||||
|
||||
export function deployPluginWorkload(
|
||||
id: string,
|
||||
body?: { reference?: string; note?: string }
|
||||
): Promise<{ workload_id: string; reference: string; triggered_by: string }> {
|
||||
return post(`/api/workloads/${id}/deploy`, body ?? {});
|
||||
}
|
||||
|
||||
export function listHookKinds(signal?: AbortSignal): Promise<import('./types').HookKinds> {
|
||||
return get<import('./types').HookKinds>('/api/hooks/kinds', signal);
|
||||
}
|
||||
|
||||
export function deletePluginWorkload(id: string): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/workloads/${id}`);
|
||||
}
|
||||
|
||||
export interface WorkloadEnv {
|
||||
id: string;
|
||||
workload_id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
encrypted: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export function listWorkloadEnv(id: string, signal?: AbortSignal): Promise<WorkloadEnv[]> {
|
||||
return get<WorkloadEnv[]>(`/api/workloads/${id}/env`, signal);
|
||||
}
|
||||
|
||||
export function setWorkloadEnv(
|
||||
id: string,
|
||||
body: { key: string; value: string; encrypted: boolean }
|
||||
): Promise<WorkloadEnv> {
|
||||
return put<WorkloadEnv>(`/api/workloads/${id}/env`, body);
|
||||
}
|
||||
|
||||
export function deleteWorkloadEnv(id: string, envID: string): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/workloads/${id}/env/${envID}`);
|
||||
}
|
||||
|
||||
export interface WorkloadWebhook {
|
||||
webhook_url: string;
|
||||
webhook_secret: string;
|
||||
has_signing_secret: boolean;
|
||||
webhook_require_signature: boolean;
|
||||
}
|
||||
|
||||
export function getWorkloadWebhook(id: string, signal?: AbortSignal): Promise<WorkloadWebhook> {
|
||||
return get<WorkloadWebhook>(`/api/workloads/${id}/webhook`, signal);
|
||||
}
|
||||
|
||||
export function regenerateWorkloadWebhook(id: string): Promise<WorkloadWebhook> {
|
||||
return post<WorkloadWebhook>(`/api/workloads/${id}/webhook/regenerate`);
|
||||
}
|
||||
|
||||
export function fetchWorkloadContainerLogs(
|
||||
workloadId: string,
|
||||
containerRowId: string,
|
||||
tail: number
|
||||
): Promise<string[]> {
|
||||
return get<string[]>(
|
||||
`/api/workloads/${workloadId}/containers/${containerRowId}/logs?tail=${tail}`
|
||||
);
|
||||
}
|
||||
|
||||
export interface WorkloadVolume {
|
||||
id: string;
|
||||
workload_id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
scope: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export function listWorkloadVolumes(id: string, signal?: AbortSignal): Promise<WorkloadVolume[]> {
|
||||
return get<WorkloadVolume[]>(`/api/workloads/${id}/volumes`, signal);
|
||||
}
|
||||
|
||||
export function setWorkloadVolume(
|
||||
id: string,
|
||||
body: { source: string; target: string; scope: string; name?: string }
|
||||
): Promise<WorkloadVolume> {
|
||||
return put<WorkloadVolume>(`/api/workloads/${id}/volumes`, body);
|
||||
}
|
||||
|
||||
export function deleteWorkloadVolume(id: string, volID: string): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/workloads/${id}/volumes/${volID}`);
|
||||
}
|
||||
|
||||
export interface HookKindSchema {
|
||||
kind: string;
|
||||
sample: unknown;
|
||||
}
|
||||
|
||||
export function getHookKindSchema(kind: string, signal?: AbortSignal): Promise<HookKindSchema> {
|
||||
return get<HookKindSchema>(`/api/hooks/kinds/${kind}/schema`, signal);
|
||||
}
|
||||
|
||||
export interface WorkloadChainNode {
|
||||
id: string;
|
||||
name: string;
|
||||
source_kind: string;
|
||||
trigger_kind: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface WorkloadChain {
|
||||
parent: WorkloadChainNode | null;
|
||||
self: WorkloadChainNode;
|
||||
children: WorkloadChainNode[];
|
||||
}
|
||||
|
||||
export function getWorkloadChain(id: string, signal?: AbortSignal): Promise<WorkloadChain> {
|
||||
return get<WorkloadChain>(`/api/workloads/${id}/chain`, signal);
|
||||
}
|
||||
|
||||
export function promoteFromWorkload(
|
||||
targetID: string,
|
||||
sourceID: string,
|
||||
body?: { image_tag?: string; deploy?: boolean }
|
||||
): Promise<{ workload_id: string; source_id: string; promoted_tag: string; deploy_queued: boolean }> {
|
||||
return post(`/api/workloads/${targetID}/promote-from/${sourceID}`, body ?? {});
|
||||
}
|
||||
|
||||
// ── Containers (global index) ───────────────────────────────────────
|
||||
|
||||
export interface ListContainersFilter {
|
||||
@@ -1121,4 +1256,141 @@ export function deleteApp(id: string): Promise<void> {
|
||||
return del<void>(`/api/apps/${id}`);
|
||||
}
|
||||
|
||||
// ── Event Triggers ──────────────────────────────────────────────────
|
||||
// Backend: internal/api/event_triggers.go. AND-composed filter shape;
|
||||
// empty filter fields mean "match any value." The dispatcher fans
|
||||
// matching event_log entries out to action_target via signed webhook.
|
||||
|
||||
export interface EventTrigger {
|
||||
id: number;
|
||||
name: string;
|
||||
filter_severity: string; // CSV; "" = any
|
||||
filter_source: string; // CSV; "" = any
|
||||
filter_message_regex: string; // "" = any
|
||||
action_type: string; // 'webhook' today
|
||||
action_target: string; // URL
|
||||
action_secret: string; // optional HMAC secret
|
||||
enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface EventTriggerInput {
|
||||
name: string;
|
||||
filter_severity?: string;
|
||||
filter_source?: string;
|
||||
filter_message_regex?: string;
|
||||
action_type?: string;
|
||||
action_target: string;
|
||||
action_secret?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function listEventTriggers(signal?: AbortSignal): Promise<EventTrigger[]> {
|
||||
return get<EventTrigger[]>('/api/event-triggers', signal);
|
||||
}
|
||||
|
||||
export function getEventTrigger(id: number, signal?: AbortSignal): Promise<EventTrigger> {
|
||||
return get<EventTrigger>(`/api/event-triggers/${id}`, signal);
|
||||
}
|
||||
|
||||
export function createEventTrigger(data: EventTriggerInput): Promise<EventTrigger> {
|
||||
return post<EventTrigger>('/api/event-triggers', data);
|
||||
}
|
||||
|
||||
export function updateEventTrigger(id: number, data: EventTriggerInput): Promise<EventTrigger> {
|
||||
return patch<EventTrigger>(`/api/event-triggers/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteEventTrigger(id: number): Promise<void> {
|
||||
return del<void>(`/api/event-triggers/${id}`);
|
||||
}
|
||||
|
||||
export function testEventTrigger(id: number): Promise<NotificationTestResult> {
|
||||
return post<NotificationTestResult>(`/api/event-triggers/${id}/test`);
|
||||
}
|
||||
|
||||
// ── Log scan rules ──────────────────────────────────────────────────
|
||||
// Backend: internal/api/log_scan_rules.go. Rules are regex patterns
|
||||
// the scanner manager evaluates against container log lines. Scope
|
||||
// model: workload_id="" + overrides_id=0 → global; workload_id set →
|
||||
// workload-only (or per-workload override of a global via
|
||||
// overrides_id).
|
||||
|
||||
export interface LogScanRule {
|
||||
id: number;
|
||||
workload_id: string; // "" = global
|
||||
overrides_id: number; // 0 = not an override
|
||||
name: string;
|
||||
pattern: string;
|
||||
severity: 'info' | 'warn' | 'error';
|
||||
streams: 'all' | 'stdout' | 'stderr';
|
||||
cooldown_seconds: number;
|
||||
enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface LogScanRuleInput {
|
||||
workload_id?: string;
|
||||
overrides_id?: number;
|
||||
name: string;
|
||||
pattern: string;
|
||||
severity?: 'info' | 'warn' | 'error';
|
||||
streams?: 'all' | 'stdout' | 'stderr';
|
||||
cooldown_seconds?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface LogScanTestResult {
|
||||
matched: boolean;
|
||||
captures?: Record<string, string>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function listLogScanRules(opts?: {
|
||||
workloadID?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<LogScanRule[]> {
|
||||
const params = opts?.workloadID ? `?workload_id=${encodeURIComponent(opts.workloadID)}` : '';
|
||||
return get<LogScanRule[]>(`/api/log-scan-rules${params}`, opts?.signal);
|
||||
}
|
||||
|
||||
export function getLogScanRule(id: number, signal?: AbortSignal): Promise<LogScanRule> {
|
||||
return get<LogScanRule>(`/api/log-scan-rules/${id}`, signal);
|
||||
}
|
||||
|
||||
export function createLogScanRule(data: LogScanRuleInput): Promise<LogScanRule> {
|
||||
return post<LogScanRule>('/api/log-scan-rules', data);
|
||||
}
|
||||
|
||||
export function updateLogScanRule(id: number, data: LogScanRuleInput): Promise<LogScanRule> {
|
||||
return patch<LogScanRule>(`/api/log-scan-rules/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteLogScanRule(id: number): Promise<void> {
|
||||
return del<void>(`/api/log-scan-rules/${id}`);
|
||||
}
|
||||
|
||||
export function testLogScanRule(id: number, sampleLine: string): Promise<LogScanTestResult> {
|
||||
return post<LogScanTestResult>(`/api/log-scan-rules/${id}/test`, { sample_line: sampleLine });
|
||||
}
|
||||
|
||||
export function getEffectiveLogScanRules(workloadID: string, signal?: AbortSignal): Promise<LogScanRule[]> {
|
||||
return get<LogScanRule[]>(`/api/workloads/${workloadID}/effective-rules`, signal);
|
||||
}
|
||||
|
||||
export interface LogScanStats {
|
||||
engine: {
|
||||
dropped_by_bucket: number;
|
||||
dropped_by_cooldown: number;
|
||||
};
|
||||
active_tails: number;
|
||||
last_compile_errors: string[];
|
||||
}
|
||||
|
||||
export function getLogScanStats(signal?: AbortSignal): Promise<LogScanStats> {
|
||||
return get<LogScanStats>('/api/log-scan-rules/stats', signal);
|
||||
}
|
||||
|
||||
export { ApiError };
|
||||
|
||||
Reference in New Issue
Block a user