feat(admin): DB-backed integration toggles + sticky cols + dark contrast
Builds on the env-var INTEGRATIONS_DISABLED shipped with the nav-design overhaul. Adds a DB-backed admin UI so operators can flip integrations without editing .env and restarting the container, plus the remaining visual cleanup items from the design backlog. Integration toggles - Migration 081 — integration_settings table (key PK, disabled flag, reason, disabled_by audit, disabled_at). Seeded with all 13 known integrations as enabled. - GET / PATCH /api/admin/integrations — gated by requirePermission (admin, access). PATCH clears the in-process integration-health cache so toggles take effect within seconds. - /admin/integrations admin page with a Switch per integration, optional reason input, audit-info subtitle (disabled by, when, why), live status light from /api/dashboard/integration-health. - integration-health service merges env-var disable list with DB rows; degrades gracefully if migration unapplied / DB unreachable. - Wired into the Admin nav dropdown (eight items now). - CLAUDE.md describes both env + DB sources. Sticky first column on tables - Table primitive accepts stickyFirstColumn?: boolean. When true, TH and TD :first-child stay pinned during horizontal scroll, with background inheritance preserving hover and selected row tints. - DataTable exposes the prop too — on by default for paginated tables. - /addigy-devices opts in. Dark-mode contrast - --border lifted from 10% to 14% in .dark; --input from 15% to 18%; --sidebar-border to 14%. - StatusLight outline ring lifted from /10 to /15 (light) and /20 (dark). - DetailModal empty-cell em-dash lifted from /40 to /70 so missing values are legible on dark surfaces. DESIGN.md - Closed sticky-first-column, dark-mode contrast, and palette-audit items (palette deprioritized — most uses are semantic). - Skeleton helpers documented as preferred for new code; existing ad-hoc patterns left in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab78e7bd4f
commit
e1427b62d7
13 changed files with 561 additions and 34 deletions
|
|
@ -252,12 +252,15 @@ function checkConfigOnly(
|
|||
}
|
||||
|
||||
/**
|
||||
* Operator-side disable list. Set INTEGRATIONS_DISABLED to a comma- or
|
||||
* space-separated list of integration keys (or aliases) to suppress them
|
||||
* from the /status page and the top-bar indicator. Disabled entries
|
||||
* render muted and don't count toward failure summaries.
|
||||
* Operator-side disable list. Two sources, merged:
|
||||
*
|
||||
* Aliases:
|
||||
* 1. INTEGRATIONS_DISABLED env var (legacy / bootstrap fallback) —
|
||||
* comma- or space-separated keys with aliases.
|
||||
* 2. integration_settings table (DB-backed, admin-toggleable at
|
||||
* /admin/integrations) — takes effect within the 5-minute health
|
||||
* cache without requiring a container restart.
|
||||
*
|
||||
* Aliases (env only — DB rows store canonical keys):
|
||||
* sentinelone, s1 → s1
|
||||
* datto, datto-rmm → datto_rmm
|
||||
* itglue, it-glue → itglue
|
||||
|
|
@ -277,7 +280,7 @@ const KEY_ALIASES: Record<string, string> = {
|
|||
'ms_graph': 'msgraph',
|
||||
};
|
||||
|
||||
function getDisabledKeys(): Set<string> {
|
||||
function getEnvDisabledKeys(): Set<string> {
|
||||
const raw = process.env.INTEGRATIONS_DISABLED;
|
||||
if (!raw) return new Set();
|
||||
return new Set(
|
||||
|
|
@ -289,9 +292,25 @@ function getDisabledKeys(): Set<string> {
|
|||
);
|
||||
}
|
||||
|
||||
function applyDisableOverlay(items: IntegrationHealth[]): IntegrationHealth[] {
|
||||
const disabled = getDisabledKeys();
|
||||
if (disabled.size === 0) return items;
|
||||
async function getDbDisabledKeys(): Promise<Set<string>> {
|
||||
// Lazy import to avoid pulling postgres-client into edge runtimes.
|
||||
const { default: postgresClient } = await import('@/lib/services/postgres-client');
|
||||
try {
|
||||
const res = await postgresClient.query<{ key: string }>(
|
||||
`SELECT key FROM integration_settings WHERE disabled = true`,
|
||||
);
|
||||
return new Set(res.rows.map((r) => r.key));
|
||||
} catch {
|
||||
// Migration not applied yet, or DB unreachable. Don't break health
|
||||
// checks — fall back to env-only behavior.
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
async function applyDisableOverlay(items: IntegrationHealth[]): Promise<IntegrationHealth[]> {
|
||||
const [envSet, dbSet] = [getEnvDisabledKeys(), await getDbDisabledKeys()];
|
||||
if (envSet.size === 0 && dbSet.size === 0) return items;
|
||||
const disabled = new Set([...envSet, ...dbSet]);
|
||||
return items.map((item) =>
|
||||
disabled.has(item.key)
|
||||
? { ...item, status: 'disabled', error: null, configured: false }
|
||||
|
|
@ -327,7 +346,7 @@ export async function checkIntegrationHealth(opts?: { skipCache?: boolean }): Pr
|
|||
Promise.resolve(checkConfigOnly('anthropic', 'Anthropic', 'llm',
|
||||
['ANTHROPIC_API_KEY'])),
|
||||
]);
|
||||
const overlaid = applyDisableOverlay(results);
|
||||
const overlaid = await applyDisableOverlay(results);
|
||||
cache = { expiresAt: Date.now() + CACHE_TTL_MS, data: overlaid };
|
||||
return overlaid;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue