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:
@@ -20,6 +20,7 @@ import type {
|
||||
CommandTemplateConfig,
|
||||
CommandTracker,
|
||||
Action,
|
||||
ReleaseStatus,
|
||||
} from '$lib/types';
|
||||
|
||||
/** Service providers — used by Dashboard, Trackers, Command Trackers, Providers page. */
|
||||
@@ -140,6 +141,46 @@ export const externalUrlCache = (() => {
|
||||
};
|
||||
})();
|
||||
|
||||
/** 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']);
|
||||
@@ -192,7 +233,13 @@ export async function fetchAllCaches(): Promise<void> {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user