feat(ui): show alert action buttons and status chips

Alerts that match a configured action get a button labelled from the
config. A confirm dialog opens first when the action defines confirm text.
After a press the UI posts the alert, reloads the state through the
existing poll path and shows the reported status as a chip, with an anchor
when the status carries a link.

The alert dialog comes from reka-ui through the usual shadcn-vue wrappers.
This commit is contained in:
2026-10-04 16:37:31 +01:00
parent 5824c4dd10
commit 23bbbedf59
18 changed files with 496 additions and 4 deletions
+20 -1
View File
@@ -1,4 +1,4 @@
import type { RepoRefreshResult, TrackerState } from './types'
import type { ActionStatus, RepoRefreshResult, TrackerState } from './types'
export async function fetchState(signal?: AbortSignal): Promise<TrackerState> {
const res = await fetch('/api/state', { signal, headers: { Accept: 'application/json' } })
@@ -32,3 +32,22 @@ export async function requestRepoRefresh(fullName: string): Promise<RepoRefreshR
if (!res.ok) throw new Error(`refresh of ${fullName} failed: HTTP ${res.status}`)
return (await res.json()) as RepoRefreshResult
}
/** Sends an alert to the webhook behind an action; returns the status the webhook reported, if any. */
export async function requestAction(
id: string,
repo: string,
url: string,
): Promise<ActionStatus | null> {
const res = await fetch(`/api/actions/${encodeURIComponent(id)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo, url }),
})
if (!res.ok) {
const body = (await res.json().catch(() => null)) as { error?: string } | null
throw new Error(body?.error ?? `action ${id} failed: HTTP ${res.status}`)
}
const status = (await res.json().catch(() => null)) as ActionStatus | null
return status && typeof status.state === 'string' ? status : null
}
+21
View File
@@ -24,6 +24,21 @@ export interface Repo {
release: Release | null
}
/** An operator-defined button, as listed by the server. An empty `kinds` matches every alert kind. */
export interface ActionDef {
id: string
label: string
confirm?: string
kinds: string[]
}
/** Status of an action for one alert; `link` is an http(s) address when present. */
export interface ActionStatus {
state: string
label?: string
link?: string
}
export interface Alert {
repo: string
kind: string
@@ -32,6 +47,7 @@ export interface Alert {
detail: string
url: string
since: string
actionStatus?: Record<string, ActionStatus>
}
export interface Thresholds {
@@ -54,6 +70,8 @@ export interface TrackerState {
thresholds: Thresholds
repos: Repo[]
alerts: Alert[]
actions?: ActionDef[]
actionsError?: string
}
/** Answer to a single-repository refresh: the state after it, plus the repo name. */
@@ -61,12 +79,15 @@ export type RepoRefreshResult = TrackerState & { repo: string }
/** One line in a repo block: every alert that targets the same URL, merged. */
export interface Row {
repo: string
kind: string
subject: string
detail: string
url: string
since: string
severity: Exclude<Severity, 'ok'>
actions?: ActionDef[]
actionStatus?: Record<string, ActionStatus>
}
export interface Block {