Add database-backed runtime configuration for feature flags and TTS provider credentials, editable via a new admin UI panel. Replace static NEXT_PUBLIC_* and TTS provider env vars with admin-managed settings stored in the database and injected at SSR for client access. Implement admin-only panels for managing shared TTS provider credentials (with encrypted API keys) and live site feature flags. Add schema migrations, API routes, React contexts, and hooks for SSR-injected runtime config and live updates. Update client and server logic to resolve configuration from the database at runtime, enforcing admin restrictions and supporting migration from legacy env-based config. BREAKING CHANGE: Feature flags and TTS provider credentials are now managed at runtime via the admin UI. Environment variables are only used for initial seeding and are ignored after first boot. Existing deployments must migrate configuration to the admin panel.
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
|
|
/**
|
|
* Site-wide runtime config resolved at SSR time and injected via
|
|
* `window.__OPENREADER_RUNTIME_CONFIG__`. Replaces module-scope reads of
|
|
* `process.env.NEXT_PUBLIC_*` so admin edits take effect on the next page
|
|
* load without a redeploy. Read-only from the client; admin writes go
|
|
* through `/api/admin/settings` and trigger a reload.
|
|
*
|
|
* Keep this in sync with `RUNTIME_CONFIG_SCHEMA` on the server.
|
|
*/
|
|
export interface RuntimeConfig {
|
|
defaultTtsProvider: string;
|
|
defaultTtsModel: string;
|
|
restrictUserApiKeys: boolean;
|
|
enableTtsProvidersTab: boolean;
|
|
enableWordHighlight: boolean;
|
|
enableAudiobookExport: boolean;
|
|
enableDocxConversion: boolean;
|
|
enableDestructiveDeleteActions: boolean;
|
|
showAllDeepInfraModels: boolean;
|
|
}
|
|
|
|
const RUNTIME_DEFAULTS: RuntimeConfig = {
|
|
defaultTtsProvider: 'custom-openai',
|
|
defaultTtsModel: 'kokoro',
|
|
restrictUserApiKeys: true,
|
|
enableTtsProvidersTab: true,
|
|
enableWordHighlight: true,
|
|
enableAudiobookExport: true,
|
|
enableDocxConversion: true,
|
|
enableDestructiveDeleteActions: true,
|
|
showAllDeepInfraModels: true,
|
|
};
|
|
|
|
declare global {
|
|
// Injected via SSR in `src/app/layout.tsx`. Always defined in the browser.
|
|
interface Window {
|
|
__OPENREADER_RUNTIME_CONFIG__?: Partial<RuntimeConfig>;
|
|
}
|
|
}
|
|
|
|
function readInjectedConfig(): RuntimeConfig {
|
|
if (typeof window === 'undefined') return { ...RUNTIME_DEFAULTS };
|
|
const injected = window.__OPENREADER_RUNTIME_CONFIG__;
|
|
if (!injected || typeof injected !== 'object') return { ...RUNTIME_DEFAULTS };
|
|
return { ...RUNTIME_DEFAULTS, ...injected };
|
|
}
|
|
|
|
const RuntimeConfigContext = createContext<RuntimeConfig>({ ...RUNTIME_DEFAULTS });
|
|
|
|
export function RuntimeConfigProvider({
|
|
children,
|
|
value,
|
|
}: {
|
|
children: ReactNode;
|
|
/** Optional override (for tests). When omitted, reads from window. */
|
|
value?: RuntimeConfig;
|
|
}) {
|
|
const resolved = useMemo<RuntimeConfig>(() => value ?? readInjectedConfig(), [value]);
|
|
return <RuntimeConfigContext.Provider value={resolved}>{children}</RuntimeConfigContext.Provider>;
|
|
}
|
|
|
|
export function useRuntimeConfig(): RuntimeConfig {
|
|
return useContext(RuntimeConfigContext);
|
|
}
|
|
|
|
export function useFeatureFlag<K extends keyof RuntimeConfig>(key: K): RuntimeConfig[K] {
|
|
const cfg = useRuntimeConfig();
|
|
return cfg[key];
|
|
}
|
|
|
|
/**
|
|
* Synchronous accessor for modules that are loaded before the React tree
|
|
* mounts (e.g. Dexie initialization, config defaults). Falls back to the
|
|
* built-in defaults during SSR.
|
|
*/
|
|
export function readRuntimeConfigSync(): RuntimeConfig {
|
|
return readInjectedConfig();
|
|
}
|