ba199f24bd
- 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.
246 lines
7.5 KiB
TypeScript
246 lines
7.5 KiB
TypeScript
/**
|
|
* Singleton entity caches for all shared entities.
|
|
*
|
|
* Import from here in page components:
|
|
* import { providersCache, targetsCache } from '$lib/stores/caches.svelte';
|
|
*/
|
|
|
|
import { createEntityCache } from './entity-cache.svelte';
|
|
import { api } from '$lib/api';
|
|
import type {
|
|
ServiceProvider,
|
|
NotificationTarget,
|
|
Tracker,
|
|
TrackingConfig,
|
|
TemplateConfig,
|
|
TelegramBot,
|
|
EmailBot,
|
|
MatrixBot,
|
|
CommandConfig,
|
|
CommandTemplateConfig,
|
|
CommandTracker,
|
|
Action,
|
|
ReleaseStatus,
|
|
} from '$lib/types';
|
|
|
|
/** Service providers — used by Dashboard, Trackers, Command Trackers, Providers page. */
|
|
export const providersCache = createEntityCache<ServiceProvider>('/providers');
|
|
|
|
/** Notification targets — used by Trackers, Targets page. */
|
|
export const targetsCache = createEntityCache<NotificationTarget>('/targets');
|
|
|
|
/** Notification trackers — used by Dashboard, Trackers page. */
|
|
export const notificationTrackersCache = createEntityCache<Tracker>('/notification-trackers');
|
|
|
|
/** Tracking configs — used by Trackers, Tracking Configs page. */
|
|
export const trackingConfigsCache = createEntityCache<TrackingConfig>('/tracking-configs');
|
|
|
|
/** Template configs — used by Trackers, Template Configs page. */
|
|
export const templateConfigsCache = createEntityCache<TemplateConfig>('/template-configs');
|
|
|
|
/** Telegram bots — used by Targets, Command Trackers, Bots page. */
|
|
export const telegramBotsCache = createEntityCache<TelegramBot>('/telegram-bots');
|
|
|
|
/** Email bots — used by Targets, Bots page. */
|
|
export const emailBotsCache = createEntityCache<EmailBot>('/email-bots');
|
|
|
|
/** Matrix bots — used by Targets, Bots page. */
|
|
export const matrixBotsCache = createEntityCache<MatrixBot>('/matrix-bots');
|
|
|
|
/** Command configs — used by Command Trackers, Command Configs page. */
|
|
export const commandConfigsCache = createEntityCache<CommandConfig>('/command-configs');
|
|
|
|
/** Command template configs — used by Command Configs, Command Template Configs page. */
|
|
export const commandTemplateConfigsCache = createEntityCache<CommandTemplateConfig>('/command-template-configs');
|
|
|
|
/** Command trackers — used by Command Trackers page. */
|
|
export const commandTrackersCache = createEntityCache<CommandTracker>('/command-trackers');
|
|
|
|
/** Actions — used by Actions page. */
|
|
export const actionsCache = createEntityCache<Action>('/actions');
|
|
|
|
export interface SlotDef {
|
|
name: string;
|
|
description: string;
|
|
required?: boolean;
|
|
}
|
|
|
|
export interface CommandDef {
|
|
name: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface ActionTypeDef {
|
|
key: string;
|
|
name: string;
|
|
description?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface ProviderCapabilities {
|
|
notification_slots?: SlotDef[];
|
|
command_slots?: SlotDef[];
|
|
commands?: CommandDef[];
|
|
action_types?: ActionTypeDef[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export type CapabilitiesMap = Record<string, ProviderCapabilities>;
|
|
|
|
/** Provider capabilities — used by Template Configs, Command Configs.
|
|
* Dedups concurrent fetches so two fast navigations do not double-hit the API. */
|
|
export const capabilitiesCache = (() => {
|
|
let data = $state<CapabilitiesMap>({});
|
|
let fetchedAt = $state(0);
|
|
let inflight: Promise<CapabilitiesMap> | null = null;
|
|
const TTL = 60_000;
|
|
return {
|
|
get items() { return data; },
|
|
async fetch(force = false): Promise<CapabilitiesMap> {
|
|
if (!force && Object.keys(data).length > 0 && Date.now() - fetchedAt < TTL) return data;
|
|
if (inflight) return inflight;
|
|
inflight = (async () => {
|
|
try {
|
|
data = await api<CapabilitiesMap>('/providers/capabilities');
|
|
fetchedAt = Date.now();
|
|
return data;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
})();
|
|
return inflight;
|
|
},
|
|
};
|
|
})();
|
|
|
|
/** Configured external base URL — used to render absolute webhook URLs.
|
|
* Available to all authenticated users. Empty string when unset. */
|
|
export const externalUrlCache = (() => {
|
|
let data = $state<string>('');
|
|
let fetchedAt = $state(0);
|
|
let inflight: Promise<string> | null = null;
|
|
const TTL = 300_000;
|
|
return {
|
|
get value() { return data; },
|
|
invalidate() { fetchedAt = 0; },
|
|
async fetch(force = false): Promise<string> {
|
|
if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data;
|
|
if (inflight) return inflight;
|
|
inflight = (async () => {
|
|
try {
|
|
const res = await api<{ external_url: string }>('/settings/external-url');
|
|
data = (res?.external_url || '').replace(/\/+$/, '');
|
|
fetchedAt = Date.now();
|
|
return data;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
})();
|
|
return inflight;
|
|
},
|
|
};
|
|
})();
|
|
|
|
/** Upstream release status — drives the sidebar badge and Settings cassette. */
|
|
export const releaseStatusCache = (() => {
|
|
let data = $state<ReleaseStatus | null>(null);
|
|
let fetchedAt = $state(0);
|
|
let inflight: Promise<ReleaseStatus | null> | null = null;
|
|
// 5 min TTL — fresh enough that "Check now" feels instant on revisit,
|
|
// long enough that route changes don't hammer the endpoint.
|
|
const TTL = 300_000;
|
|
return {
|
|
get value() { return data; },
|
|
invalidate() { fetchedAt = 0; },
|
|
clear() {
|
|
data = null;
|
|
fetchedAt = 0;
|
|
inflight = null;
|
|
},
|
|
set(next: ReleaseStatus | null) {
|
|
data = next;
|
|
fetchedAt = Date.now();
|
|
},
|
|
async fetch(force = false): Promise<ReleaseStatus | null> {
|
|
if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data;
|
|
if (inflight) return inflight;
|
|
inflight = (async () => {
|
|
try {
|
|
data = await api<ReleaseStatus>('/settings/release');
|
|
fetchedAt = Date.now();
|
|
return data;
|
|
} catch {
|
|
// Swallow — the badge falls back to its default "no status" state.
|
|
return data;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
})();
|
|
return inflight;
|
|
},
|
|
};
|
|
})();
|
|
|
|
/** Supported template locales — fetched from app settings. */
|
|
export const supportedLocalesCache = (() => {
|
|
let data = $state<string[]>(['en', 'ru']);
|
|
let fetchedAt = $state(0);
|
|
let inflight: Promise<string[]> | null = null;
|
|
const TTL = 300_000;
|
|
return {
|
|
get items() { return data; },
|
|
async fetch(force = false): Promise<string[]> {
|
|
if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data;
|
|
if (inflight) return inflight;
|
|
inflight = (async () => {
|
|
try {
|
|
data = await api<string[]>('/settings/locales');
|
|
fetchedAt = Date.now();
|
|
return data;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
})();
|
|
return inflight;
|
|
},
|
|
};
|
|
})();
|
|
|
|
/**
|
|
* All caches keyed by entity type — for search palette and crosslink resolution.
|
|
*/
|
|
export const allCaches = {
|
|
providers: providersCache,
|
|
targets: targetsCache,
|
|
notification_trackers: notificationTrackersCache,
|
|
tracking_configs: trackingConfigsCache,
|
|
template_configs: templateConfigsCache,
|
|
telegram_bots: telegramBotsCache,
|
|
email_bots: emailBotsCache,
|
|
matrix_bots: matrixBotsCache,
|
|
command_configs: commandConfigsCache,
|
|
command_template_configs: commandTemplateConfigsCache,
|
|
command_trackers: commandTrackersCache,
|
|
actions: actionsCache,
|
|
} as const;
|
|
|
|
/**
|
|
* Fetch all caches in parallel. Used by search palette.
|
|
*/
|
|
export async function fetchAllCaches(): Promise<void> {
|
|
await Promise.all(Object.values(allCaches).map(c => c.fetch()));
|
|
}
|
|
|
|
/**
|
|
* Invalidate all entity caches. Useful on logout.
|
|
*
|
|
* Singleton state caches (release status, external URL, supported locales)
|
|
* live outside `allCaches` because their shape differs from entity caches —
|
|
* we clear them explicitly so a returning user as a different role can't
|
|
* briefly see the previous user's cached payload.
|
|
*/
|
|
export function clearAllCaches(): void {
|
|
Object.values(allCaches).forEach(c => c.clear());
|
|
releaseStatusCache.clear();
|
|
}
|