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:
2026-05-12 02:58:07 +03:00
parent bb5afcc222
commit ba199f24bd
47 changed files with 5627 additions and 290 deletions
+47
View File
@@ -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();
}