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:
gpt-engineer-app[bot] 2026-04-24 10:34:36 +00:00
commit 1e72250c73
4 changed files with 246 additions and 26 deletions

View file

@ -1,8 +1,8 @@
"use client"; "use client";
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { ArrowRight, CheckCircle2, Loader2, Sparkles } from "lucide-react"; import { ArrowRight, CheckCircle2, Loader2, MessageCircle, Sparkles } from "lucide-react";
import { useSubscription } from "@/hooks/use-profile"; import { useSubscription } from "@/hooks/use-profile";
import { useProfile } from "@/hooks/use-profile"; import { useProfile } from "@/hooks/use-profile";
import { useAgentInstance } from "@/hooks/use-agent-instance"; import { useAgentInstance } from "@/hooks/use-agent-instance";
@ -33,6 +33,7 @@ function DashboardPage() {
const search = Route.useSearch(); const search = Route.useSearch();
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
const [wizardOpen, setWizardOpen] = useState(false); const [wizardOpen, setWizardOpen] = useState(false);
const previousStatusRef = useRef<string | null>(null);
// Auto-open do wizard ao voltar com ?status=success // Auto-open do wizard ao voltar com ?status=success
useEffect(() => { useEffect(() => {
@ -51,6 +52,18 @@ function DashboardPage() {
} }
}, [search.status, agent, isLoading]); }, [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) { if (isLoading) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -79,10 +92,16 @@ function DashboardPage() {
{subscription && {subscription &&
(subscription.status === "incomplete" || subscription.status === "active") && (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" && ( {subscription && subscription.status === "active" && agent?.status === "active" && (
<> <>
{agent.telegram_bot_username && <ActiveSuccessCard botUsername={agent.telegram_bot_username} />}
<AutoPausedBanner /> <AutoPausedBanner />
<div className="grid gap-6 lg:grid-cols-2"> <div className="grid gap-6 lg:grid-cols-2">
<SkillsDashboardWidget /> <SkillsDashboardWidget />
@ -121,13 +140,34 @@ function NoSubscriptionCard() {
); );
} }
function ProvisioningCard() { type StepState = "done" | "active" | "pending";
const steps = [
{ label: "Provisionar container na VPS", state: "active" as const }, function ProvisioningCard({
{ label: "Configurar modelo de IA", state: "pending" as const }, telegramConnected,
{ label: "Conectar seu Telegram", state: "pending" as const }, railwayServiceCreated: _railwayServiceCreated,
{ label: "Personalizar seu agente", state: "pending" as const }, }: {
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 ( return (
<div className="rounded-xl border border-border bg-card p-6 sm:p-8 shadow-soft"> <div className="rounded-xl border border-border bg-card p-6 sm:p-8 shadow-soft">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
@ -135,9 +175,9 @@ function ProvisioningCard() {
<Loader2 className="h-5 w-5 text-primary animate-spin" /> <Loader2 className="h-5 w-5 text-primary animate-spin" />
</div> </div>
<div className="flex-1"> <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"> <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> </p>
</div> </div>
</div> </div>
@ -149,23 +189,25 @@ function ProvisioningCard() {
<div <div
className={cn( className={cn(
"h-10 w-10 rounded-full flex items-center justify-center font-bold text-sm shrink-0", "h-10 w-10 rounded-full flex items-center justify-center font-bold text-sm shrink-0",
step.state === "active" step.state === "done"
? "bg-success/15 text-success"
: step.state === "active"
? "bg-primary text-primary-foreground shadow-glow" ? "bg-primary text-primary-foreground shadow-glow"
: "bg-muted text-muted-foreground", : "bg-muted text-muted-foreground",
)} )}
> >
{step.state === "active" ? ( {step.state === "done" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : step.state === "pending" ? (
i + 1
) : (
<CheckCircle2 className="h-5 w-5" /> <CheckCircle2 className="h-5 w-5" />
) : step.state === "active" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
i + 1
)} )}
</div> </div>
<p <p
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
step.state === "active" ? "text-foreground" : "text-muted-foreground", step.state === "pending" ? "text-muted-foreground" : "text-foreground",
)} )}
> >
{step.label} {step.label}
@ -177,3 +219,37 @@ function ProvisioningCard() {
</div> </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>
);
}

View file

@ -113,6 +113,34 @@ Deno.serve(async (req) => {
return jsonResponse({ error: "Webhook configurado, mas falha ao salvar." }, 500); 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 }); return jsonResponse({ success: true });
} catch (err) { } catch (err) {
console.error("configure-telegram-webhook fatal", err); console.error("configure-telegram-webhook fatal", err);

View file

@ -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 SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN"); const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN");
const OPENROUTER_API_KEY = Deno.env.get("OPENROUTER_API_KEY") ?? ""; 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) => { Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
@ -53,6 +75,8 @@ Deno.serve(async (req) => {
auth: { persistSession: false, autoRefreshToken: false }, auth: { persistSession: false, autoRefreshToken: false },
}); });
console.log(`[provision-agent] início para agent_instance_id=${body.agent_instance_id}`);
// 1) Carregar agent_instance // 1) Carregar agent_instance
const { data: agent, error: agentErr } = await supabase const { data: agent, error: agentErr } = await supabase
.from("agent_instances") .from("agent_instances")
@ -63,14 +87,17 @@ Deno.serve(async (req) => {
.maybeSingle(); .maybeSingle();
if (agentErr || !agent) { 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 }); return jsonResponse(404, { error: "agent_instance not found", detail: agentErr?.message });
} }
if (agent.status !== "provisioning") { 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 }); return jsonResponse(409, { error: "agent_instance is not in provisioning status", status: agent.status });
} }
if (agent.railway_service_id) { 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 }); 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 fullName = (profile?.full_name?.trim() || "Usuário").toString();
const firstName = fullName.split(" ")[0] || "Usuário"; const firstName = fullName.split(" ")[0] || "Usuário";
const agentName = body.agent_name?.trim() || `Mika de ${firstName}`; 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) // 1c) Carregar subscription ativa (para definir modelo Pro vs Basic)
const { data: subscription } = await supabase const { data: subscription } = await supabase
@ -98,6 +126,7 @@ Deno.serve(async (req) => {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
const planSlug = ((subscription as any)?.plans?.slug as string | undefined) ?? "basic"; const planSlug = ((subscription as any)?.plans?.slug as string | undefined) ?? "basic";
const isPro = ["professional", "enterprise"].includes(planSlug); 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) // 2) Buscar pool disponível (com IDs Railway preenchidos e capacidade)
const { data: pool, error: poolErr } = await supabase const { data: pool, error: poolErr } = await supabase
@ -111,9 +140,17 @@ Deno.serve(async (req) => {
.maybeSingle(); .maybeSingle();
if (poolErr || !pool || !pool.railway_project_id || !pool.railway_environment_id) { 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 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" }); 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 // 3) Criar provisioning_job em status running
const { data: job, error: jobErr } = await supabase const { data: job, error: jobErr } = await supabase
@ -136,12 +173,15 @@ Deno.serve(async (req) => {
.single(); .single();
if (jobErr || !job) { 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 }); 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) // 4) Decrypt do telegram_bot_token (se existir)
let telegramBotToken = ""; let telegramBotToken = "";
if (agent.telegram_bot_token_vault_id) { 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", { const { data: secret } = await supabase.rpc("vault_decrypt_secret", {
secret_id: agent.telegram_bot_token_vault_id, secret_id: agent.telegram_bot_token_vault_id,
}); });
@ -149,15 +189,18 @@ Deno.serve(async (req) => {
} }
if (!telegramBotToken) { 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"); 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" }); 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) // 5) Apagar webhook Telegram (Hermes vai usar polling)
try { try {
await deleteTelegramWebhook(telegramBotToken); await deleteTelegramWebhook(telegramBotToken);
console.log(`[provision-agent] deleteTelegramWebhook OK`);
} catch (e) { } 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 // 6) Montar variáveis de ambiente do container
@ -190,6 +233,7 @@ Deno.serve(async (req) => {
// 7) Criar serviço no Railway // 7) Criar serviço no Railway
const serviceName = `mika-${agent.uuid_tenant.replace(/-/g, "").slice(0, 8)}`; const serviceName = `mika-${agent.uuid_tenant.replace(/-/g, "").slice(0, 8)}`;
console.log(`[provision-agent] criando serviço Railway: ${serviceName}`);
let railwayServiceId: string; let railwayServiceId: string;
try { try {
@ -198,6 +242,7 @@ Deno.serve(async (req) => {
projectId: pool.railway_project_id, projectId: pool.railway_project_id,
name: serviceName, name: serviceName,
}); });
console.log(`[provision-agent] serviço criado: ${railwayServiceId}`);
await configureRailwayService({ await configureRailwayService({
token: RAILWAY_API_TOKEN, token: RAILWAY_API_TOKEN,
@ -207,16 +252,26 @@ Deno.serve(async (req) => {
startCommand: HERMES_START_COMMAND, startCommand: HERMES_START_COMMAND,
variables: envVars, variables: envVars,
}); });
console.log(`[provision-agent] serviço configurado com ${Object.keys(envVars).length} env vars`);
await deployRailwayService({ await deployRailwayService({
token: RAILWAY_API_TOKEN, token: RAILWAY_API_TOKEN,
serviceId: railwayServiceId, serviceId: railwayServiceId,
environmentId: pool.railway_environment_id, environmentId: pool.railway_environment_id,
}); });
console.log(`[provision-agent] deploy disparado em Railway`);
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
console.error("Railway provisioning failed:", msg); console.error("[provision-agent] Railway provisioning failed:", msg);
await scheduleRetry(supabase, agent, job.id, 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 }); return jsonResponse(500, { error: "railway provisioning failed", detail: msg });
} }
@ -240,6 +295,8 @@ Deno.serve(async (req) => {
.update({ railway_service_id: railwayServiceId, status: "running" }) .update({ railway_service_id: railwayServiceId, status: "running" })
.eq("id", job.id); .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 // status do agent permanece 'provisioning' — railway-webhook atualiza para 'active' quando deploy subir
return jsonResponse(200, { return jsonResponse(200, {
success: true, success: true,
@ -278,7 +335,7 @@ async function scheduleRetry(
agent: { id: string }, agent: { id: string },
jobId: string, jobId: string,
message: string, message: string,
) { ): Promise<boolean> {
const { data: job } = await supabase const { data: job } = await supabase
.from("provisioning_jobs") .from("provisioning_jobs")
.select("attempt, max_attempts") .select("attempt, max_attempts")
@ -290,7 +347,7 @@ async function scheduleRetry(
if (attempt >= max) { if (attempt >= max) {
await failJob(supabase, agent, jobId, `Max attempts reached. Last error: ${message}`); await failJob(supabase, agent, jobId, `Max attempts reached. Last error: ${message}`);
return; return true;
} }
const nextDelayMs = Math.pow(attempt, 2) * 60_000; const nextDelayMs = Math.pow(attempt, 2) * 60_000;
@ -305,4 +362,5 @@ async function scheduleRetry(
next_retry_at: nextRetryAt, next_retry_at: nextRetryAt,
}) })
.eq("id", jobId); .eq("id", jobId);
return false;
} }

View file

@ -13,6 +13,28 @@ import { corsHeaders } from "../_shared/cors.ts";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; 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) => { Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
@ -59,7 +81,7 @@ Deno.serve(async (req) => {
const { data: agent } = await supabase const { data: agent } = await supabase
.from("agent_instances") .from("agent_instances")
.select("id, status") .select("id, status, user_id, telegram_bot_username, railway_service_id")
.eq("railway_service_id", serviceId) .eq("railway_service_id", serviceId)
.maybeSingle(); .maybeSingle();
@ -70,6 +92,17 @@ Deno.serve(async (req) => {
const now = new Date().toISOString(); const now = new Date().toISOString();
const upper = status.toUpperCase(); 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") { if (upper === "SUCCESS" || upper === "ACTIVE" || upper === "DEPLOYED") {
await supabase await supabase
@ -88,6 +121,19 @@ Deno.serve(async (req) => {
.in("status", ["running", "retrying", "pending"]); .in("status", ["running", "retrying", "pending"]);
console.log(`railway-webhook: agent ${agent.id} marcado como active (status=${upper})`); 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" }); 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"]); .in("status", ["running", "retrying", "pending"]);
console.log(`railway-webhook: agent ${agent.id} marcado como error (status=${upper})`); 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" }); return jsonResponse(200, { ok: true, agent_id: agent.id, new_status: "error" });
} }