feat(service-integrations): phase 2 — integration widget & app form UI

- Add 6 renderer components: StatCard, Gauge, List, Progress, AlertBanner, Chart
- Add IntegrationWidget container with auto-refresh, loading, error states
- Add IntegrationAlertOverlay for layout-level critical alerts
- Add IntegrationConfigFields for dynamic form generation from Zod schemas
- Register integration type in WidgetRenderer
- Extend WidgetCreationForm with integration app/endpoint pickers
- Extend AppForm with integration config section and test connection button
- Add /api/integrations/alerts endpoint
This commit is contained in:
2026-03-25 22:07:51 +03:00
parent 114dee57a8
commit 50e8519220
25 changed files with 1360 additions and 1 deletions
@@ -0,0 +1,55 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { success } from '$lib/server/utils/response.js';
import { prisma } from '$lib/server/prisma.js';
import * as registry from '$lib/server/integrations/registry.js';
import * as cache from '$lib/server/integrations/cache.js';
import { tryDecrypt } from '$lib/server/integrations/encryption.js';
import type { AlertBannerData } from '$lib/server/integrations/types.js';
export const GET: RequestHandler = async () => {
try {
const apps = await prisma.app.findMany({
where: { integrationEnabled: true, integrationType: { not: null } }
});
const alerts: AlertBannerData[] = [];
for (const app of apps) {
const integration = registry.get(app.integrationType!);
if (!integration) continue;
const alertEndpoints = integration.endpoints.filter((ep) => ep.renderer === 'alert-banner');
if (alertEndpoints.length === 0) continue;
const configJson = tryDecrypt(app.integrationConfig);
const config = configJson ? JSON.parse(configJson) : {};
for (const endpoint of alertEndpoints) {
const cacheKey = `${app.id}:${endpoint.id}`;
const cached = cache.get<AlertBannerData>(cacheKey);
if (cached) {
alerts.push(cached);
continue;
}
try {
const data = await integration.fetchData(app.url, config, endpoint.id);
if (data.renderer === 'alert-banner') {
const alertData = data.data as AlertBannerData;
cache.set(cacheKey, alertData, endpoint.refreshInterval);
if (alertData.severity === 'warning' || alertData.severity === 'critical') {
alerts.push(alertData);
}
}
} catch {
// Skip failed alert fetches
}
}
}
return json(success(alerts));
} catch {
return json(success([]));
}
};