/* Tab: Configuración - App personal (sin billing, sin danger zone) */ function SettingsTab({ authUser }) { const isAdmin = authUser?.is_admin; const [section, setSection] = React.useState("account"); const sections = [ { id: "account", label: "Cuenta", Icon: IconUser, adminOnly: false }, { id: "appearance", label: "Apariencia", Icon: IconPalette, adminOnly: false }, { id: "notif", label: "Notificaciones", Icon: IconBell, adminOnly: false }, { id: "api", label: "API Keys", Icon: IconKey, adminOnly: true }, { id: "service", label: "Servicio", Icon: IconSparkle, adminOnly: true }, { id: "status", label: "System Status", Icon: IconWave, adminOnly: true }, { id: "logs", label: "Logs", Icon: IconScript, adminOnly: true }, { id: "reset", label: "Resetear app", Icon: IconTrash, adminOnly: true }, ].filter((s) => !s.adminOnly || isAdmin); return (
{section === "account" && } {section === "api" && } {section === "appearance" && } {section === "notif" && } {section === "service" && } {section === "status" && } {section === "logs" && } {section === "reset" && }
); } function AccountSection({ authUser, isAdmin }) { const username = authUser?.username || authUser || "—"; const email = authUser?.email || "—"; return ( {isAdmin && (
Rol
ADMINISTRADOR
)}
); } const API_KEYS_CONFIG = [ { id: "anthropic", name: "Claude (Anthropic)", icon: "C", color: "#cc785c", placeholder: "sk-ant-…" }, { id: "nanobanana", name: "Nanobanana", icon: "🍌", color: "#fbbf24", placeholder: "nb-…" }, { id: "ai33", name: "ai33 (voz)", icon: "33", color: "#60a5fa", placeholder: "ai33-…" }, { id: "lemonfox", name: "LemonFox", icon: "🍋", color: "#84cc16", placeholder: "lf-…" }, ]; const STATUS_DOT = { null: { color: "var(--border-strong)", glow: "none", label: "Sin comprobar" }, checking: { color: "var(--yellow)", glow: "none", label: "Comprobando…" }, ok: { color: "var(--green)", glow: "0 0 7px var(--green)", label: "Operativa" }, warn: { color: "var(--yellow)", glow: "0 0 7px var(--yellow)", label: "Inestable" }, error: { color: "var(--red)", glow: "0 0 7px var(--red)", label: "Sin conexión" }, }; function ApiSection() { const [keys, setKeys] = React.useState(() => Object.fromEntries(API_KEYS_CONFIG.map((k) => [k.id, ""])) ); const [visible, setVisible] = React.useState({}); const [saved, setSaved] = React.useState({}); const [health, setHealth] = React.useState({}); const [loading, setLoading] = React.useState(true); React.useEffect(() => { fetch("/api/settings/keys", { credentials: "same-origin" }) .then((r) => r.json()) .then((d) => { if (d) setKeys((prev) => ({ ...prev, ...d })); }) .catch(() => {}) .finally(() => setLoading(false)); }, []); async function save(id) { try { await fetch("/api/settings/keys", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, value: keys[id] }), }); setSaved((s) => ({ ...s, [id]: true })); setTimeout(() => setSaved((s) => ({ ...s, [id]: false })), 2000); } catch {} } async function checkOne(id) { setHealth((h) => ({ ...h, [id]: "checking" })); try { const r = await fetch("/api/settings/keys/check", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }), }); const d = await r.json(); setHealth((h) => ({ ...h, [id]: d.status || "error" })); } catch { setHealth((h) => ({ ...h, [id]: "error" })); } } async function checkAll() { for (const k of API_KEYS_CONFIG) { if (keys[k.id]) checkOne(k.id); } } return ( {loading ? (
Cargando…
) : ( <>
{API_KEYS_CONFIG.map((k) => { const st = STATUS_DOT[health[k.id] ?? null]; const isChecking = health[k.id] === "checking"; return (
{k.icon}
{k.name}
{st.label}
{keys[k.id] && health[k.id] == null && Configurada}
{ setKeys((prev) => ({ ...prev, [k.id]: e.target.value })); setHealth((h) => ({ ...h, [k.id]: null })); }} placeholder={k.placeholder} style={{ paddingRight: 36, fontFamily: "var(--font-mono)", fontSize: 12 }} />
); })} )}
); } const ACCENT_PALETTE = [ { hex: "#7c5cff", h2: "#a78bfa", r: 124, g: 92, b: 255 }, { hex: "#4ade80", h2: "#86efac", r: 74, g: 222, b: 128 }, { hex: "#60a5fa", h2: "#93c5fd", r: 96, g: 165, b: 250 }, { hex: "#f87171", h2: "#fca5a5", r: 248, g: 113, b: 113 }, { hex: "#fbbf24", h2: "#fcd34d", r: 251, g: 191, b: 36 }, ]; function applyAccent(a) { const root = document.documentElement.style; root.setProperty("--accent", a.hex); root.setProperty("--accent-2", a.h2); root.setProperty("--accent-soft", `rgba(${a.r},${a.g},${a.b},0.14)`); root.setProperty("--accent-softer", `rgba(${a.r},${a.g},${a.b},0.08)`); root.setProperty("--accent-glow", `0 0 24px rgba(${a.r},${a.g},${a.b},0.35)`); } function applyTheme(theme) { const resolved = theme === "auto" ? (window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : theme; document.documentElement.setAttribute("data-theme", resolved); } function AppearanceSection() { const [theme, setTheme] = React.useState(() => localStorage.getItem("vc-theme") || "dark"); const [accent, setAccent] = React.useState(() => localStorage.getItem("vc-accent") || "#7c5cff"); const [autosave, setAutosave] = React.useState(true); React.useEffect(() => { applyTheme(theme); localStorage.setItem("vc-theme", theme); }, [theme]); React.useEffect(() => { const a = ACCENT_PALETTE.find((c) => c.hex === accent) || ACCENT_PALETTE[0]; applyAccent(a); localStorage.setItem("vc-accent", accent); }, [accent]); return ( {[["dark","Dark"],["light","Light"],["auto","Auto"]].map(([id, label]) => ( ))} }/> {ACCENT_PALETTE.map((c) => (
setAccent(c.hex)}/> ))}
}/> setAutosave(!autosave)}/>}/>
); } function NotifSection() { const [a, setA] = React.useState(true); const [b, setB] = React.useState(true); const [c, setC] = React.useState(false); const [d, setD] = React.useState(true); return ( setA(!a)}/>}/> setB(!b)}/>}/> setD(!d)}/>}/> setC(!c)}/>}/> ); } function ServiceSection() { const [restarting, setRestarting] = React.useState(false); async function restart() { if (!confirm("¿Reiniciar el servidor? Las generaciones en curso se interrumpirán.")) return; setRestarting(true); try { await fetch("/api/service/restart", { method: "POST", credentials: "same-origin" }); setTimeout(() => { window.location.reload(); }, 3000); } catch { setRestarting(false); } } return (
Estado
VIRAL CODE Dashboard v3.0
{restarting ? "Reiniciando…" : "En ejecución"}
Reiniciar servicio
Corta cualquier trabajo en curso y vuelve a arrancar el pipeline. Útil si algo se ha quedado colgado.
); } function StatusSection() { const [stats, setStats] = React.useState(null); React.useEffect(() => { function load() { fetch("/api/system/stats", { credentials: "same-origin" }) .then((r) => r.json()).then(setStats).catch(() => {}); } load(); const id = setInterval(load, 5000); return () => clearInterval(id); }, []); const services = [ { name: "Claude (Anthropic)", latency: "—" }, { name: "Nanobanana", latency: "—" }, { name: "ai33 (voz)", latency: "—" }, { name: "LemonFox", latency: "—" }, { name: "FFmpeg worker", latency: "local" }, ]; const resources = stats ? [ { k: "CPU", v: stats.cpu, suffix: "%", note: null }, { k: "RAM", v: stats.ram.percent, suffix: "%", note: `${stats.ram.used_gb} / ${stats.ram.total_gb} GB` }, { k: "Disco", v: stats.disk.percent, suffix: "%", note: `${stats.disk.used_gb} / ${stats.disk.total_gb} GB` }, ] : []; return ( <> {services.map((s) => (
{s.name}
{s.latency} Conectado
))}
{!stats ? (
Cargando…
) : resources.map((r) => (
{r.k} {r.note && · {r.note}}
80 ? "var(--yellow)" : "var(--text-2)" }}> {r.v}{r.suffix}
80 ? "linear-gradient(90deg,#fbbf24,#f97316)" : "linear-gradient(90deg,#7c5cff,#a78bfa)", }}/>
))} ); } function LogsSection() { const [filter, setFilter] = React.useState("all"); const [logs, setLogs] = React.useState([]); React.useEffect(() => { function load() { fetch("/api/logs?limit=100", { credentials: "same-origin" }) .then((r) => r.json()).then((d) => { if (d.logs) setLogs([...d.logs].reverse()); }).catch(() => {}); } load(); const id = setInterval(load, 5000); return () => clearInterval(id); }, []); const filtered = filter === "all" ? logs : logs.filter((l) => l.lvl === filter); return (

Logs del sistema

Últimas operaciones en orden cronológico inverso.

{[{id:"all",l:"Todos"},{id:"info",l:"Info"},{id:"warn",l:"Warn"},{id:"error",l:"Error"}].map((o) => ( ))}
{filtered.map((l, i) => (
{l.t} {l.lvl} {l.src} {l.msg}
))}
); } function ResetSection() { const [confirmOpen, setConfirmOpen] = React.useState(false); const [scope, setScope] = React.useState(new Set(["cache"])); const opts = [ { id: "cache", l: "Caché de generaciones", d: "Libera espacio. Seguro." }, { id: "logs", l: "Logs antiguos", d: "Elimina logs con más de 7 días." }, { id: "history", l: "Historial de videos", d: "Elimina el registro, no los archivos de YouTube." }, { id: "channels", l: "Configuración de canales", d: "Vuelve a los valores por defecto." }, { id: "all", l: "Resetear TODO", d: "Estado de fábrica. API keys incluidas." }, ]; function toggle(id) { const s = new Set(scope); s.has(id) ? s.delete(id) : s.add(id); setScope(s); } return (
{opts.map((o) => ( ))}
{confirmOpen && (
setConfirmOpen(false)}>
e.stopPropagation()}>

¿Seguro que quieres resetear?

Vas a limpiar: {Array.from(scope).map((id) => opts.find((o) => o.id === id).l).join(", ")}. Esto no se puede deshacer.

)}
); } function SettingsGroup({ title, desc, children }) { return (

{title}

{desc &&

{desc}

}
{children}
); } function SettingsRow({ title, desc, control }) { return (
{title}
{desc &&
{desc}
}
{control}
); } function Field({ label, value, readOnly }) { const [v, setV] = React.useState(value); return (
!readOnly && setV(e.target.value)} readOnly={readOnly} style={{ padding: "8px 10px", opacity: readOnly ? 0.7 : 1 }}/>
); } Object.assign(window, { SettingsTab });