Comprehensive admin + user dashboards (production-ready)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import "server-only";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* Feature flags with runtime effect.
|
||||
*
|
||||
* Flags are stored in the `feature_flag` table (admin-toggleable, no deploy) and
|
||||
* READ here to actually gate behavior. Known flags have a registry entry that
|
||||
* supplies a label, description, and a safe default when the row doesn't exist
|
||||
* yet. Admins may also create ad-hoc custom flags from the admin UI.
|
||||
*/
|
||||
export interface FlagDef {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
default: boolean;
|
||||
}
|
||||
|
||||
export const FLAG_REGISTRY: FlagDef[] = [
|
||||
{
|
||||
key: "signups_enabled",
|
||||
label: "Public sign-ups",
|
||||
description: "Allow new users to create an account. Turn off to pause registration.",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: "episode_generation_enabled",
|
||||
label: "Episode generation",
|
||||
description:
|
||||
"Master kill-switch for AI generation (new episodes, regenerations, series). Turn off during incidents or provider outages.",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: "ai_moderation_enabled",
|
||||
label: "AI content moderation",
|
||||
description:
|
||||
"Screen episode topics and generated scripts with OpenAI moderation; flag violations for review.",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: "maintenance_banner",
|
||||
label: "Maintenance banner",
|
||||
description: "Show a site-wide notice in the app that maintenance is in progress.",
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULTS = new Map(FLAG_REGISTRY.map((f) => [f.key, f.default]));
|
||||
|
||||
// Short per-process cache so hot paths (layout, generation) don't hit the DB on
|
||||
// every request. Admin toggles propagate within TTL_MS; the admin UI itself
|
||||
// reads fresh (uncached) data via getAllFlags().
|
||||
const TTL_MS = 10_000;
|
||||
let cache: { at: number; map: Map<string, boolean> } | null = null;
|
||||
|
||||
async function loadFlags(): Promise<Map<string, boolean>> {
|
||||
if (cache && Date.now() - cache.at < TTL_MS) return cache.map;
|
||||
const rows = await prisma.featureFlag.findMany({ select: { key: true, enabled: true } });
|
||||
const map = new Map<string, boolean>(rows.map((r) => [r.key, r.enabled]));
|
||||
cache = { at: Date.now(), map };
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Resolve a flag: DB value if present, else the registry default (else false). */
|
||||
export async function isFlagEnabled(key: string): Promise<boolean> {
|
||||
const map = await loadFlags();
|
||||
if (map.has(key)) return map.get(key)!;
|
||||
return DEFAULTS.get(key) ?? false;
|
||||
}
|
||||
|
||||
/** Invalidate the in-process cache (call after an admin toggles a flag). */
|
||||
export function bustFlagCache(): void {
|
||||
cache = null;
|
||||
}
|
||||
|
||||
export interface FlagView {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
known: boolean;
|
||||
}
|
||||
|
||||
/** Fresh (uncached) merged view of known + custom flags for the admin screen. */
|
||||
export async function getAllFlags(): Promise<FlagView[]> {
|
||||
const rows = await prisma.featureFlag.findMany();
|
||||
const byKey = new Map(rows.map((r) => [r.key, r.enabled]));
|
||||
const known: FlagView[] = FLAG_REGISTRY.map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
description: f.description,
|
||||
enabled: byKey.get(f.key) ?? f.default,
|
||||
known: true,
|
||||
}));
|
||||
const custom: FlagView[] = rows
|
||||
.filter((r) => !DEFAULTS.has(r.key))
|
||||
.map((r) => ({
|
||||
key: r.key,
|
||||
label: r.key,
|
||||
description: "Custom flag (no registry entry — not read by application code).",
|
||||
enabled: r.enabled,
|
||||
known: false,
|
||||
}));
|
||||
return [...known, ...custom];
|
||||
}
|
||||
Reference in New Issue
Block a user