* chore: move @types/canvas-confetti to devDependencies, remove unused get-tsconfig direct dep * chore: configure knip with workspace entry points for all packages * refactor(shared): split 1119-line types.ts into domain modules under types/ * refactor: remove llm-service.ts shim, migrate all import sites to llm/service directly * refactor(settings): migrate 4 manually-resolved settings into conversion registry * refactor: split gmail-sync.ts into gmail-api, email-router, and thin orchestrator * refactor(orchestrator): extract useKeyboardShortcuts and usePipelineControls from OrchestratorPage Splits the 840-line OrchestratorPage into a thin orchestration shell (~480 lines) by extracting keyboard shortcut handling into useKeyboardShortcuts.ts and pipeline control logic into usePipelineControls.ts. Net negative line count across all files. * feat: create settings registry (Step 1) Introduces a single source of truth for all settings, combining schema definitions, default logic, parsing, and serialization into a single configuration object. * feat: derive schema, keys, and types from settings registry (Step 2) Derives AppSettings nested shape, SettingKey DB union, and updateSettingsSchema Zod shape automatically from the settings registry. * refactor: gut envSettings and remove settings-conversion (Step 3) Replaces manual env arrays with registry-driven maps in envSettings.ts. Deletes settings-conversion.ts since all parsing/defaults now live in the registry. * refactor: simplify getEffectiveSettings with generic loop (Step 4) Replaces ~334 lines of manual key-by-key unpacking with a generic registry-driven iteration loop (~40 lines). Models, typed, string, and virtual kinds are automatically derived. * refactor: simplify settingsUpdateRegistry (Step 5) Replaces ~350 lines of explicit per-key update handlers with a dynamic generic loop over the settings registry, properly routing persistence and side effects. * refactor(settings): implement nested settings registry and clean up tests - Migrate settings system to use a centralized nested registry (`settings-schema.ts`, `registry.ts`) - Remove obsolete flat-to-nested conversion logic (`settings-conversion.ts`) - Address Biome warnings by explicitly ignoring intentional `any` usage in generic runtime schema builder and registry logic - Clean up unused variables in test files (`SettingsPage.test.tsx`) to achieve a 100% green CI pipeline * refactor(settings): address PR comments on env data and registry parsing - Narrow `getEnvSettingsData` return type to `Partial<AppSettings>` to satisfy strict typing and omit 'typed' registry entries - Introduce `parseNonEmptyStringOrNull` for typed string settings so empty-string overrides cleanly fall back to defaults (matching original `||` logic) - Add missing unit tests for registry parse/serialize helpers (JSON, bools, numeric clamping)
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
import type { SettingKey } from "@server/repositories/settings";
|
|
import * as settingsRepo from "@server/repositories/settings";
|
|
import { settingsRegistry } from "@shared/settings-registry";
|
|
import type { AppSettings } from "@shared/types";
|
|
|
|
const envDefaults: Record<string, string | undefined> = { ...process.env };
|
|
|
|
export function normalizeEnvInput(
|
|
value: string | null | undefined,
|
|
): string | null {
|
|
const trimmed = value?.trim();
|
|
return trimmed ? trimmed : null;
|
|
}
|
|
|
|
export function applyEnvValue(envKey: string, value: string | null): void {
|
|
if (value === null) {
|
|
const fallback = envDefaults[envKey];
|
|
if (fallback === undefined) {
|
|
delete process.env[envKey];
|
|
} else {
|
|
process.env[envKey] = fallback;
|
|
}
|
|
return;
|
|
}
|
|
|
|
process.env[envKey] = value;
|
|
}
|
|
|
|
export async function applyStoredEnvOverrides(): Promise<void> {
|
|
const safeGetSetting = async (key: SettingKey): Promise<string | null> => {
|
|
try {
|
|
return await settingsRepo.getSetting(key);
|
|
} catch (error) {
|
|
const msg = String((error as Error)?.message ?? error);
|
|
if (msg.includes("no such table") && msg.includes("settings")) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const tasks = Object.entries(settingsRegistry).map(async ([key, def]) => {
|
|
if (!("envKey" in def) || !def.envKey) return;
|
|
const override = await safeGetSetting(key as SettingKey);
|
|
if (override === null) return;
|
|
applyEnvValue(def.envKey, normalizeEnvInput(override));
|
|
});
|
|
|
|
await Promise.all(tasks);
|
|
}
|
|
|
|
export async function getEnvSettingsData(
|
|
overrides?: Partial<Record<SettingKey, string>>,
|
|
): Promise<Partial<AppSettings>> {
|
|
const activeOverrides = overrides || (await settingsRepo.getAllSettings());
|
|
const values: Partial<AppSettings> = {};
|
|
|
|
for (const [key, def] of Object.entries(settingsRegistry)) {
|
|
if (def.kind === "typed") continue;
|
|
if (!("envKey" in def) || !def.envKey) continue;
|
|
|
|
const override = activeOverrides[key as SettingKey] ?? null;
|
|
const rawValue = override ?? process.env[def.envKey];
|
|
|
|
if (def.kind === "secret") {
|
|
const hintKey = `${key}Hint` as keyof AppSettings;
|
|
if (!rawValue) {
|
|
// biome-ignore lint/suspicious/noExplicitAny: explicit partial assignment
|
|
(values as any)[hintKey] = null;
|
|
continue;
|
|
}
|
|
const hintLength =
|
|
rawValue.length > 4 ? 4 : Math.max(rawValue.length - 1, 1);
|
|
// biome-ignore lint/suspicious/noExplicitAny: explicit partial assignment
|
|
(values as any)[hintKey] = rawValue.slice(0, hintLength);
|
|
} else {
|
|
// biome-ignore lint/suspicious/noExplicitAny: explicit partial assignment
|
|
(values as any)[key] = normalizeEnvInput(rawValue);
|
|
}
|
|
}
|
|
|
|
const basicAuthUser =
|
|
activeOverrides.basicAuthUser ?? process.env.BASIC_AUTH_USER;
|
|
const basicAuthPassword =
|
|
activeOverrides.basicAuthPassword ?? process.env.BASIC_AUTH_PASSWORD;
|
|
|
|
values.basicAuthActive = Boolean(basicAuthUser && basicAuthPassword);
|
|
|
|
return values;
|
|
}
|