mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 12:36:50 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
885b2ebe1f
commit
9e6c8a482e
1 changed files with 47 additions and 41 deletions
|
|
@ -9,8 +9,8 @@ import {
|
||||||
Loader2,
|
Loader2,
|
||||||
PlayCircle,
|
PlayCircle,
|
||||||
PauseCircle,
|
PauseCircle,
|
||||||
RotateCw,
|
|
||||||
Server,
|
Server,
|
||||||
|
Settings,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
|
@ -43,6 +43,8 @@ interface AgentRow {
|
||||||
vps_pool_id: string | null;
|
vps_pool_id: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
provisioned_at: string | null;
|
provisioned_at: string | null;
|
||||||
|
profile: { full_name: string | null } | null;
|
||||||
|
subscription: { plans: { slug: string; name: string } | null } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AdminPage() {
|
function AdminPage() {
|
||||||
|
|
@ -65,18 +67,30 @@ function AdminPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: agents, isLoading: agentsLoading } = useQuery({
|
const { data: agents, isLoading: agentsLoading } = useQuery({
|
||||||
queryKey: ["admin-agents"],
|
queryKey: ["agents-admin"],
|
||||||
enabled: !!isAdmin,
|
enabled: !!isAdmin,
|
||||||
|
refetchInterval: 15000,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("agent_instances")
|
.from("agent_instances")
|
||||||
.select(
|
.select(
|
||||||
"id, user_id, status, uuid_tenant, telegram_bot_username, telegram_bot_token_vault_id, railway_service_id, vps_pool_id, created_at, provisioned_at",
|
`id, user_id, status, uuid_tenant, telegram_bot_username, telegram_bot_token_vault_id,
|
||||||
|
railway_service_id, vps_pool_id, created_at, provisioned_at,
|
||||||
|
profile:profiles!agent_instances_user_id_fkey(full_name),
|
||||||
|
subscription:subscriptions!subscriptions_user_id_fkey(plans(slug, name))`,
|
||||||
)
|
)
|
||||||
.order("created_at", { ascending: false })
|
.order("created_at", { ascending: false })
|
||||||
.limit(100);
|
.limit(100);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return data as AgentRow[];
|
// Pegar apenas a subscription ativa (primeira) — o relacionamento retorna array
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
return (data as any[]).map((a) => ({
|
||||||
|
...a,
|
||||||
|
profile: Array.isArray(a.profile) ? a.profile[0] ?? null : a.profile,
|
||||||
|
subscription: Array.isArray(a.subscription)
|
||||||
|
? a.subscription.find((s: { plans: unknown }) => s.plans) ?? a.subscription[0] ?? null
|
||||||
|
: a.subscription,
|
||||||
|
})) as AgentRow[];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -115,10 +129,7 @@ function AdminPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function action(
|
async function action(fn: "suspend-agent" | "resume-agent", agentId: string) {
|
||||||
fn: "provision-agent" | "suspend-agent" | "resume-agent",
|
|
||||||
agentId: string,
|
|
||||||
) {
|
|
||||||
setBusy(agentId + fn);
|
setBusy(agentId + fn);
|
||||||
const { data, error } = await invokeFunction<{ ok?: boolean; error?: string }>(fn, {
|
const { data, error } = await invokeFunction<{ ok?: boolean; error?: string }>(fn, {
|
||||||
agent_instance_id: agentId,
|
agent_instance_id: agentId,
|
||||||
|
|
@ -130,20 +141,20 @@ function AdminPage() {
|
||||||
toast.error(`${fn}: ${data.error}`);
|
toast.error(`${fn}: ${data.error}`);
|
||||||
} else {
|
} else {
|
||||||
toast.success(`${fn} executado com sucesso`);
|
toast.success(`${fn} executado com sucesso`);
|
||||||
queryClient.invalidateQueries({ queryKey: ["admin-agents"] });
|
queryClient.invalidateQueries({ queryKey: ["agents-admin"] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background px-4 sm:px-8 py-8">
|
<div className="min-h-screen bg-background px-4 sm:px-8 py-8">
|
||||||
<div className="mx-auto max-w-6xl space-y-6">
|
<div className="mx-auto max-w-7xl space-y-6">
|
||||||
<header className="flex items-start justify-between gap-4 flex-wrap">
|
<header className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
|
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
|
||||||
<Server className="h-7 w-7 text-primary" /> Admin · Mika
|
<Server className="h-7 w-7 text-primary" /> Admin · Mika
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Gerencie agentes provisionados, suspenda e reative containers Railway.
|
Configure e gerencie agentes provisionados.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button asChild variant="outline" size="sm">
|
||||||
|
|
@ -167,7 +178,9 @@ function AdminPage() {
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Tenant</TableHead>
|
<TableHead>Tenant</TableHead>
|
||||||
|
<TableHead>Cliente</TableHead>
|
||||||
<TableHead>Bot</TableHead>
|
<TableHead>Bot</TableHead>
|
||||||
|
<TableHead>Plano</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Railway</TableHead>
|
<TableHead>Railway</TableHead>
|
||||||
<TableHead className="text-right">Ações</TableHead>
|
<TableHead className="text-right">Ações</TableHead>
|
||||||
|
|
@ -179,9 +192,15 @@ function AdminPage() {
|
||||||
<TableCell className="font-mono text-xs">
|
<TableCell className="font-mono text-xs">
|
||||||
{a.uuid_tenant.slice(0, 8)}
|
{a.uuid_tenant.slice(0, 8)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-sm max-w-[180px] truncate">
|
||||||
|
{a.profile?.full_name || "—"}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-sm">
|
<TableCell className="text-sm">
|
||||||
{a.telegram_bot_username ? `@${a.telegram_bot_username}` : "—"}
|
{a.telegram_bot_username ? `@${a.telegram_bot_username}` : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PlanBadge slug={a.subscription?.plans?.slug ?? null} />
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<StatusBadge status={a.status} />
|
<StatusBadge status={a.status} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -189,34 +208,12 @@ function AdminPage() {
|
||||||
{a.railway_service_id?.slice(0, 8) ?? "—"}
|
{a.railway_service_id?.slice(0, 8) ?? "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right space-x-1 whitespace-nowrap">
|
<TableCell className="text-right space-x-1 whitespace-nowrap">
|
||||||
{(a.status === "provisioning" || a.status === "error") &&
|
<Button asChild size="sm" variant="outline">
|
||||||
!a.railway_service_id && (
|
<Link to="/admin/agente/$id" params={{ id: a.id }}>
|
||||||
<Button
|
<Settings className="h-3.5 w-3.5" />
|
||||||
size="sm"
|
<span className="ml-1.5 hidden sm:inline">Configurar</span>
|
||||||
variant="outline"
|
</Link>
|
||||||
disabled={
|
|
||||||
busy === a.id + "provision-agent" ||
|
|
||||||
!a.telegram_bot_token_vault_id
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
!a.telegram_bot_token_vault_id
|
|
||||||
? "Usuário precisa concluir onboarding do Telegram primeiro"
|
|
||||||
: "Provisionar container Railway"
|
|
||||||
}
|
|
||||||
onClick={() => action("provision-agent", a.id)}
|
|
||||||
>
|
|
||||||
{busy === a.id + "provision-agent" ? (
|
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<RotateCw className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1.5">
|
|
||||||
{a.telegram_bot_token_vault_id
|
|
||||||
? "Provisionar"
|
|
||||||
: "Aguardando token"}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
{a.status === "active" && (
|
{a.status === "active" && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -229,7 +226,7 @@ function AdminPage() {
|
||||||
) : (
|
) : (
|
||||||
<PauseCircle className="h-3.5 w-3.5" />
|
<PauseCircle className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
<span className="ml-1.5">Suspender</span>
|
<span className="ml-1.5 hidden sm:inline">Suspender</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{a.status === "suspended" && (
|
{a.status === "suspended" && (
|
||||||
|
|
@ -244,7 +241,7 @@ function AdminPage() {
|
||||||
) : (
|
) : (
|
||||||
<PlayCircle className="h-3.5 w-3.5" />
|
<PlayCircle className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
<span className="ml-1.5">Reativar</span>
|
<span className="ml-1.5 hidden sm:inline">Reativar</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -267,3 +264,12 @@ function StatusBadge({ status }: { status: string }) {
|
||||||
if (status === "error") return <Badge variant="destructive">Erro</Badge>;
|
if (status === "error") return <Badge variant="destructive">Erro</Badge>;
|
||||||
return <Badge variant="secondary">{status}</Badge>;
|
return <Badge variant="secondary">{status}</Badge>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PlanBadge({ slug }: { slug: string | null }) {
|
||||||
|
if (!slug) return <Badge variant="outline" className="text-xs">Sem plano</Badge>;
|
||||||
|
if (slug === "professional" || slug === "enterprise")
|
||||||
|
return <Badge variant="success" className="text-xs capitalize">{slug}</Badge>;
|
||||||
|
if (slug === "starter") return <Badge variant="secondary" className="text-xs capitalize">{slug}</Badge>;
|
||||||
|
return <Badge variant="outline" className="text-xs capitalize">{slug}</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue