Shaheer Sarfaraz b18c2eccbb
Code cleanup (#218)
* 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)
2026-02-21 03:07:51 +00:00

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;
}