feat: migrate frontend to Astro + expand AI moderation + backend admin/runtime config

Frontend:
- migrate from Vite to Astro (astro.config.mjs, pages/, layouts/)
- add admin panel, settings page, command palette, error boundary
- refactor App.tsx, MascotChatbot, Sidebar, Header, DashboardLayout
- update API client, WebSocket, auth, dashboard features

Backend:
- add admin module and config routes
- refactor middlewares, Redis connection, WebSocket server/bridge
- add runtime config loader

Discord Gateway:
- refactor AI moderation: circuit breaker, concurrency limiter, fallback processor
- add media analysis client, Seaxng search, user profile learner
- add new drizzle migration

Shared:
- extend database schema, add new config fields
This commit is contained in:
asepharyana
2026-07-02 00:02:41 +07:00
parent d5c22a3959
commit d59b59a7a7
91 changed files with 11165 additions and 674 deletions
@@ -0,0 +1,110 @@
/**
* Runtime configuration manager.
*
* Stores settings that can change at runtime (e.g., DASHBOARD_IS_PUBLIC)
* in a JSON file. Falls back to env-based defaults from the static config.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { createChildLogger } from "@bete/shared/logger";
import type { config } from "./index.js";
type Config = typeof config;
const logger = createChildLogger("runtime-config");
const DATA_DIR = resolve(import.meta.dirname ?? process.cwd(), "..", "data");
const SETTINGS_FILE = resolve(DATA_DIR, "settings.json");
interface RuntimeSettings {
dashboardIsPublic: boolean;
}
/** Nilai fallback dari env. Dipakai saat settings.json belum pernah dibuat. */
function envDefaultSettings(): RuntimeSettings {
return {
dashboardIsPublic: process.env.DASHBOARD_IS_PUBLIC === "true",
};
}
function ensureDataDir(): void {
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true });
}
}
function loadSettings(): RuntimeSettings {
try {
ensureDataDir();
const fallback = envDefaultSettings();
if (!existsSync(SETTINGS_FILE)) {
writeFileSync(SETTINGS_FILE, JSON.stringify(fallback, null, 2));
return { ...fallback };
}
const raw = readFileSync(SETTINGS_FILE, "utf-8");
const parsed = JSON.parse(raw) as Partial<RuntimeSettings>;
return { ...fallback, ...parsed };
} catch (err) {
logger.error({ err }, "Failed to load runtime settings");
return envDefaultSettings();
}
}
function saveSettings(settings: RuntimeSettings): void {
try {
ensureDataDir();
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
} catch (err) {
logger.error({ err }, "Failed to save runtime settings");
}
}
// ─── Singleton ───────────────────────────────────────────────────────────────
let _cache: RuntimeSettings | null = null;
function getSettings(): RuntimeSettings {
if (!_cache) {
_cache = loadSettings();
}
return _cache;
}
function invalidateCache(): void {
_cache = null;
}
// ─── Public API ──────────────────────────────────────────────────────────────
/**
* Whether the dashboard is publicly accessible without auth, using runtime
* override if available, otherwise falling back to the env-based static config.
*/
export function isDashboardPublic(staticConfig?: Config): boolean {
const runtime = getSettings();
return runtime.dashboardIsPublic;
}
export function getRuntimeSettings(): RuntimeSettings {
return { ...getSettings() };
}
/**
* Update runtime settings. Pass only the fields you want to change.
* Invalidates the internal cache so the next read picks up changes.
*/
export function updateRuntimeSettings(
patch: Partial<RuntimeSettings>,
): RuntimeSettings {
const current = getSettings();
const updated = { ...current, ...patch };
saveSettings(updated);
invalidateCache();
return { ...updated };
}
/**
* Reset runtime settings to env-based defaults (does NOT change the file).
*/
export function resetRuntimeSettings(): void {
invalidateCache();
}