- Remove legacy TTS rate limit environment variables and migrate all related configuration to runtime settings managed via the Admin UI - Add runtime config keys for TTS rate limiting enable/disable and per-user/IP daily quotas - Refactor rate limiter and API routes to use runtime config for thresholds and enablement - Update AdminFeaturesPanel to allow editing TTS rate limiting and quota values in the UI - Add shared-provider-selection utility for consistent provider selection logic - Update documentation to reflect new runtime/admin configuration and remove obsolete env var docs - Add unit tests for rate limit runtime settings and provider selection BREAKING CHANGE: TTS rate limiting is now controlled via Admin → Site features; environment variables for TTS rate limiting are no longer supported and will be ignored.
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
|
|
/**
|
|
* Site-wide runtime config resolved at SSR time and injected via
|
|
* `window.__RUNTIME_CONFIG__`. Replaces module-scope reads of
|
|
* build-time public env flags 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;
|
|
changelogFeedUrl: string;
|
|
appVersion: string;
|
|
enableUserSignups: boolean;
|
|
restrictUserApiKeys: boolean;
|
|
enableTtsProvidersTab: boolean;
|
|
enableAudiobookExport: boolean;
|
|
enableDocxConversion: boolean;
|
|
enableDestructiveDeleteActions: boolean;
|
|
showAllProviderModels: boolean;
|
|
disableTtsRateLimit: boolean;
|
|
ttsDailyLimitAnonymous: number;
|
|
ttsDailyLimitAuthenticated: number;
|
|
ttsIpDailyLimitAnonymous: number;
|
|
ttsIpDailyLimitAuthenticated: number;
|
|
computeAvailable: boolean;
|
|
}
|
|
|
|
const RUNTIME_DEFAULTS: RuntimeConfig = {
|
|
defaultTtsProvider: 'custom-openai',
|
|
changelogFeedUrl: 'https://docs.openreader.richardr.dev/changelog/manifest.json',
|
|
appVersion: '0.0.0',
|
|
enableUserSignups: true,
|
|
restrictUserApiKeys: true,
|
|
enableTtsProvidersTab: true,
|
|
enableAudiobookExport: true,
|
|
enableDocxConversion: true,
|
|
enableDestructiveDeleteActions: true,
|
|
showAllProviderModels: true,
|
|
disableTtsRateLimit: true,
|
|
ttsDailyLimitAnonymous: 50_000,
|
|
ttsDailyLimitAuthenticated: 500_000,
|
|
ttsIpDailyLimitAnonymous: 100_000,
|
|
ttsIpDailyLimitAuthenticated: 1_000_000,
|
|
computeAvailable: true,
|
|
};
|
|
|
|
declare global {
|
|
// Injected via SSR in `src/app/layout.tsx`. Always defined in the browser.
|
|
interface Window {
|
|
__RUNTIME_CONFIG__?: Partial<RuntimeConfig>;
|
|
}
|
|
}
|
|
|
|
function readInjectedConfig(): RuntimeConfig {
|
|
if (typeof window === 'undefined') return { ...RUNTIME_DEFAULTS };
|
|
const injected = window.__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();
|
|
}
|