feat: deferred dispatch, release-check provider, settings polish
- Defer quiet-hours dispatches into new deferred_dispatch table; drain job + periodic catch-up scan re-fire at window end with coalescing on (link, event_type, collection_id). - Add ON DELETE SET NULL migration on event_log_id and partial unique index on (link_id, collection_id, event_type) WHERE status='pending'. - Add release-check provider abstraction (Gitea/GitHub) with SSRF-safe URL validation, settings UI cassette, and scheduled polling. - Replace importlib-only version lookup with version.py helper that prefers the higher of installed metadata vs source pyproject so stale editable dev installs stop misreporting. - Aurora frontend polish: MetaStrip component, ReleaseCassette, EventDetailModal expansion, and i18n additions.
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
import ExecutionHistory from './ExecutionHistory.svelte';
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import MetaStrip, { type MetaTile } from '$lib/components/MetaStrip.svelte';
|
||||
import type { Action, ActionRule } from '$lib/types';
|
||||
|
||||
let allActions = $derived(actionsCache.items);
|
||||
@@ -193,6 +194,51 @@
|
||||
if (status === 'failed') return 'var(--color-error-fg)';
|
||||
return 'var(--color-muted-foreground)';
|
||||
}
|
||||
|
||||
function statusTone(status: string | undefined): MetaTile['tone'] {
|
||||
if (status === 'success') return 'mint';
|
||||
if (status === 'partial') return 'citrus';
|
||||
if (status === 'failed') return 'coral';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function actionTiles(action: Action): MetaTile[] {
|
||||
const tiles: MetaTile[] = [];
|
||||
tiles.push(action.enabled
|
||||
? { icon: 'mdiCheckCircle', label: t('commandTracker.enabled'), tone: 'mint' }
|
||||
: { icon: 'mdiPauseCircleOutline', label: t('commandTracker.disabled'), tone: 'default' });
|
||||
tiles.push({
|
||||
icon: 'mdiServer',
|
||||
label: getProviderName(action.provider_id),
|
||||
tone: 'lavender',
|
||||
});
|
||||
tiles.push({
|
||||
icon: 'mdiTagOutline',
|
||||
label: action.action_type,
|
||||
tone: 'sky',
|
||||
mono: true,
|
||||
});
|
||||
tiles.push({
|
||||
icon: action.schedule_type === 'cron' ? 'mdiClockOutline' : 'mdiTimerOutline',
|
||||
label: formatSchedule(action),
|
||||
tone: 'orchid',
|
||||
mono: true,
|
||||
});
|
||||
tiles.push({
|
||||
icon: 'mdiFormatListBulleted',
|
||||
value: String(action.rules?.length || 0),
|
||||
label: t('actions.rules'),
|
||||
tone: (action.rules?.length || 0) > 0 ? 'sky' : 'default',
|
||||
});
|
||||
if (action.last_run_status) {
|
||||
tiles.push({
|
||||
icon: 'mdiHistory',
|
||||
label: action.last_run_status,
|
||||
tone: statusTone(action.last_run_status),
|
||||
});
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
</script>
|
||||
|
||||
<PageHeader
|
||||
@@ -323,32 +369,35 @@
|
||||
<EmptyState icon="mdiFilterOff" message={t('common.noFilterResults')} />
|
||||
</Card>
|
||||
{:else if !showForm}
|
||||
<div class="space-y-3 stagger-children">
|
||||
<div class="list-stack stagger-children">
|
||||
{#each actions as action}
|
||||
<Card hover entityId={action.id}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
||||
style="background: {action.enabled ? '#059669' : 'var(--color-muted-foreground)'}"></div>
|
||||
<span style="color: var(--color-primary);"><MdiIcon name={action.icon || 'mdiPlayCircleOutline'} size={20} /></span>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="font-medium">{action.name}</p>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{action.action_type}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-[var(--color-muted-foreground)]">
|
||||
<CrossLink href="/providers" icon={providerDefaultIcon(getProvider(action.provider_id) || {})} label={getProviderName(action.provider_id)} entityId={action.provider_id} />
|
||||
<span>{formatSchedule(action)}</span>
|
||||
<span>{action.rules?.length || 0} {t('actions.rules')}</span>
|
||||
{#if action.last_run_status}
|
||||
<span style="color: {statusColor(action.last_run_status)}">
|
||||
{action.last_run_status}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="list-row">
|
||||
<div class="list-row__identity">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
||||
style="background: {action.enabled ? '#059669' : 'var(--color-muted-foreground)'}"></div>
|
||||
<span style="color: var(--color-primary);" class="shrink-0"><MdiIcon name={action.icon || 'mdiPlayCircleOutline'} size={20} /></span>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<p class="font-medium truncate">{action.name}</p>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)] shrink-0">{action.action_type}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-[var(--color-muted-foreground)] list-row__secondary">
|
||||
<CrossLink href="/providers" icon={providerDefaultIcon(getProvider(action.provider_id) || {})} label={getProviderName(action.provider_id)} entityId={action.provider_id} />
|
||||
<span>{formatSchedule(action)}</span>
|
||||
<span>{action.rules?.length || 0} {t('actions.rules')}</span>
|
||||
{#if action.last_run_status}
|
||||
<span style="color: {statusColor(action.last_run_status)}">
|
||||
{action.last_run_status}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<MetaStrip tiles={actionTiles(action)} />
|
||||
<div class="list-row__actions">
|
||||
<IconButton icon="mdiPlay" title={t('actions.execute')}
|
||||
onclick={() => executeAction(action.id)}
|
||||
disabled={executing[action.id]} />
|
||||
|
||||
Reference in New Issue
Block a user