Files
tiny-forge/web/src/lib/api.ts
T
alexei.dolgolyov 2aff22f565
Build / build (push) Successful in 10m39s
feat(triggers): first-class triggers + bindings with fan-out webhook
Promote triggers from embedded workload fields to standalone records
joined to workloads via workload_trigger_bindings. One trigger (webhook,
registry watcher, git push, manual) now fans out to many workloads with
per-binding config overrides (top-level JSON merge, binding wins).

Backend
- new triggers + workload_trigger_bindings tables with ON DELETE CASCADE
- boot-time backfill of embedded trigger config inside per-workload tx
- store.ErrUnique sentinel translates SQLite UNIQUE at store boundary
- /api/triggers CRUD + /api/triggers/{id}/{webhook,bindings}
- /api/bindings/{id} update/delete; /api/workloads/{id}/triggers list+bind
- bindTriggerToWorkload accepts trigger_id or inline {kind,name,config}
- inline-create uses CreateTriggerWithBindingTx (no orphan triggers)
- validateBindingConfig enforces 8 KiB cap + plugin Validate on merged
- ListTriggersWithBindingCount + ListBindings*WithNames remove N+1
- POST /api/webhook/triggers/{secret} resolves trigger then fans out
- bounded worker pool (4) per request; per-binding error isolation
- outcome accounting: deployed / skipped / no-match / errored
- legacy /api/webhook/workloads/{secret} route removed (clean break;
  backfill keeps secrets resolvable at the new /triggers/{secret} path)
- reconciler gate dropped from (Source && Trigger) to Source only
- MergeJSONConfig returns freshly allocated slices (no fan-out aliasing)
- WithEffectiveTrigger lets existing Trigger.Match contract stay unchanged

Frontend
- /triggers list, new wizard, [id] detail (bindings, webhook rotate)
- workload create wizard: NEW / PICK / SKIP trigger modes
- workload detail: bindings panel + Add-trigger modal (inline / pick)
- per-binding override editor with merged-preview + 8 KiB guard
- "OVERRIDES n FIELDS" row badge when binding_config is non-empty
- shared TriggerKindForm component (registry / git / manual + JSON)
- 3 raw <input type=checkbox> replaced with <ToggleSwitch>
- full EN + RU i18n: redeployTriggers.*, apps.detail.bindings.*,
  apps.new.triggers.*, nav.triggers; event-triggers nav disambiguated

Doc
- WORKLOAD_REFACTOR_TODO: trigger-split marked DONE; next focus is
  the static-source inline port + hard legacy cutover (Priority 1)
2026-05-16 02:24:31 +03:00

1519 lines
51 KiB
TypeScript

import type {
ApiEnvelope,
App,
Container,
ContainerStats,
ContainerStatsSample,
ContainerView,
SystemStats,
SystemStatsSample,
TopContainerSample,
Deploy,
DeployLog,
DockerHealth,
ProxyHealth,
EventLogEntry,
EventLogStats,
InspectResult,
Instance,
LocalImage,
NpmCertificate,
NpmAccessList,
ProxyRoute,
Project,
ProjectDetail,
Registry,
RegistryImage,
Settings,
StaleContainer,
Stage,
StageEnv,
Volume,
VolumeScopeInfo,
BrowseResult,
DnsZone,
DnsRecordView,
BackupInfo,
Workload,
WorkloadKind
} from './types';
// ── Helpers ─────────────────────────────────────────────────────────
class ApiError extends Error {
constructor(
message: string,
public readonly status: number
) {
super(message);
this.name = 'ApiError';
}
}
import { getAuthToken, clearAuth } from './auth';
// ── Concurrency limiter ────────────────────────────────────────────
// Chrome allows only 6 concurrent HTTP/1.1 connections per host.
// Reserve slots for the persistent SSE stream and health checks by
// capping regular API requests to 4 concurrent.
const MAX_CONCURRENT = 4;
let inflight = 0;
const queue: Array<() => void> = [];
function acquireSlot(signal?: AbortSignal | null): Promise<void> {
if (inflight < MAX_CONCURRENT) {
inflight++;
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
// A queued waiter inherits the releasing request's slot, so it
// must not increment `inflight` again — `releaseSlot` skips the
// decrement when it hands the slot off, keeping the count stable.
const entry = () => { resolve(); };
queue.push(entry);
signal?.addEventListener('abort', () => {
const idx = queue.indexOf(entry);
if (idx !== -1) {
queue.splice(idx, 1);
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
}
}, { once: true });
});
}
function releaseSlot(): void {
const next = queue.shift();
if (next) {
next();
} else {
inflight--;
}
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
// Write operations (user-initiated) bypass the concurrency limiter
// so they are never blocked behind background polling.
const method = init?.method?.toUpperCase() ?? 'GET';
if (method !== 'GET') {
return requestInner<T>(path, init);
}
await acquireSlot(init?.signal);
try {
return await requestInner<T>(path, init);
} finally {
releaseSlot();
}
}
async function requestInner<T>(path: string, init?: RequestInit): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(init?.headers as Record<string, string>)
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(path, {
...init,
headers
});
// Redirect to login on 401 (expired/missing token).
if (res.status === 401 && typeof window !== 'undefined' && !path.includes('/auth/')) {
clearAuth();
window.location.href = '/login';
throw new ApiError('Authentication required', 401);
}
let envelope: ApiEnvelope<T>;
try {
envelope = await res.json();
} catch {
throw new ApiError(
`Server returned non-JSON response (HTTP ${res.status})`,
res.status
);
}
if (!envelope.success) {
throw new ApiError(envelope.error ?? 'Unknown API error', res.status);
}
return envelope.data as T;
}
function get<T>(path: string, signal?: AbortSignal): Promise<T> {
return request<T>(path, signal ? { signal } : undefined);
}
function post<T>(path: string, body?: unknown): Promise<T> {
return request<T>(path, {
method: 'POST',
body: body !== undefined ? JSON.stringify(body) : undefined
});
}
function put<T>(path: string, body: unknown): Promise<T> {
return request<T>(path, {
method: 'PUT',
body: JSON.stringify(body)
});
}
function del<T>(path: string): Promise<T> {
return request<T>(path, { method: 'DELETE' });
}
function patch<T>(path: string, body: unknown): Promise<T> {
return request<T>(path, {
method: 'PATCH',
body: JSON.stringify(body)
});
}
// ── Projects ────────────────────────────────────────────────────────
export function listProjects(signal?: AbortSignal): Promise<Project[]> {
return get<Project[]>('/api/projects', signal);
}
export function getProject(id: string, signal?: AbortSignal): Promise<ProjectDetail> {
return get<ProjectDetail>(`/api/projects/${id}`, signal);
}
export function createProject(data: Partial<Project>): Promise<Project> {
return post<Project>('/api/projects', data);
}
export function updateProject(id: string, data: Partial<Project>): Promise<Project> {
return put<Project>(`/api/projects/${id}`, data);
}
export function deleteProject(id: string): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/projects/${id}`);
}
// ── Stages ─────────────────────────────────────────────────────────
export function createStage(projectId: string, data: Partial<Stage>): Promise<Stage> {
return post<Stage>(`/api/projects/${projectId}/stages`, data);
}
export function updateStage(projectId: string, stageId: string, data: Partial<Stage>): Promise<Stage> {
return put<Stage>(`/api/projects/${projectId}/stages/${stageId}`, data);
}
export function deleteStage(projectId: string, stageId: string): Promise<void> {
return del<void>(`/api/projects/${projectId}/stages/${stageId}`);
}
// ── Instances ───────────────────────────────────────────────────────
export function listInstances(projectId: string, stageId: string, signal?: AbortSignal): Promise<Instance[]> {
return get<Instance[]>(`/api/projects/${projectId}/stages/${stageId}/instances`, signal);
}
export function deployInstance(
projectId: string,
stageId: string,
imageTag: string
): Promise<{ status: string }> {
return post<{ status: string }>(`/api/projects/${projectId}/stages/${stageId}/instances`, {
image_tag: imageTag
});
}
export function removeInstance(
projectId: string,
stageId: string,
instanceId: string
): Promise<{ deleted: string }> {
return del<{ deleted: string }>(
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}`
);
}
export function stopInstance(
projectId: string,
stageId: string,
instanceId: string
): Promise<{ status: string }> {
return post<{ status: string }>(
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stop`
);
}
export function startInstance(
projectId: string,
stageId: string,
instanceId: string
): Promise<{ status: string }> {
return post<{ status: string }>(
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/start`
);
}
export function restartInstance(
projectId: string,
stageId: string,
instanceId: string
): Promise<{ status: string }> {
return post<{ status: string }>(
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/restart`
);
}
// ── Deploys ─────────────────────────────────────────────────────────
export function listDeploys(limit = 50, signal?: AbortSignal): Promise<Deploy[]> {
return get<Deploy[]>(`/api/deploys?limit=${limit}`, signal);
}
export function getDeployLogs(deployId: string): Promise<DeployLog[]> {
return get<DeployLog[]>(`/api/deploys/${deployId}/logs`);
}
export function inspectImage(image: string): Promise<InspectResult> {
return post<InspectResult>('/api/deploy/inspect', { image });
}
export function quickDeploy(data: {
name?: string;
image: string;
tag?: string;
registry?: string;
port?: number;
force?: boolean;
enable_proxy?: boolean;
auto_deploy?: boolean;
}): Promise<{ project: Project; status: string }> {
return post<{ project: Project; status: string }>('/api/deploy/quick', data);
}
// ── Registries ──────────────────────────────────────────────────────
export function listRegistries(): Promise<Registry[]> {
return get<Registry[]>('/api/registries');
}
export function createRegistry(data: Partial<Registry>): Promise<Registry> {
return post<Registry>('/api/registries', data);
}
export function updateRegistry(id: string, data: Partial<Registry>): Promise<Registry> {
return put<Registry>(`/api/registries/${id}`, data);
}
export function deleteRegistry(id: string): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/registries/${id}`);
}
export function testRegistry(id: string): Promise<{ status: string }> {
return post<{ status: string }>(`/api/registries/${id}/test`);
}
export function listRegistryTags(registryId: string, image: string): Promise<string[]> {
// Image contains slashes (e.g. "owner/name") — don't encode them;
// the backend route uses a wildcard (/tags/*) that expects path segments.
return get<string[]>(`/api/registries/${registryId}/tags/${image}`);
}
export function listRegistryImages(registryId: string): Promise<RegistryImage[]> {
return get<RegistryImage[]>(`/api/registries/${registryId}/images`);
}
// ── Settings ────────────────────────────────────────────────────────
export function getSettings(signal?: AbortSignal): Promise<Settings> {
return get<Settings>('/api/settings', signal);
}
export function updateSettings(data: Partial<Settings>): Promise<Settings> {
return put<Settings>('/api/settings', data);
}
// ── Webhooks ───────────────────────────────────────────────────────
export interface WebhookUrlResponse {
webhook_url: string;
webhook_secret: string;
has_signing_secret?: boolean;
webhook_require_signature?: boolean;
}
export interface SigningSecretResponse {
signing_secret: string;
}
export function getProjectWebhook(projectId: string): Promise<WebhookUrlResponse> {
return get<WebhookUrlResponse>(`/api/projects/${projectId}/webhook`);
}
export function regenerateProjectWebhook(projectId: string): Promise<WebhookUrlResponse> {
return post<WebhookUrlResponse>(`/api/projects/${projectId}/webhook/regenerate`);
}
export function regenerateProjectSigningSecret(projectId: string): Promise<SigningSecretResponse> {
return post<SigningSecretResponse>(`/api/projects/${projectId}/webhook/signing-secret/regenerate`);
}
export async function disableProjectSigningSecret(projectId: string): Promise<void> {
await del<void>(`/api/projects/${projectId}/webhook/signing-secret`);
}
export async function setProjectRequireSignature(projectId: string, require: boolean): Promise<void> {
await put<void>(`/api/projects/${projectId}/webhook/require-signature`, { require_signature: require });
}
export function getStaticSiteWebhook(siteId: string): Promise<WebhookUrlResponse> {
return get<WebhookUrlResponse>(`/api/sites/${siteId}/webhook`);
}
export function regenerateStaticSiteWebhook(siteId: string): Promise<WebhookUrlResponse> {
return post<WebhookUrlResponse>(`/api/sites/${siteId}/webhook/regenerate`);
}
export function regenerateStaticSiteSigningSecret(siteId: string): Promise<SigningSecretResponse> {
return post<SigningSecretResponse>(`/api/sites/${siteId}/webhook/signing-secret/regenerate`);
}
export async function disableStaticSiteSigningSecret(siteId: string): Promise<void> {
await del<void>(`/api/sites/${siteId}/webhook/signing-secret`);
}
export async function setStaticSiteRequireSignature(siteId: string, require: boolean): Promise<void> {
await put<void>(`/api/sites/${siteId}/webhook/require-signature`, { require_signature: require });
}
export interface WebhookDelivery {
id: number;
target_type: 'project' | 'site';
target_id: string;
target_name: string;
received_at: string;
source_ip: string;
signature_state: 'valid' | 'invalid' | 'missing' | 'unconfigured';
status_code: number;
outcome: string;
detail: string;
body_size: number;
}
export function listProjectWebhookDeliveries(projectId: string, signal?: AbortSignal): Promise<WebhookDelivery[]> {
return get<WebhookDelivery[]>(`/api/projects/${projectId}/webhook/deliveries`, signal);
}
export function listStaticSiteWebhookDeliveries(siteId: string, signal?: AbortSignal): Promise<WebhookDelivery[]> {
return get<WebhookDelivery[]>(`/api/sites/${siteId}/webhook/deliveries`, signal);
}
// ── Outgoing-webhook signing & test ────────────────────────────────
export interface NotificationSecretResponse {
secret: string;
has_secret: boolean;
}
export interface NotificationTestResult {
url: string;
tier: 'settings' | 'project' | 'stage' | 'site';
status_code: number;
latency_ms: number;
signature_sent: boolean;
delivery_id: string;
response_snippet: string;
error?: string;
}
// Settings (global) tier.
export function getSettingsNotificationSecret(): Promise<NotificationSecretResponse> {
return get<NotificationSecretResponse>('/api/settings/notification-secret');
}
export function regenerateSettingsNotificationSecret(): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>('/api/settings/notification-secret/regenerate');
}
export function disableSettingsNotificationSigning(): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>('/api/settings/notification-secret/disable');
}
export function testSettingsNotification(): Promise<NotificationTestResult> {
return post<NotificationTestResult>('/api/settings/notification-test');
}
// Project tier.
export function getProjectNotificationSecret(projectId: string): Promise<NotificationSecretResponse> {
return get<NotificationSecretResponse>(`/api/projects/${projectId}/notification-secret`);
}
export function regenerateProjectNotificationSecret(projectId: string): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>(`/api/projects/${projectId}/notification-secret/regenerate`);
}
export function disableProjectNotificationSigning(projectId: string): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>(`/api/projects/${projectId}/notification-secret/disable`);
}
export function testProjectNotification(projectId: string): Promise<NotificationTestResult> {
return post<NotificationTestResult>(`/api/projects/${projectId}/notification-test`);
}
// Stage tier.
export function getStageNotificationSecret(projectId: string, stageId: string): Promise<NotificationSecretResponse> {
return get<NotificationSecretResponse>(`/api/projects/${projectId}/stages/${stageId}/notification-secret`);
}
export function regenerateStageNotificationSecret(projectId: string, stageId: string): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>(`/api/projects/${projectId}/stages/${stageId}/notification-secret/regenerate`);
}
export function disableStageNotificationSigning(projectId: string, stageId: string): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>(`/api/projects/${projectId}/stages/${stageId}/notification-secret/disable`);
}
export function testStageNotification(projectId: string, stageId: string): Promise<NotificationTestResult> {
return post<NotificationTestResult>(`/api/projects/${projectId}/stages/${stageId}/notification-test`);
}
// Static-site tier.
export function getStaticSiteNotificationSecret(siteId: string): Promise<NotificationSecretResponse> {
return get<NotificationSecretResponse>(`/api/sites/${siteId}/notification-secret`);
}
export function regenerateStaticSiteNotificationSecret(siteId: string): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>(`/api/sites/${siteId}/notification-secret/regenerate`);
}
export function disableStaticSiteNotificationSigning(siteId: string): Promise<NotificationSecretResponse> {
return post<NotificationSecretResponse>(`/api/sites/${siteId}/notification-secret/disable`);
}
export function testStaticSiteNotification(siteId: string): Promise<NotificationTestResult> {
return post<NotificationTestResult>(`/api/sites/${siteId}/notification-test`);
}
// ── Proxy Routes ───────────────────────────────────────────────────
export function listProxyRoutes(): Promise<ProxyRoute[]> {
return get<ProxyRoute[]>('/api/proxies');
}
// ── Docker Management ──────────────────────────────────────────────
export function fetchContainerLogs(
projectId: string, stageId: string, instanceId: string, tail = 200
): Promise<string[]> {
return get<string[]>(`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/logs?tail=${tail}`);
}
export function listProjectImages(projectId: string, signal?: AbortSignal): Promise<LocalImage[]> {
return get<LocalImage[]>(`/api/projects/${projectId}/images`, signal);
}
export function getUnusedImageStats(signal?: AbortSignal): Promise<{
total_size_mb: number; count: number; threshold_mb: number; exceeded: boolean;
}> {
return get<{ total_size_mb: number; count: number; threshold_mb: number; exceeded: boolean }>('/api/docker/unused-images', signal);
}
export function pruneImages(): Promise<{ images_removed: number; space_reclaimed_mb: number }> {
return post<{ images_removed: number; space_reclaimed_mb: number }>('/api/docker/prune-images');
}
export function testNpmConnection(data: { npm_url?: string; npm_email?: string; npm_password?: string }): Promise<{ status: string }> {
return post<{ status: string }>('/api/settings/npm/test', data);
}
export function listNpmCertificates(): Promise<NpmCertificate[]> {
return get<NpmCertificate[]>('/api/settings/npm-certificates');
}
export function listNpmAccessLists(): Promise<NpmAccessList[]> {
return get<NpmAccessList[]>('/api/settings/npm-access-lists');
}
// ── DNS ────────────────────────────────────────────────────────────
export function testDnsConnection(provider: string, token: string, zoneId: string): Promise<{ success: boolean; error?: string }> {
return post<{ success: boolean; error?: string }>('/api/settings/dns/test', { provider, token, zone_id: zoneId });
}
export function listDnsZones(token?: string): Promise<DnsZone[]> {
return post<DnsZone[]>('/api/settings/dns/zones', { token: token ?? '' });
}
export function getDnsRecords(): Promise<DnsRecordView[]> {
return get<DnsRecordView[]>('/api/dns/records');
}
export function syncDnsRecords(): Promise<{ created: number; deleted: number; already_synced: number }> {
return post<{ created: number; deleted: number; already_synced: number }>('/api/dns/sync');
}
export function deleteDnsRecord(fqdn: string): Promise<void> {
return del<void>(`/api/dns/records/${encodeURIComponent(fqdn)}`);
}
// ── Backups ────────────────────────────────────────────────────────
export function listBackups(): Promise<BackupInfo[]> {
return get<BackupInfo[]>('/api/backups');
}
export function triggerBackup(): Promise<BackupInfo> {
return post<BackupInfo>('/api/backups');
}
export function deleteBackup(id: string): Promise<void> {
return del<void>(`/api/backups/${id}`);
}
export function restoreBackup(id: string): Promise<{ status: string; message: string }> {
return post<{ status: string; message: string }>(`/api/backups/${id}/restore`);
}
export function backupDownloadUrl(id: string): string {
return `/api/backups/${id}/download`;
}
// ── Health ──────────────────────────────────────────────────────────
export function getHealth(): Promise<{ docker: DockerHealth; proxy?: ProxyHealth }> {
return get<{ docker: DockerHealth; proxy?: ProxyHealth }>('/api/health');
}
// ── Auth ─────────────────────────────────────────────────────────────
export function login(username: string, password: string): Promise<{ token: string; expires_at: string }> {
return post<{ token: string; expires_at: string }>('/api/auth/login', { username, password });
}
export function getCurrentUser(): Promise<{ id: string; username: string; email: string; role: string }> {
return get<{ id: string; username: string; email: string; role: string }>('/api/auth/me');
}
// Auth settings
export async function getAuthSettings(): Promise<any> {
return request<any>('/api/auth/settings');
}
export async function updateAuthSettings(settings: any): Promise<any> {
return request<any>('/api/auth/settings', {
method: 'PUT',
body: JSON.stringify(settings)
});
}
export async function listUsers(): Promise<any[]> {
return request<any[]>('/api/auth/users');
}
export async function createUser(data: { username: string; password: string; email?: string; role?: string }): Promise<any> {
return request<any>('/api/auth/users', {
method: 'POST',
body: JSON.stringify(data)
});
}
export async function updateUser(uid: string, data: { email?: string; role?: string }): Promise<any> {
return request<any>(`/api/auth/users/${uid}`, {
method: 'PUT',
body: JSON.stringify(data)
});
}
export async function changeUserPassword(uid: string, password: string): Promise<any> {
return request<any>(`/api/auth/users/${uid}/password`, {
method: 'PUT',
body: JSON.stringify({ password })
});
}
export async function deleteUser(uid: string): Promise<any> {
return request<any>(`/api/auth/users/${uid}`, { method: 'DELETE' });
}
export async function logout(): Promise<void> {
await request<any>('/api/auth/logout', { method: 'POST' });
}
// ── Config Export ────────────────────────────────────────────────────
export function exportConfigUrl(): string {
return '/api/config/export';
}
// ── Stage Env Overrides ──────────────────────────────────────────────
export function listStageEnv(projectId: string, stageId: string): Promise<StageEnv[]> {
return get<StageEnv[]>(`/api/projects/${projectId}/stages/${stageId}/env`);
}
export function createStageEnv(
projectId: string,
stageId: string,
data: { key: string; value: string; encrypted?: boolean }
): Promise<StageEnv> {
return post<StageEnv>(`/api/projects/${projectId}/stages/${stageId}/env`, data);
}
export function updateStageEnv(
projectId: string,
stageId: string,
envId: string,
data: { key?: string; value?: string; encrypted?: boolean }
): Promise<StageEnv> {
return put<StageEnv>(`/api/projects/${projectId}/stages/${stageId}/env/${envId}`, data);
}
export function deleteStageEnv(
projectId: string,
stageId: string,
envId: string
): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/projects/${projectId}/stages/${stageId}/env/${envId}`);
}
// ── Volumes ──────────────────────────────────────────────────────────
export function listVolumes(projectId: string): Promise<Volume[]> {
return get<Volume[]>(`/api/projects/${projectId}/volumes`);
}
export function createVolume(
projectId: string,
data: { source: string; target: string; scope: string; name?: string; mode?: string }
): Promise<Volume> {
return post<Volume>(`/api/projects/${projectId}/volumes`, data);
}
export function updateVolume(
projectId: string,
volId: string,
data: { source?: string; target?: string; scope?: string; name?: string; mode?: string }
): Promise<Volume> {
return put<Volume>(`/api/projects/${projectId}/volumes/${volId}`, data);
}
export function listVolumeScopes(): Promise<VolumeScopeInfo[]> {
return get<VolumeScopeInfo[]>('/api/volumes/scopes');
}
export function deleteVolume(
projectId: string,
volId: string
): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/projects/${projectId}/volumes/${volId}`);
}
export function browseVolume(
projectId: string,
volId: string,
params?: { path?: string; stage?: string; tag?: string }
): Promise<BrowseResult> {
const query = new URLSearchParams();
if (params?.path) query.set('path', params.path);
if (params?.stage) query.set('stage', params.stage);
if (params?.tag) query.set('tag', params.tag);
const qs = query.toString();
return get<BrowseResult>(`/api/projects/${projectId}/volumes/${volId}/browse${qs ? `?${qs}` : ''}`);
}
export function volumeDownloadUrl(
projectId: string,
volId: string,
params?: { path?: string; stage?: string; tag?: string }
): string {
const query = new URLSearchParams();
if (params?.path) query.set('path', params.path);
if (params?.stage) query.set('stage', params.stage);
if (params?.tag) query.set('tag', params.tag);
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
if (token) query.set('token', token);
const qs = query.toString();
return `/api/projects/${projectId}/volumes/${volId}/download${qs ? `?${qs}` : ''}`;
}
export async function uploadToVolume(
projectId: string,
volId: string,
files: FileList,
params?: { path?: string; stage?: string; tag?: string }
): Promise<{ uploaded: string[]; count: number }> {
const query = new URLSearchParams();
if (params?.path) query.set('path', params.path);
if (params?.stage) query.set('stage', params.stage);
if (params?.tag) query.set('tag', params.tag);
const qs = query.toString();
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`/api/projects/${projectId}/volumes/${volId}/upload${qs ? `?${qs}` : ''}`, {
method: 'POST',
headers,
body: formData,
});
const envelope = await res.json();
if (!envelope.success) throw new Error(envelope.error ?? 'Upload failed');
return envelope.data;
}
// ── Event Log ───────────────────────────────────────────────────────
export function fetchEventLog(params?: {
severity?: string;
source?: string;
since?: string;
until?: string;
limit?: number;
offset?: number;
}): Promise<EventLogEntry[]> {
const query = new URLSearchParams();
if (params?.severity) query.set('severity', params.severity);
if (params?.source) query.set('source', params.source);
if (params?.since) query.set('since', params.since);
if (params?.until) query.set('until', params.until);
if (params?.limit) query.set('limit', String(params.limit));
if (params?.offset) query.set('offset', String(params.offset));
const qs = query.toString();
return get<EventLogEntry[]>(`/api/events/log${qs ? `?${qs}` : ''}`);
}
export function fetchEventLogStats(): Promise<EventLogStats> {
return get<EventLogStats>('/api/events/log/stats');
}
export function deleteEvent(id: number): Promise<{ status: string }> {
return del<{ status: string }>(`/api/events/log/${id}`);
}
export function clearAllEvents(): Promise<{ status: string; count: number }> {
return del<{ status: string; count: number }>('/api/events/log');
}
// ── Stale Containers ────────────────────────────────────────────────
export function fetchStaleContainers(signal?: AbortSignal): Promise<StaleContainer[]> {
return get<StaleContainer[]>('/api/containers/stale', signal);
}
export function cleanupStaleContainer(id: string): Promise<{ deleted: string }> {
return post<{ deleted: string }>(`/api/containers/stale/${id}/cleanup`);
}
export function bulkCleanupStaleContainers(): Promise<{ deleted: number }> {
return post<{ deleted: number }>('/api/containers/stale/cleanup');
}
// ── Container Stats ────────────────────────────────────────────────
export function fetchContainerStats(
projectId: string,
stageId: string,
instanceId: string,
signal?: AbortSignal
): Promise<ContainerStats> {
return get<ContainerStats>(
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stats`,
signal
);
}
export function fetchInstanceStatsHistory(
projectId: string,
stageId: string,
instanceId: string,
window = '2h',
signal?: AbortSignal
): Promise<ContainerStatsSample[]> {
return get<ContainerStatsSample[]>(
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stats/history?window=${encodeURIComponent(window)}`,
signal
);
}
export function fetchSystemStats(signal?: AbortSignal): Promise<SystemStats> {
return get<SystemStats>('/api/system/stats', signal);
}
export function fetchSystemStatsHistory(
window = '2h',
signal?: AbortSignal
): Promise<SystemStatsSample[]> {
return get<SystemStatsSample[]>(`/api/system/stats/history?window=${encodeURIComponent(window)}`, signal);
}
export function fetchTopContainers(
by: 'cpu' | 'memory' = 'cpu',
limit = 5,
signal?: AbortSignal
): Promise<TopContainerSample[]> {
return get<TopContainerSample[]>(`/api/system/stats/top?by=${by}&limit=${limit}`, signal);
}
export function fetchStaticSiteStats(id: string, signal?: AbortSignal): Promise<ContainerStats> {
return get<ContainerStats>(`/api/sites/${id}/stats`, signal);
}
export function fetchStaticSiteStatsHistory(
id: string,
window = '2h',
signal?: AbortSignal
): Promise<ContainerStatsSample[]> {
return get<ContainerStatsSample[]>(
`/api/sites/${id}/stats/history?window=${encodeURIComponent(window)}`,
signal
);
}
export async function fetchStaticSiteLogs(id: string, tail = 200): Promise<string[]> {
const result = await get<string[] | null>(`/api/sites/${id}/logs?tail=${tail}`);
return result ?? [];
}
// ── Static Sites ──────────────────────────────────────────────────────
import type { StaticSite, StaticSiteSecret, FolderEntry, GitProvider, RepoInfo } from './types';
export function listStaticSites(signal?: AbortSignal): Promise<StaticSite[]> {
return get<StaticSite[]>('/api/sites', signal);
}
export function getStaticSite(id: string): Promise<StaticSite> {
return get<StaticSite>(`/api/sites/${id}`);
}
export function createStaticSite(data: Partial<StaticSite>): Promise<StaticSite> {
return post<StaticSite>('/api/sites', data);
}
export function updateStaticSite(id: string, data: Partial<StaticSite>): Promise<StaticSite> {
return put<StaticSite>(`/api/sites/${id}`, data);
}
export function deleteStaticSite(id: string): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/sites/${id}`);
}
export function deployStaticSite(id: string): Promise<{ status: string }> {
return post<{ status: string }>(`/api/sites/${id}/deploy`);
}
export function stopStaticSite(id: string): Promise<{ status: string }> {
return post<{ status: string }>(`/api/sites/${id}/stop`);
}
export function startStaticSite(id: string): Promise<{ status: string }> {
return post<{ status: string }>(`/api/sites/${id}/start`);
}
export function listStaticSiteRepos(data: {
provider?: string;
gitea_url: string;
access_token?: string;
query?: string;
}): Promise<RepoInfo[]> {
return post<RepoInfo[]>('/api/sites/repos', data);
}
export function detectStaticSiteProvider(url: string): Promise<{ provider: GitProvider }> {
return post<{ provider: GitProvider }>('/api/sites/detect-provider', { url });
}
export function testStaticSiteConnection(data: {
provider?: string;
gitea_url: string;
access_token?: string;
repo_owner: string;
repo_name: string;
}): Promise<{ status: string }> {
return post<{ status: string }>('/api/sites/test-connection', data);
}
export function listStaticSiteBranches(data: {
provider?: string;
gitea_url: string;
access_token?: string;
repo_owner: string;
repo_name: string;
}): Promise<string[]> {
return post<string[]>('/api/sites/branches', data);
}
export function listStaticSiteTree(data: {
provider?: string;
gitea_url: string;
access_token?: string;
repo_owner: string;
repo_name: string;
branch: string;
}): Promise<FolderEntry[]> {
return post<FolderEntry[]>('/api/sites/tree', data);
}
export function listStaticSiteSecrets(siteId: string): Promise<StaticSiteSecret[]> {
return get<StaticSiteSecret[]>(`/api/sites/${siteId}/secrets`);
}
export function createStaticSiteSecret(
siteId: string,
data: { key: string; value: string; encrypted?: boolean }
): Promise<StaticSiteSecret> {
return post<StaticSiteSecret>(`/api/sites/${siteId}/secrets`, data);
}
export function updateStaticSiteSecret(
siteId: string,
secretId: string,
data: { key?: string; value?: string; encrypted?: boolean }
): Promise<StaticSiteSecret> {
return put<StaticSiteSecret>(`/api/sites/${siteId}/secrets/${secretId}`, data);
}
export function deleteStaticSiteSecret(
siteId: string,
secretId: string
): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/sites/${siteId}/secrets/${secretId}`);
}
export function getStaticSiteStorage(
siteId: string
): Promise<import('./types').StaticSiteStorageUsage> {
return get<import('./types').StaticSiteStorageUsage>(`/api/sites/${siteId}/storage`);
}
// ── Stacks (docker-compose) ─────────────────────────────────────────
import type { Stack, StackRevision, StackService } from './types';
export function listStacks(signal?: AbortSignal): Promise<Stack[]> {
return get<Stack[]>('/api/stacks', signal);
}
export function getStack(id: string, signal?: AbortSignal): Promise<Stack> {
return get<Stack>(`/api/stacks/${id}`, signal);
}
export function createStack(data: {
name: string;
description?: string;
yaml: string;
deploy?: boolean;
}): Promise<{ stack: Stack; revision: StackRevision }> {
return post<{ stack: Stack; revision: StackRevision }>('/api/stacks', data);
}
export function updateStack(id: string, data: { name?: string; description?: string }): Promise<Stack> {
return put<Stack>(`/api/stacks/${id}`, data);
}
export function deleteStack(id: string, removeVolumes = false): Promise<{ deleted: string }> {
const qs = removeVolumes ? '?remove_volumes=true' : '';
return del<{ deleted: string }>(`/api/stacks/${id}${qs}`);
}
export function listStackRevisions(id: string, signal?: AbortSignal): Promise<StackRevision[]> {
return get<StackRevision[]>(`/api/stacks/${id}/revisions`, signal);
}
export function getStackRevision(id: string, revId: string): Promise<StackRevision> {
return get<StackRevision>(`/api/stacks/${id}/revisions/${revId}`);
}
export function createStackRevision(id: string, yaml: string): Promise<StackRevision> {
return post<StackRevision>(`/api/stacks/${id}/revisions`, { yaml });
}
export function rollbackStack(id: string, revId: string): Promise<StackRevision> {
return post<StackRevision>(`/api/stacks/${id}/rollback/${revId}`);
}
export function stopStack(id: string): Promise<{ status: string }> {
return post<{ status: string }>(`/api/stacks/${id}/stop`);
}
export function startStack(id: string): Promise<{ status: string }> {
return post<{ status: string }>(`/api/stacks/${id}/start`);
}
export function getStackServices(id: string, signal?: AbortSignal): Promise<StackService[]> {
return get<StackService[]>(`/api/stacks/${id}/services`, signal);
}
export async function getStackLogs(
id: string,
service?: string,
tail = 200
): Promise<string> {
const params = new URLSearchParams();
if (service) params.set('service', service);
params.set('tail', String(tail));
const token = getAuthToken();
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`/api/stacks/${id}/logs?${params.toString()}`, { headers });
if (!res.ok) {
throw new ApiError(`Failed to fetch logs: ${res.status}`, res.status);
}
return res.text();
}
// ── Workloads ───────────────────────────────────────────────────────
export function listWorkloads(kind?: WorkloadKind, signal?: AbortSignal): Promise<Workload[]> {
const path = kind ? `/api/workloads?kind=${encodeURIComponent(kind)}` : '/api/workloads';
return get<Workload[]>(path, signal);
}
export function getWorkload(id: string, signal?: AbortSignal): Promise<Workload> {
return get<Workload>(`/api/workloads/${id}`, signal);
}
export function listWorkloadContainers(id: string, signal?: AbortSignal): Promise<Container[]> {
return get<Container[]>(`/api/workloads/${id}/containers`, signal);
}
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);
}
// ── Triggers (first-class redeploy signal sources) ──────────────────
export interface RedeployTrigger {
id: string;
kind: string;
name: string;
config: unknown;
webhook_enabled: boolean;
webhook_require_signature: boolean;
binding_count: number;
created_at: string;
updated_at: string;
}
export interface TriggerWebhook {
url: string;
secret: string;
webhook_require_signature: boolean;
}
export interface TriggerBinding {
id: string;
workload_id: string;
workload_name: string;
trigger_id: string;
binding_config: unknown;
enabled: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
export interface WorkloadTriggerBinding extends TriggerBinding {
trigger_kind: string;
trigger_name: string;
}
export interface TriggerInput {
kind: string;
name: string;
config: unknown;
webhook_enabled: boolean;
webhook_require_signature: boolean;
}
export interface BindingInput {
workload_id: string;
binding_config?: unknown;
enabled?: boolean;
sort_order?: number;
}
export interface WorkloadBindInput {
trigger_id?: string;
binding_config?: unknown;
enabled?: boolean;
sort_order?: number;
inline?: TriggerInput;
}
export function listTriggers(kind?: string, signal?: AbortSignal): Promise<RedeployTrigger[]> {
const path = kind ? `/api/triggers?kind=${encodeURIComponent(kind)}` : '/api/triggers';
return get<RedeployTrigger[]>(path, signal);
}
export function getTrigger(id: string, signal?: AbortSignal): Promise<RedeployTrigger> {
return get<RedeployTrigger>(`/api/triggers/${id}`, signal);
}
export function createTrigger(body: TriggerInput): Promise<RedeployTrigger> {
return post<RedeployTrigger>('/api/triggers', body);
}
export function updateTrigger(id: string, body: TriggerInput): Promise<RedeployTrigger> {
return put<RedeployTrigger>(`/api/triggers/${id}`, body);
}
export function deleteTrigger(id: string): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/triggers/${id}`);
}
export function getTriggerWebhook(id: string, signal?: AbortSignal): Promise<TriggerWebhook> {
return get<TriggerWebhook>(`/api/triggers/${id}/webhook`, signal);
}
export function regenerateTriggerWebhook(id: string): Promise<{ secret: string; url: string }> {
return post<{ secret: string; url: string }>(`/api/triggers/${id}/webhook/regenerate`);
}
export function listBindingsForTrigger(id: string, signal?: AbortSignal): Promise<TriggerBinding[]> {
return get<TriggerBinding[]>(`/api/triggers/${id}/bindings`, signal);
}
export function bindWorkloadToTrigger(triggerId: string, body: BindingInput): Promise<TriggerBinding> {
return post<TriggerBinding>(`/api/triggers/${triggerId}/bindings`, body);
}
export function listBindingsForWorkload(
workloadId: string,
signal?: AbortSignal
): Promise<WorkloadTriggerBinding[]> {
return get<WorkloadTriggerBinding[]>(`/api/workloads/${workloadId}/triggers`, signal);
}
export function bindTriggerToWorkload(
workloadId: string,
body: WorkloadBindInput
): Promise<TriggerBinding> {
return post<TriggerBinding>(`/api/workloads/${workloadId}/triggers`, body);
}
export function updateBinding(
id: string,
body: { binding_config?: unknown; enabled?: boolean; sort_order?: number }
): Promise<TriggerBinding> {
return put<TriggerBinding>(`/api/bindings/${id}`, body);
}
export function deleteBinding(id: string): Promise<{ deleted: string }> {
return del<{ deleted: string }>(`/api/bindings/${id}`);
}
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 {
workload_id?: string;
kind?: WorkloadKind;
state?: string;
app_id?: string;
}
export function listContainers(filter: ListContainersFilter = {}, signal?: AbortSignal): Promise<ContainerView[]> {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(filter)) {
// Skip unset / empty filters; explicitly check undefined and '' instead
// of truthy so future filter shapes (numbers, booleans) aren't dropped.
if (v !== undefined && v !== '') params.set(k, String(v));
}
const qs = params.toString();
const path = qs ? `/api/containers?${qs}` : '/api/containers';
return get<ContainerView[]>(path, signal);
}
// ── Apps ────────────────────────────────────────────────────────────
export function listApps(signal?: AbortSignal): Promise<App[]> {
return get<App[]>('/api/apps', signal);
}
export function getApp(id: string, signal?: AbortSignal): Promise<App> {
return get<App>(`/api/apps/${id}`, signal);
}
export function createApp(data: { name: string; description?: string }): Promise<App> {
return post<App>('/api/apps', data);
}
export function updateApp(id: string, data: { name: string; description?: string }): Promise<App> {
return put<App>(`/api/apps/${id}`, data);
}
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 };