mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 06:16:42 +00:00
Desenvolvido auto-provisionamento
X-Lovable-Edit-ID: edt-d65058c7-c314-48aa-a609-48a021b9020f Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
commit
1e72250c73
4 changed files with 246 additions and 26 deletions
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowRight, CheckCircle2, Loader2, Sparkles } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ArrowRight, CheckCircle2, Loader2, MessageCircle, Sparkles } from "lucide-react";
|
||||
import { useSubscription } from "@/hooks/use-profile";
|
||||
import { useProfile } from "@/hooks/use-profile";
|
||||
import { useAgentInstance } from "@/hooks/use-agent-instance";
|
||||
|
|
@ -33,6 +33,7 @@ function DashboardPage() {
|
|||
const search = Route.useSearch();
|
||||
const navigate = Route.useNavigate();
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const previousStatusRef = useRef<string | null>(null);
|
||||
|
||||
// Auto-open do wizard ao voltar com ?status=success
|
||||
useEffect(() => {
|
||||
|
|
@ -51,6 +52,18 @@ function DashboardPage() {
|
|||
}
|
||||
}, [search.status, agent, isLoading]);
|
||||
|
||||
// Toast quando agente sair de provisioning → active (uma única vez)
|
||||
useEffect(() => {
|
||||
if (!agent) return;
|
||||
const prev = previousStatusRef.current;
|
||||
if (prev === "provisioning" && agent.status === "active") {
|
||||
toast.success("Sua Mika está pronta! 🎉", {
|
||||
description: "Abra o Telegram e mande uma mensagem para começar.",
|
||||
});
|
||||
}
|
||||
previousStatusRef.current = agent.status;
|
||||
}, [agent]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -79,10 +92,16 @@ function DashboardPage() {
|
|||
|
||||
{subscription &&
|
||||
(subscription.status === "incomplete" || subscription.status === "active") &&
|
||||
agent?.status === "provisioning" && <ProvisioningCard />}
|
||||
agent?.status === "provisioning" && (
|
||||
<ProvisioningCard
|
||||
telegramConnected={!!agent.telegram_bot_username}
|
||||
railwayServiceCreated={false /* não temos campo direto; mostramos como "em andamento" */}
|
||||
/>
|
||||
)}
|
||||
|
||||
{subscription && subscription.status === "active" && agent?.status === "active" && (
|
||||
<>
|
||||
{agent.telegram_bot_username && <ActiveSuccessCard botUsername={agent.telegram_bot_username} />}
|
||||
<AutoPausedBanner />
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<SkillsDashboardWidget />
|
||||
|
|
@ -121,13 +140,34 @@ function NoSubscriptionCard() {
|
|||
);
|
||||
}
|
||||
|
||||
function ProvisioningCard() {
|
||||
const steps = [
|
||||
{ label: "Provisionar container na VPS", state: "active" as const },
|
||||
{ label: "Configurar modelo de IA", state: "pending" as const },
|
||||
{ label: "Conectar seu Telegram", state: "pending" as const },
|
||||
{ label: "Personalizar seu agente", state: "pending" as const },
|
||||
type StepState = "done" | "active" | "pending";
|
||||
|
||||
function ProvisioningCard({
|
||||
telegramConnected,
|
||||
railwayServiceCreated: _railwayServiceCreated,
|
||||
}: {
|
||||
telegramConnected: boolean;
|
||||
railwayServiceCreated?: boolean;
|
||||
}) {
|
||||
// Etapas dinâmicas — sabemos: pagamento confirmado (sempre done aqui),
|
||||
// Telegram conectado (vem do agent.telegram_bot_username), e o resto
|
||||
// está em andamento até o railway-webhook chegar como SUCCESS.
|
||||
const steps: { label: string; state: StepState }[] = [
|
||||
{ label: "Pagamento confirmado", state: "done" },
|
||||
{
|
||||
label: telegramConnected ? "Telegram conectado" : "Conecte seu Telegram",
|
||||
state: telegramConnected ? "done" : "active",
|
||||
},
|
||||
{
|
||||
label: "Provisionando container",
|
||||
state: telegramConnected ? "active" : "pending",
|
||||
},
|
||||
{
|
||||
label: "Mika quase pronta!",
|
||||
state: "pending",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-6 sm:p-8 shadow-soft">
|
||||
<div className="flex items-start gap-4">
|
||||
|
|
@ -135,9 +175,9 @@ function ProvisioningCard() {
|
|||
<Loader2 className="h-5 w-5 text-primary animate-spin" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-bold">Seu agente Mika está sendo provisionado</h2>
|
||||
<h2 className="text-xl font-bold">Estamos preparando seu agente Mika</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Você receberá um e-mail quando estiver pronto — geralmente em até 10 minutos.
|
||||
Geralmente leva de 3 a 5 minutos. Esta página atualiza sozinha quando ficar pronta.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -149,23 +189,25 @@ function ProvisioningCard() {
|
|||
<div
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full flex items-center justify-center font-bold text-sm shrink-0",
|
||||
step.state === "active"
|
||||
? "bg-primary text-primary-foreground shadow-glow"
|
||||
: "bg-muted text-muted-foreground",
|
||||
step.state === "done"
|
||||
? "bg-success/15 text-success"
|
||||
: step.state === "active"
|
||||
? "bg-primary text-primary-foreground shadow-glow"
|
||||
: "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{step.state === "active" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : step.state === "pending" ? (
|
||||
i + 1
|
||||
) : (
|
||||
{step.state === "done" ? (
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
) : step.state === "active" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
i + 1
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
step.state === "active" ? "text-foreground" : "text-muted-foreground",
|
||||
step.state === "pending" ? "text-muted-foreground" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
|
|
@ -177,3 +219,37 @@ function ProvisioningCard() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActiveSuccessCard({ botUsername }: { botUsername: string }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-success/30 bg-success/5 p-6 shadow-soft">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-10 w-10 rounded-full bg-success/15 flex items-center justify-center shrink-0">
|
||||
<CheckCircle2 className="h-5 w-5 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">Sua Mika está no ar 🎉</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Abra o Telegram e converse com{" "}
|
||||
<span className="font-mono text-foreground">@{botUsername}</span> para começar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
className="rounded-lg bg-primary hover:bg-primary-dark text-primary-foreground"
|
||||
>
|
||||
<a
|
||||
href={`https://t.me/${botUsername}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<MessageCircle className="mr-2 h-4 w-4" />
|
||||
Falar com a Mika agora
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,34 @@ Deno.serve(async (req) => {
|
|||
return jsonResponse({ error: "Webhook configurado, mas falha ao salvar." }, 500);
|
||||
}
|
||||
|
||||
// Auto-provisionamento: agora que temos token + webhook, disparamos provision-agent
|
||||
// de forma assíncrona. Não bloqueamos a resposta — falhas são tratadas via retry/admin.
|
||||
if (agent.status === "provisioning") {
|
||||
try {
|
||||
const provisionUrl = `${supabaseUrl}/functions/v1/provision-agent`;
|
||||
console.log(
|
||||
`auto-provision: disparando para agent ${agent.id} (user ${userId})`,
|
||||
);
|
||||
// Fire-and-forget: não fazemos await para não bloquear o cliente
|
||||
fetch(provisionUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${serviceKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
agent_instance_id: agent.id,
|
||||
user_id: userId,
|
||||
}),
|
||||
}).catch((err) =>
|
||||
console.error("auto-provision fetch error:", err)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("auto-provision trigger error:", err);
|
||||
// Não re-throw — wizard do Telegram deve retornar sucesso mesmo se provision falhar
|
||||
}
|
||||
}
|
||||
|
||||
return jsonResponse({ success: true });
|
||||
} catch (err) {
|
||||
console.error("configure-telegram-webhook fatal", err);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,28 @@ const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
|
|||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN");
|
||||
const OPENROUTER_API_KEY = Deno.env.get("OPENROUTER_API_KEY") ?? "";
|
||||
const ADMIN_TELEGRAM_BOT_TOKEN = Deno.env.get("ADMIN_TELEGRAM_BOT_TOKEN");
|
||||
const ADMIN_TELEGRAM_CHAT_ID = Deno.env.get("ADMIN_TELEGRAM_CHAT_ID");
|
||||
|
||||
async function notifyAdmin(message: string): Promise<void> {
|
||||
if (!ADMIN_TELEGRAM_BOT_TOKEN || !ADMIN_TELEGRAM_CHAT_ID) return;
|
||||
try {
|
||||
await fetch(
|
||||
`https://api.telegram.org/bot${ADMIN_TELEGRAM_BOT_TOKEN}/sendMessage`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: ADMIN_TELEGRAM_CHAT_ID,
|
||||
text: message,
|
||||
parse_mode: "HTML",
|
||||
}),
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("notifyAdmin failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
|
||||
|
|
@ -53,6 +75,8 @@ Deno.serve(async (req) => {
|
|||
auth: { persistSession: false, autoRefreshToken: false },
|
||||
});
|
||||
|
||||
console.log(`[provision-agent] início para agent_instance_id=${body.agent_instance_id}`);
|
||||
|
||||
// 1) Carregar agent_instance
|
||||
const { data: agent, error: agentErr } = await supabase
|
||||
.from("agent_instances")
|
||||
|
|
@ -63,14 +87,17 @@ Deno.serve(async (req) => {
|
|||
.maybeSingle();
|
||||
|
||||
if (agentErr || !agent) {
|
||||
console.error(`[provision-agent] agent_instance não encontrado: ${agentErr?.message}`);
|
||||
return jsonResponse(404, { error: "agent_instance not found", detail: agentErr?.message });
|
||||
}
|
||||
|
||||
if (agent.status !== "provisioning") {
|
||||
console.log(`[provision-agent] status atual=${agent.status}, abortando`);
|
||||
return jsonResponse(409, { error: "agent_instance is not in provisioning status", status: agent.status });
|
||||
}
|
||||
|
||||
if (agent.railway_service_id) {
|
||||
console.log(`[provision-agent] já tem railway_service_id=${agent.railway_service_id}, abortando`);
|
||||
return jsonResponse(409, { error: "agent_instance already has a railway_service_id", railway_service_id: agent.railway_service_id });
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +111,7 @@ Deno.serve(async (req) => {
|
|||
const fullName = (profile?.full_name?.trim() || "Usuário").toString();
|
||||
const firstName = fullName.split(" ")[0] || "Usuário";
|
||||
const agentName = body.agent_name?.trim() || `Mika de ${firstName}`;
|
||||
console.log(`[provision-agent] profile carregado: ${fullName} → agent_name=${agentName}`);
|
||||
|
||||
// 1c) Carregar subscription ativa (para definir modelo Pro vs Basic)
|
||||
const { data: subscription } = await supabase
|
||||
|
|
@ -98,6 +126,7 @@ Deno.serve(async (req) => {
|
|||
// deno-lint-ignore no-explicit-any
|
||||
const planSlug = ((subscription as any)?.plans?.slug as string | undefined) ?? "basic";
|
||||
const isPro = ["professional", "enterprise"].includes(planSlug);
|
||||
console.log(`[provision-agent] plano=${planSlug} isPro=${isPro}`);
|
||||
|
||||
// 2) Buscar pool disponível (com IDs Railway preenchidos e capacidade)
|
||||
const { data: pool, error: poolErr } = await supabase
|
||||
|
|
@ -111,9 +140,17 @@ Deno.serve(async (req) => {
|
|||
.maybeSingle();
|
||||
|
||||
if (poolErr || !pool || !pool.railway_project_id || !pool.railway_environment_id) {
|
||||
console.error(`[provision-agent] sem vps_pool disponível: ${poolErr?.message}`);
|
||||
await failJob(supabase, agent, null, "Nenhum vps_pool com Railway IDs configurados disponível");
|
||||
await notifyAdmin(
|
||||
`❌ <b>Falha no auto-provisionamento</b>\n\n` +
|
||||
`👤 <b>Cliente:</b> ${fullName}\n` +
|
||||
`❗ <b>Erro:</b> Nenhum vps_pool disponível\n\n` +
|
||||
`➡️ <a href="https://mika.domco.ai/admin">Resolver manualmente</a>`,
|
||||
);
|
||||
return jsonResponse(503, { error: "no railway pool available" });
|
||||
}
|
||||
console.log(`[provision-agent] pool selecionado: ${pool.id} (railway_project=${pool.railway_project_id})`);
|
||||
|
||||
// 3) Criar provisioning_job em status running
|
||||
const { data: job, error: jobErr } = await supabase
|
||||
|
|
@ -136,12 +173,15 @@ Deno.serve(async (req) => {
|
|||
.single();
|
||||
|
||||
if (jobErr || !job) {
|
||||
console.error(`[provision-agent] falha ao criar job: ${jobErr?.message}`);
|
||||
return jsonResponse(500, { error: "failed to create provisioning_job", detail: jobErr?.message });
|
||||
}
|
||||
console.log(`[provision-agent] provisioning_job criado: ${job.id}`);
|
||||
|
||||
// 4) Decrypt do telegram_bot_token (se existir)
|
||||
let telegramBotToken = "";
|
||||
if (agent.telegram_bot_token_vault_id) {
|
||||
console.log(`[provision-agent] decifrando token do Vault: ${agent.telegram_bot_token_vault_id}`);
|
||||
const { data: secret } = await supabase.rpc("vault_decrypt_secret", {
|
||||
secret_id: agent.telegram_bot_token_vault_id,
|
||||
});
|
||||
|
|
@ -149,15 +189,18 @@ Deno.serve(async (req) => {
|
|||
}
|
||||
|
||||
if (!telegramBotToken) {
|
||||
console.error(`[provision-agent] telegram_bot_token ausente — usuário ainda não conectou bot`);
|
||||
await failJob(supabase, agent, job.id, "telegram_bot_token ausente no Vault — usuário precisa concluir onboarding antes");
|
||||
return jsonResponse(412, { error: "telegram token missing" });
|
||||
}
|
||||
console.log(`[provision-agent] token Telegram OK (len=${telegramBotToken.length})`);
|
||||
|
||||
// 5) Apagar webhook Telegram (Hermes vai usar polling)
|
||||
try {
|
||||
await deleteTelegramWebhook(telegramBotToken);
|
||||
console.log(`[provision-agent] deleteTelegramWebhook OK`);
|
||||
} catch (e) {
|
||||
console.warn("deleteTelegramWebhook failed (continuing):", String(e));
|
||||
console.warn("[provision-agent] deleteTelegramWebhook failed (continuing):", String(e));
|
||||
}
|
||||
|
||||
// 6) Montar variáveis de ambiente do container
|
||||
|
|
@ -190,6 +233,7 @@ Deno.serve(async (req) => {
|
|||
|
||||
// 7) Criar serviço no Railway
|
||||
const serviceName = `mika-${agent.uuid_tenant.replace(/-/g, "").slice(0, 8)}`;
|
||||
console.log(`[provision-agent] criando serviço Railway: ${serviceName}`);
|
||||
let railwayServiceId: string;
|
||||
|
||||
try {
|
||||
|
|
@ -198,6 +242,7 @@ Deno.serve(async (req) => {
|
|||
projectId: pool.railway_project_id,
|
||||
name: serviceName,
|
||||
});
|
||||
console.log(`[provision-agent] serviço criado: ${railwayServiceId}`);
|
||||
|
||||
await configureRailwayService({
|
||||
token: RAILWAY_API_TOKEN,
|
||||
|
|
@ -207,16 +252,26 @@ Deno.serve(async (req) => {
|
|||
startCommand: HERMES_START_COMMAND,
|
||||
variables: envVars,
|
||||
});
|
||||
console.log(`[provision-agent] serviço configurado com ${Object.keys(envVars).length} env vars`);
|
||||
|
||||
await deployRailwayService({
|
||||
token: RAILWAY_API_TOKEN,
|
||||
serviceId: railwayServiceId,
|
||||
environmentId: pool.railway_environment_id,
|
||||
});
|
||||
console.log(`[provision-agent] deploy disparado em Railway`);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Railway provisioning failed:", msg);
|
||||
await scheduleRetry(supabase, agent, job.id, msg);
|
||||
console.error("[provision-agent] Railway provisioning failed:", msg);
|
||||
const reachedMax = await scheduleRetry(supabase, agent, job.id, msg);
|
||||
if (reachedMax) {
|
||||
await notifyAdmin(
|
||||
`❌ <b>Falha no auto-provisionamento</b>\n\n` +
|
||||
`👤 <b>Cliente:</b> ${fullName}\n` +
|
||||
`❗ <b>Erro:</b> ${msg}\n\n` +
|
||||
`➡️ <a href="https://mika.domco.ai/admin">Provisionar manualmente</a>`,
|
||||
);
|
||||
}
|
||||
return jsonResponse(500, { error: "railway provisioning failed", detail: msg });
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +295,8 @@ Deno.serve(async (req) => {
|
|||
.update({ railway_service_id: railwayServiceId, status: "running" })
|
||||
.eq("id", job.id);
|
||||
|
||||
console.log(`[provision-agent] sucesso: agent=${agent.id} railway=${railwayServiceId} (aguardando deploy)`);
|
||||
|
||||
// status do agent permanece 'provisioning' — railway-webhook atualiza para 'active' quando deploy subir
|
||||
return jsonResponse(200, {
|
||||
success: true,
|
||||
|
|
@ -278,7 +335,7 @@ async function scheduleRetry(
|
|||
agent: { id: string },
|
||||
jobId: string,
|
||||
message: string,
|
||||
) {
|
||||
): Promise<boolean> {
|
||||
const { data: job } = await supabase
|
||||
.from("provisioning_jobs")
|
||||
.select("attempt, max_attempts")
|
||||
|
|
@ -290,7 +347,7 @@ async function scheduleRetry(
|
|||
|
||||
if (attempt >= max) {
|
||||
await failJob(supabase, agent, jobId, `Max attempts reached. Last error: ${message}`);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const nextDelayMs = Math.pow(attempt, 2) * 60_000;
|
||||
|
|
@ -305,4 +362,5 @@ async function scheduleRetry(
|
|||
next_retry_at: nextRetryAt,
|
||||
})
|
||||
.eq("id", jobId);
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,28 @@ import { corsHeaders } from "../_shared/cors.ts";
|
|||
|
||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
|
||||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const ADMIN_TELEGRAM_BOT_TOKEN = Deno.env.get("ADMIN_TELEGRAM_BOT_TOKEN");
|
||||
const ADMIN_TELEGRAM_CHAT_ID = Deno.env.get("ADMIN_TELEGRAM_CHAT_ID");
|
||||
|
||||
async function notifyAdmin(message: string): Promise<void> {
|
||||
if (!ADMIN_TELEGRAM_BOT_TOKEN || !ADMIN_TELEGRAM_CHAT_ID) return;
|
||||
try {
|
||||
await fetch(
|
||||
`https://api.telegram.org/bot${ADMIN_TELEGRAM_BOT_TOKEN}/sendMessage`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: ADMIN_TELEGRAM_CHAT_ID,
|
||||
text: message,
|
||||
parse_mode: "HTML",
|
||||
}),
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("notifyAdmin failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
|
||||
|
|
@ -59,7 +81,7 @@ Deno.serve(async (req) => {
|
|||
|
||||
const { data: agent } = await supabase
|
||||
.from("agent_instances")
|
||||
.select("id, status")
|
||||
.select("id, status, user_id, telegram_bot_username, railway_service_id")
|
||||
.eq("railway_service_id", serviceId)
|
||||
.maybeSingle();
|
||||
|
||||
|
|
@ -70,6 +92,17 @@ Deno.serve(async (req) => {
|
|||
|
||||
const now = new Date().toISOString();
|
||||
const upper = status.toUpperCase();
|
||||
const wasProvisioning = agent.status === "provisioning";
|
||||
|
||||
// Carrega nome do cliente para a notificação (best-effort)
|
||||
async function loadFullName(): Promise<string> {
|
||||
const { data } = await supabase
|
||||
.from("profiles")
|
||||
.select("full_name")
|
||||
.eq("id", agent.user_id)
|
||||
.maybeSingle();
|
||||
return (data?.full_name as string | undefined) || "—";
|
||||
}
|
||||
|
||||
if (upper === "SUCCESS" || upper === "ACTIVE" || upper === "DEPLOYED") {
|
||||
await supabase
|
||||
|
|
@ -88,6 +121,19 @@ Deno.serve(async (req) => {
|
|||
.in("status", ["running", "retrying", "pending"]);
|
||||
|
||||
console.log(`railway-webhook: agent ${agent.id} marcado como active (status=${upper})`);
|
||||
|
||||
// Notifica admin somente se era um auto-provisionamento (status anterior=provisioning)
|
||||
if (wasProvisioning) {
|
||||
const fullName = await loadFullName();
|
||||
await notifyAdmin(
|
||||
`✅ <b>Agente provisionado automaticamente!</b>\n\n` +
|
||||
`👤 <b>Cliente:</b> ${fullName}\n` +
|
||||
`🤖 <b>Bot:</b> @${agent.telegram_bot_username || "—"}\n` +
|
||||
`🚀 <b>Railway:</b> <code>${agent.railway_service_id}</code>\n\n` +
|
||||
`O cliente já pode conversar com a Mika no Telegram.`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(200, { ok: true, agent_id: agent.id, new_status: "active" });
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +154,18 @@ Deno.serve(async (req) => {
|
|||
.in("status", ["running", "retrying", "pending"]);
|
||||
|
||||
console.log(`railway-webhook: agent ${agent.id} marcado como error (status=${upper})`);
|
||||
|
||||
if (wasProvisioning) {
|
||||
const fullName = await loadFullName();
|
||||
await notifyAdmin(
|
||||
`❌ <b>Falha no deploy do agente</b>\n\n` +
|
||||
`👤 <b>Cliente:</b> ${fullName}\n` +
|
||||
`🚀 <b>Railway:</b> <code>${agent.railway_service_id}</code>\n` +
|
||||
`❗ <b>Status:</b> ${upper}\n\n` +
|
||||
`➡️ <a href="https://mika.domco.ai/admin">Investigar</a>`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(200, { ok: true, agent_id: agent.id, new_status: "error" });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue