feat(settings): painel central de configurações (API estável + versão)

O ícone de configurações deixa de abrir popover flutuante e passa a abrir um
painel grande na área central (no lugar das mensagens). O painel mostra apenas:
- "API estável" (= engine Baileys 7 oficial/baileys7, a que conecta), com ação
  de ativar na instância;
- a versão do sistema (lida de /version.json).
ThinSidebar simplificada (sem o seletor flutuante); as engines seguem
selecionáveis no backend para teste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-06-28 05:07:50 +02:00
parent 5170a31aac
commit c8c58c3a35
3 changed files with 138 additions and 166 deletions
@@ -0,0 +1,129 @@
import React, { useEffect, useState, useCallback } from 'react';
import { ShieldCheck, Check, Loader2, Tag } from 'lucide-react';
interface SettingsPanelProps {
instanceId: string | null;
}
// "API estável" aponta para a engine Baileys 7 oficial (a que conecta de fato).
const STABLE_ENGINE = 'baileys7';
export default function SettingsPanel({ instanceId }: SettingsPanelProps) {
const [engine, setEngine] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [version, setVersion] = useState<any>(null);
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8008';
const authHeader = (): Record<string, string> => {
const t = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
return t ? { Authorization: `Bearer ${t}` } : {};
};
// Versão do sistema (servida em /version.json)
useEffect(() => {
fetch('/version.json')
.then((r) => (r.ok ? r.json() : null))
.then(setVersion)
.catch(() => {});
}, []);
// Engine atual da instância
useEffect(() => {
if (!instanceId) return;
setLoading(true);
fetch(`${API_URL}/api/instances`, { headers: authHeader() })
.then((r) => (r.ok ? r.json() : []))
.then((list) => {
const inst = Array.isArray(list) ? list.find((i: any) => i.id === instanceId) : null;
setEngine(inst?.engine ?? null);
})
.catch(() => {})
.finally(() => setLoading(false));
}, [instanceId]);
const ativarEstavel = useCallback(async () => {
if (!instanceId || saving) return;
setSaving(true);
setError(null);
try {
const res = await fetch(`${API_URL}/api/instances/${instanceId}/engine`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...authHeader() },
body: JSON.stringify({ engine: STABLE_ENGINE }),
});
if (res.status === 429) {
const d = await res.json().catch(() => ({}));
throw new Error(d.error ?? 'Aguarde o intervalo entre trocas.');
}
if (!res.ok) throw new Error('Falha ao ativar a API estável.');
setEngine(STABLE_ENGINE);
} catch (e: any) {
setError(e?.message ?? 'Erro ao ativar.');
} finally {
setSaving(false);
}
}, [instanceId, saving]);
const ativa = engine === STABLE_ENGINE;
return (
<div className="relative z-[1] flex-1 h-full overflow-y-auto bg-[#f0f2f5] p-6 md:p-10">
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-light text-[#41525d] mb-8">Configurações</h1>
{/* ── API estável ─────────────────────────────────────────── */}
<div className={`rounded-2xl border-2 bg-white p-8 mb-6 transition-colors ${ativa ? 'border-emerald-400' : 'border-[#e9edef]'}`}>
<div className="flex items-center gap-4 mb-2">
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center ${ativa ? 'bg-emerald-50' : 'bg-[#f0f2f5]'}`}>
<ShieldCheck className={`w-8 h-8 ${ativa ? 'text-emerald-500' : 'text-[#54656f]'}`} />
</div>
<div className="flex-1">
<h2 className="text-2xl font-semibold text-[#111b21]">API estável</h2>
<p className="text-base text-[#667781]">Conexão recomendada do WhatsApp</p>
</div>
{ativa && (
<span className="flex items-center gap-1 text-emerald-600 font-medium text-lg">
<Check className="w-6 h-6" /> Ativa
</span>
)}
</div>
{!instanceId ? (
<p className="text-base text-[#667781] mt-2">Selecione uma instância primeiro.</p>
) : ativa ? (
<p className="text-base text-emerald-600 mt-2">A API estável está ativa nesta instância.</p>
) : (
<button
onClick={ativarEstavel}
disabled={saving}
className="mt-3 inline-flex items-center gap-2 rounded-xl bg-emerald-500 hover:bg-emerald-600 text-white text-lg font-medium px-6 py-3 disabled:opacity-60"
>
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <ShieldCheck className="w-5 h-5" />}
Ativar API estável
</button>
)}
{loading && <p className="text-sm text-[#8696a0] mt-3">Carregando configuração</p>}
{error && <p className="text-sm text-red-500 mt-3">{error}</p>}
</div>
{/* ── Versão ──────────────────────────────────────────────── */}
<div className="rounded-2xl border-2 border-[#e9edef] bg-white p-8">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-[#f0f2f5] flex items-center justify-center">
<Tag className="w-8 h-8 text-[#54656f]" />
</div>
<div className="flex-1">
<h2 className="text-2xl font-semibold text-[#111b21]">Versão</h2>
<p className="text-3xl font-light text-[#41525d] mt-1">{version?.version ?? '—'}</p>
{version?.buildCount != null && (
<p className="text-base text-[#667781]">build {version.buildCount}</p>
)}
</div>
</div>
</div>
</div>
</div>
);
}