From aec46d386e16ea834247e465481486737725873a Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:25:36 +0000 Subject: [PATCH 1/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/painel.index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/painel.index.tsx b/src/routes/painel.index.tsx index c849fbd..0306fb4 100644 --- a/src/routes/painel.index.tsx +++ b/src/routes/painel.index.tsx @@ -1,8 +1,6 @@ "use client"; import { createFileRoute, Link } from "@tanstack/react-router"; -import { zodValidator, fallback } from "@tanstack/zod-adapter"; -import { z } from "zod"; import { useEffect, useState } from "react"; import { ArrowRight, CheckCircle2, Loader2, Sparkles } from "lucide-react"; import { useSubscription } from "@/hooks/use-profile"; From 7583dae5cedfad78c045958a13aaa653d219d0d1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:26:00 +0000 Subject: [PATCH 2/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/painel.index.tsx | 8 ++++---- src/routes/painel.integracoes.index.tsx | 14 ++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/routes/painel.index.tsx b/src/routes/painel.index.tsx index 0306fb4..8904bb3 100644 --- a/src/routes/painel.index.tsx +++ b/src/routes/painel.index.tsx @@ -17,12 +17,12 @@ import { TelegramOnboardingWizard } from "@/components/mika/telegram/TelegramOnb import { toast } from "sonner"; import { cn } from "@/lib/utils"; -const dashboardSearchSchema = z.object({ - status: fallback(z.string().optional(), undefined), -}); +type DashboardSearch = { status?: string }; export const Route = createFileRoute("/painel/")({ - validateSearch: zodValidator(dashboardSearchSchema), + validateSearch: (search: Record): DashboardSearch => ({ + status: typeof search.status === "string" ? search.status : undefined, + }), component: DashboardPage, }); diff --git a/src/routes/painel.integracoes.index.tsx b/src/routes/painel.integracoes.index.tsx index 6ff9dce..0855b81 100644 --- a/src/routes/painel.integracoes.index.tsx +++ b/src/routes/painel.integracoes.index.tsx @@ -1,8 +1,6 @@ "use client"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { zodValidator, fallback } from "@tanstack/zod-adapter"; -import { z } from "zod"; import { Plug } from "lucide-react"; import { useEffect } from "react"; import { toast } from "sonner"; @@ -11,14 +9,14 @@ import { useIntegrationCards } from "@/hooks/use-integrations"; import { useAgentInstance } from "@/hooks/use-agent-instance"; import { IntegrationCard } from "@/components/mika/integrations/IntegrationCard"; -const integracoesSearchSchema = z.object({ - status: fallback(z.string().optional(), undefined), - error: fallback(z.string().optional(), undefined), - mcp: fallback(z.string().optional(), undefined), -}); +type IntegracoesSearch = { status?: string; error?: string; mcp?: string }; export const Route = createFileRoute("/painel/integracoes/")({ - validateSearch: zodValidator(integracoesSearchSchema), + validateSearch: (search: Record): IntegracoesSearch => ({ + status: typeof search.status === "string" ? search.status : undefined, + error: typeof search.error === "string" ? search.error : undefined, + mcp: typeof search.mcp === "string" ? search.mcp : undefined, + }), component: IntegracoesPage, }); From 75ad1de24d3ff4f5ef60de5dcb7218c3cebb9097 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:26:33 +0000 Subject: [PATCH 3/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- ...8_59cd91b6-2e03-4e6a-9b92-1bcab3625f23.sql | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 supabase/migrations/20260422172628_59cd91b6-2e03-4e6a-9b92-1bcab3625f23.sql diff --git a/supabase/migrations/20260422172628_59cd91b6-2e03-4e6a-9b92-1bcab3625f23.sql b/supabase/migrations/20260422172628_59cd91b6-2e03-4e6a-9b92-1bcab3625f23.sql new file mode 100644 index 0000000..4585be3 --- /dev/null +++ b/supabase/migrations/20260422172628_59cd91b6-2e03-4e6a-9b92-1bcab3625f23.sql @@ -0,0 +1,72 @@ +-- Trigger para chamar suspend-agent quando subscription fica inactive (canceled/past_due) +-- e resume-agent quando volta para active. + +CREATE OR REPLACE FUNCTION public.trigger_suspend_or_resume_agent() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = 'public', 'extensions' +AS $$ +DECLARE + v_supabase_url text; + v_anon_key text; + v_function text; + v_agent_id uuid; +BEGIN + -- Só age em UPDATE, não em INSERT inicial + IF TG_OP <> 'UPDATE' THEN + RETURN NEW; + END IF; + + IF NEW.status = OLD.status THEN + RETURN NEW; + END IF; + + -- Determina ação: active/trialing => resume; canceled/past_due/unpaid => suspend + IF NEW.status IN ('active', 'trialing') AND OLD.status NOT IN ('active', 'trialing') THEN + v_function := 'resume-agent'; + ELSIF NEW.status IN ('canceled', 'past_due', 'unpaid', 'paused') AND OLD.status IN ('active', 'trialing') THEN + v_function := 'suspend-agent'; + ELSE + RETURN NEW; + END IF; + + -- Busca o agent_instance correspondente + SELECT id INTO v_agent_id FROM public.agent_instances WHERE user_id = NEW.user_id LIMIT 1; + IF v_agent_id IS NULL THEN + RETURN NEW; + END IF; + + -- Lê config + BEGIN + SELECT decrypted_secret INTO v_supabase_url + FROM vault.decrypted_secrets WHERE name = 'project_url' LIMIT 1; + SELECT decrypted_secret INTO v_anon_key + FROM vault.decrypted_secrets WHERE name = 'anon_key' LIMIT 1; + EXCEPTION WHEN OTHERS THEN + v_supabase_url := NULL; + END; + + IF v_supabase_url IS NULL OR v_anon_key IS NULL THEN + RAISE LOG 'trigger_suspend_or_resume_agent: vault não configurado, pulando'; + RETURN NEW; + END IF; + + PERFORM net.http_post( + url := v_supabase_url || '/functions/v1/' || v_function, + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'Authorization', 'Bearer ' || v_anon_key + ), + body := jsonb_build_object('agent_instance_id', v_agent_id) + ); + + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS subscription_status_change_trigger ON public.subscriptions; +CREATE TRIGGER subscription_status_change_trigger +AFTER UPDATE ON public.subscriptions +FOR EACH ROW +EXECUTE FUNCTION public.trigger_suspend_or_resume_agent(); \ No newline at end of file From e6c9e42cffbc1466e755e78c7ed9a21300661860 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:27:24 +0000 Subject: [PATCH 4/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/admin.tsx | 255 ++++++++++++++++++++++ supabase/functions/resume-agent/index.ts | 94 ++++++++ supabase/functions/suspend-agent/index.ts | 95 ++++++++ 3 files changed, 444 insertions(+) create mode 100644 src/routes/admin.tsx create mode 100644 supabase/functions/resume-agent/index.ts create mode 100644 supabase/functions/suspend-agent/index.ts diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx new file mode 100644 index 0000000..f5fda66 --- /dev/null +++ b/src/routes/admin.tsx @@ -0,0 +1,255 @@ +"use client"; + +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { + ArrowLeft, + Loader2, + PlayCircle, + PauseCircle, + RotateCw, + Server, + ShieldAlert, +} from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/hooks/use-auth"; +import { invokeFunction } from "@/lib/invoke-function"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +export const Route = createFileRoute("/admin")({ + component: AdminPage, +}); + +interface AgentRow { + id: string; + user_id: string; + status: string; + uuid_tenant: string; + telegram_bot_username: string | null; + railway_service_id: string | null; + vps_pool_id: string | null; + created_at: string; + provisioned_at: string | null; +} + +function AdminPage() { + const { user, loading: authLoading } = useAuth(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [busy, setBusy] = useState(null); + + const { data: isAdmin, isLoading: roleLoading } = useQuery({ + queryKey: ["is-admin", user?.id], + enabled: !!user, + queryFn: async () => { + const { data, error } = await supabase.rpc("has_role", { + _user_id: user!.id, + _role: "admin", + }); + if (error) throw error; + return data === true; + }, + }); + + const { data: agents, isLoading: agentsLoading } = useQuery({ + queryKey: ["admin-agents"], + enabled: !!isAdmin, + queryFn: async () => { + const { data, error } = await supabase + .from("agent_instances") + .select( + "id, user_id, status, uuid_tenant, telegram_bot_username, railway_service_id, vps_pool_id, created_at, provisioned_at", + ) + .order("created_at", { ascending: false }) + .limit(100); + if (error) throw error; + return data as AgentRow[]; + }, + }); + + useEffect(() => { + if (!authLoading && !user) { + navigate({ to: "/login", search: { redirect: "/admin" } }); + } + }, [authLoading, user, navigate]); + + if (authLoading || roleLoading) { + return ( +
+ +
+ ); + } + + if (!isAdmin) { + return ( +
+
+
+ +
+

Acesso negado

+

+ Esta área é restrita a administradores do Mika. +

+ +
+
+ ); + } + + async function action( + fn: "provision-agent" | "suspend-agent" | "resume-agent", + agentId: string, + ) { + setBusy(agentId + fn); + const { data, error } = await invokeFunction<{ ok?: boolean; error?: string }>(fn, { + agent_instance_id: agentId, + }); + setBusy(null); + if (error) { + toast.error(`${fn} falhou: ${error.message}`); + } else if (data?.error) { + toast.error(`${fn}: ${data.error}`); + } else { + toast.success(`${fn} executado com sucesso`); + queryClient.invalidateQueries({ queryKey: ["admin-agents"] }); + } + } + + return ( +
+
+
+
+

+ Admin · Mika +

+

+ Gerencie agentes provisionados, suspenda e reative containers Railway. +

+
+ +
+ +
+

Agentes ({agents?.length ?? 0})

+ {agentsLoading ? ( + + ) : !agents?.length ? ( +

+ Nenhum agente cadastrado ainda. +

+ ) : ( +
+ + + + Tenant + Bot + Status + Railway + Ações + + + + {agents.map((a) => ( + + + {a.uuid_tenant.slice(0, 8)} + + + {a.telegram_bot_username ? `@${a.telegram_bot_username}` : "—"} + + + + + + {a.railway_service_id?.slice(0, 8) ?? "—"} + + + {a.status === "provisioning" && !a.railway_service_id && ( + + )} + {a.status === "active" && ( + + )} + {a.status === "suspended" && ( + + )} + + + ))} + +
+
+ )} +
+
+
+ ); +} + +function StatusBadge({ status }: { status: string }) { + if (status === "active") return Ativo; + if (status === "provisioning") return Provisionando; + if (status === "suspended") return Suspenso; + if (status === "error") return Erro; + return {status}; +} diff --git a/supabase/functions/resume-agent/index.ts b/supabase/functions/resume-agent/index.ts new file mode 100644 index 0000000..525a208 --- /dev/null +++ b/supabase/functions/resume-agent/index.ts @@ -0,0 +1,94 @@ +// resume-agent +// Retoma o serviço Railway (numReplicas=1) e marca agent_instance.status='active'. +// Disparado automaticamente quando subscription volta para active/trialing, +// ou manualmente pelo painel admin. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { setRailwayReplicas } from "../_shared/railway.ts"; + +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"); + +interface RequestBody { + agent_instance_id: string; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + + if (!RAILWAY_API_TOKEN) { + return jsonResponse(500, { error: "RAILWAY_API_TOKEN not configured" }); + } + + let body: RequestBody; + try { + body = await req.json(); + } catch { + return jsonResponse(400, { error: "invalid json body" }); + } + + if (!body.agent_instance_id) { + return jsonResponse(400, { error: "agent_instance_id required" }); + } + + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + + const { data: agent } = await supabase + .from("agent_instances") + .select("id, status, railway_service_id, vps_pool_id") + .eq("id", body.agent_instance_id) + .maybeSingle(); + + if (!agent) return jsonResponse(404, { error: "agent_instance not found" }); + + if (!agent.railway_service_id || !agent.vps_pool_id) { + return jsonResponse(409, { + error: "agent has no container — needs full provisioning instead", + }); + } + + if (agent.status === "active") { + return jsonResponse(200, { ok: true, already_active: true }); + } + + const { data: pool } = await supabase + .from("vps_pool") + .select("railway_environment_id") + .eq("id", agent.vps_pool_id) + .maybeSingle(); + + if (!pool?.railway_environment_id) { + return jsonResponse(500, { error: "pool environment not configured" }); + } + + try { + await setRailwayReplicas({ + token: RAILWAY_API_TOKEN, + serviceId: agent.railway_service_id, + environmentId: pool.railway_environment_id, + replicas: 1, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error("resume-agent: setRailwayReplicas failed:", msg); + return jsonResponse(500, { error: "railway scale-up failed", detail: msg }); + } + + await supabase + .from("agent_instances") + .update({ status: "active", last_health_check_at: new Date().toISOString() }) + .eq("id", agent.id); + + return jsonResponse(200, { ok: true, agent_id: agent.id, new_status: "active" }); +}); + +function jsonResponse(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} diff --git a/supabase/functions/suspend-agent/index.ts b/supabase/functions/suspend-agent/index.ts new file mode 100644 index 0000000..f81837b --- /dev/null +++ b/supabase/functions/suspend-agent/index.ts @@ -0,0 +1,95 @@ +// suspend-agent +// Pausa o serviço Railway (numReplicas=0) e marca agent_instance.status='suspended'. +// Disparado automaticamente quando subscription muda para canceled/past_due/unpaid/paused, +// ou manualmente pelo painel admin. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { setRailwayReplicas } from "../_shared/railway.ts"; + +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"); + +interface RequestBody { + agent_instance_id: string; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + + if (!RAILWAY_API_TOKEN) { + return jsonResponse(500, { error: "RAILWAY_API_TOKEN not configured" }); + } + + let body: RequestBody; + try { + body = await req.json(); + } catch { + return jsonResponse(400, { error: "invalid json body" }); + } + + if (!body.agent_instance_id) { + return jsonResponse(400, { error: "agent_instance_id required" }); + } + + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + + const { data: agent } = await supabase + .from("agent_instances") + .select("id, status, railway_service_id, vps_pool_id") + .eq("id", body.agent_instance_id) + .maybeSingle(); + + if (!agent) return jsonResponse(404, { error: "agent_instance not found" }); + if (agent.status === "suspended") { + return jsonResponse(200, { ok: true, already_suspended: true }); + } + if (!agent.railway_service_id || !agent.vps_pool_id) { + // Sem container provisionado ainda — só marca o status + await supabase + .from("agent_instances") + .update({ status: "suspended" }) + .eq("id", agent.id); + return jsonResponse(200, { ok: true, no_container: true }); + } + + const { data: pool } = await supabase + .from("vps_pool") + .select("railway_environment_id") + .eq("id", agent.vps_pool_id) + .maybeSingle(); + + if (!pool?.railway_environment_id) { + return jsonResponse(500, { error: "pool environment not configured" }); + } + + try { + await setRailwayReplicas({ + token: RAILWAY_API_TOKEN, + serviceId: agent.railway_service_id, + environmentId: pool.railway_environment_id, + replicas: 0, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error("suspend-agent: setRailwayReplicas failed:", msg); + return jsonResponse(500, { error: "railway scale-down failed", detail: msg }); + } + + await supabase + .from("agent_instances") + .update({ status: "suspended" }) + .eq("id", agent.id); + + return jsonResponse(200, { ok: true, agent_id: agent.id, new_status: "suspended" }); +}); + +function jsonResponse(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} From 1bfa097ffd855fb03e2508c299676389342e0d59 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:27:32 +0000 Subject: [PATCH 5/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routeTree.gen.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index b32d2a8..d4b2b3e 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -14,6 +14,7 @@ import { Route as RedefinirSenhaRouteImport } from './routes/redefinir-senha' import { Route as RecuperarSenhaRouteImport } from './routes/recuperar-senha' import { Route as PainelRouteImport } from './routes/painel' import { Route as LoginRouteImport } from './routes/login' +import { Route as AdminRouteImport } from './routes/admin' import { Route as IndexRouteImport } from './routes/index' import { Route as PainelIndexRouteImport } from './routes/painel.index' import { Route as PainelSkillsRouteImport } from './routes/painel.skills' @@ -56,6 +57,11 @@ const LoginRoute = LoginRouteImport.update({ path: '/login', getParentRoute: () => rootRouteImport, } as any) +const AdminRoute = AdminRouteImport.update({ + id: '/admin', + path: '/admin', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -139,6 +145,7 @@ const PainelCronjobsIdRoute = PainelCronjobsIdRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/admin': typeof AdminRoute '/login': typeof LoginRoute '/painel': typeof PainelRouteWithChildren '/recuperar-senha': typeof RecuperarSenhaRoute @@ -162,6 +169,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/': typeof IndexRoute + '/admin': typeof AdminRoute '/login': typeof LoginRoute '/recuperar-senha': typeof RecuperarSenhaRoute '/redefinir-senha': typeof RedefinirSenhaRoute @@ -184,6 +192,7 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/admin': typeof AdminRoute '/login': typeof LoginRoute '/painel': typeof PainelRouteWithChildren '/recuperar-senha': typeof RecuperarSenhaRoute @@ -209,6 +218,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/admin' | '/login' | '/painel' | '/recuperar-senha' @@ -232,6 +242,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' + | '/admin' | '/login' | '/recuperar-senha' | '/redefinir-senha' @@ -253,6 +264,7 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/admin' | '/login' | '/painel' | '/recuperar-senha' @@ -277,6 +289,7 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + AdminRoute: typeof AdminRoute LoginRoute: typeof LoginRoute PainelRoute: typeof PainelRouteWithChildren RecuperarSenhaRoute: typeof RecuperarSenhaRoute @@ -322,6 +335,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginRouteImport parentRoute: typeof rootRouteImport } + '/admin': { + id: '/admin' + path: '/admin' + fullPath: '/admin' + preLoaderRoute: typeof AdminRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -486,6 +506,7 @@ const PainelRouteWithChildren = const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + AdminRoute: AdminRoute, LoginRoute: LoginRoute, PainelRoute: PainelRouteWithChildren, RecuperarSenhaRoute: RecuperarSenhaRoute, From c65eea1e38a059fb7418dbc95ff5da9325fee988 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:28:01 +0000 Subject: [PATCH 6/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../telegram/TelegramOnboardingWizard.tsx | 49 +++---------------- 1 file changed, 7 insertions(+), 42 deletions(-) diff --git a/src/components/mika/telegram/TelegramOnboardingWizard.tsx b/src/components/mika/telegram/TelegramOnboardingWizard.tsx index 9a1e2c8..2f013d1 100644 --- a/src/components/mika/telegram/TelegramOnboardingWizard.tsx +++ b/src/components/mika/telegram/TelegramOnboardingWizard.tsx @@ -1,37 +1,32 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { X } from "lucide-react"; import { useQueryClient } from "@tanstack/react-query"; import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { useProfile } from "@/hooks/use-profile"; import { useAgentInstance } from "@/hooks/use-agent-instance"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/use-auth"; -import { suggestBotName, suggestBotUsername } from "@/lib/telegram-username"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { StepWelcome } from "./StepWelcome"; import { StepCreateBot } from "./StepCreateBot"; -import { StepNaming } from "./StepNaming"; import { StepToken, type ValidatedBot } from "./StepToken"; -import { StepConfiguring } from "./StepConfiguring"; import { StepWaiting } from "./StepWaiting"; const STORAGE_KEY = "mika-onboarding-last-step"; -const TOTAL_STEPS = 6; +const TOTAL_STEPS = 4; interface Props { open: boolean; onOpenChange: (open: boolean) => void; - /** Step inicial (1-6). Se omitido, lê do localStorage ou começa em 1. */ + /** Step inicial (1-4). Se omitido, lê do localStorage ou começa em 1. */ initialStep?: number; } export function TelegramOnboardingWizard({ open, onOpenChange, initialStep }: Props) { - const { data: profile } = useProfile(); const { data: agent } = useAgentInstance(); const { user } = useAuth(); const queryClient = useQueryClient(); @@ -39,12 +34,6 @@ export function TelegramOnboardingWizard({ open, onOpenChange, initialStep }: Pr const [step, setStep] = useState(1); const [validated, setValidated] = useState(null); - const suggestedName = useMemo(() => suggestBotName(profile?.full_name), [profile?.full_name]); - const suggestedUsername = useMemo( - () => suggestBotUsername(profile?.full_name), - [profile?.full_name], - ); - // Inicializa step ao abrir useEffect(() => { if (!open) return; @@ -101,13 +90,6 @@ export function TelegramOnboardingWizard({ open, onOpenChange, initialStep }: Pr } } - function handleConfigured() { - if (user) { - queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] }); - } - setStep(6); - } - const botUsername = validated?.bot_username ?? agent?.telegram_bot_username ?? ""; const connectedAt = agent?.telegram_connected_at ?? null; @@ -118,9 +100,7 @@ export function TelegramOnboardingWizard({ open, onOpenChange, initialStep }: Pr - Wizard guiado de 6 passos para conectar seu agente Mika ao Telegram. + Wizard guiado para conectar seu agente Mika ao Telegram em 4 passos. - {/* Header com progress + close */}
@@ -157,7 +136,6 @@ export function TelegramOnboardingWizard({ open, onOpenChange, initialStep }: Pr
- {/* Conteúdo dos steps */}
setStep(2)} />} {step === 2 && setStep(3)} />} {step === 3 && ( - setStep(4)} - /> - )} - {step === 4 && ( setStep(5)} + onNext={() => setStep(4)} /> )} - {step === 5 && ( - setStep(6)} - /> - )} - {step === 6 && agent && botUsername && ( + {step === 4 && agent && botUsername && ( )} - {step === 6 && (!agent || !botUsername) && ( + {step === 4 && (!agent || !botUsername) && (
Conecte o bot primeiro para receber a primeira mensagem.
From be2c9a2f6ca281fd58d691239254cceb40abd123 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:28:36 +0000 Subject: [PATCH 7/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/LandingHeader.tsx | 4 ++-- src/components/mika/cronjobs/CronjobWizard.tsx | 2 +- .../mika/integrations/IntegrationsDashboardWidget.tsx | 4 ++-- src/routes/checkout.sucesso.tsx | 4 ++-- src/routes/login.tsx | 2 +- src/routes/painel.cronjobs.$id.tsx | 2 +- src/routes/painel.integracoes.$slug.tsx | 10 +++++----- src/routes/signup.tsx | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/mika/LandingHeader.tsx b/src/components/mika/LandingHeader.tsx index dc7cf5c..9d70807 100644 --- a/src/components/mika/LandingHeader.tsx +++ b/src/components/mika/LandingHeader.tsx @@ -57,7 +57,7 @@ export function LandingHeader() { {user ? ( ) : ( <> @@ -98,7 +98,7 @@ export function LandingHeader() {
{user ? ( ) : ( <> diff --git a/src/components/mika/cronjobs/CronjobWizard.tsx b/src/components/mika/cronjobs/CronjobWizard.tsx index 94f0f06..69d40f0 100644 --- a/src/components/mika/cronjobs/CronjobWizard.tsx +++ b/src/components/mika/cronjobs/CronjobWizard.tsx @@ -324,7 +324,7 @@ export function CronjobWizard({ onCreated, onCancel }: Props) { Conecte as integrações faltantes antes de criar.

)} diff --git a/src/components/mika/integrations/IntegrationsDashboardWidget.tsx b/src/components/mika/integrations/IntegrationsDashboardWidget.tsx index 71d0bb4..18b1983 100644 --- a/src/components/mika/integrations/IntegrationsDashboardWidget.tsx +++ b/src/components/mika/integrations/IntegrationsDashboardWidget.tsx @@ -40,7 +40,7 @@ export function IntegrationsDashboardWidget() {
@@ -52,7 +52,7 @@ export function IntegrationsDashboardWidget() { Conecte serviços como Gmail, Notion e Cal.com para ampliar seu Mika.

) : ( diff --git a/src/routes/checkout.sucesso.tsx b/src/routes/checkout.sucesso.tsx index 3b23d37..833c614 100644 --- a/src/routes/checkout.sucesso.tsx +++ b/src/routes/checkout.sucesso.tsx @@ -18,7 +18,7 @@ function CheckoutSuccessPage() { // Invalida queries de assinatura para refletir o novo estado quando o webhook chegar queryClient.invalidateQueries({ queryKey: ["subscription"] }); const t = setTimeout(() => { - navigate({ to: "/painel" }); + navigate({ to: "/painel", search: {} }); }, 6000); return () => clearTimeout(t); }, [navigate, queryClient]); @@ -37,7 +37,7 @@ function CheckoutSuccessPage() {
)} diff --git a/src/routes/painel.integracoes.$slug.tsx b/src/routes/painel.integracoes.$slug.tsx index 0687ae2..508c985 100644 --- a/src/routes/painel.integracoes.$slug.tsx +++ b/src/routes/painel.integracoes.$slug.tsx @@ -69,7 +69,7 @@ function IntegrationDetailPage() { return (
@@ -84,7 +84,7 @@ function IntegrationDetailPage() { return (
@@ -93,7 +93,7 @@ function IntegrationDetailPage() { Você ainda não conectou {mcp.name}.

@@ -154,7 +154,7 @@ function IntegrationDetailPage() { return (
@@ -298,7 +298,7 @@ function IntegrationDetailPage() { .then(() => { const stillConnected = integs.some((i) => i.id === integration.id); if (!stillConnected) { - navigate({ to: "/painel/integracoes" }); + navigate({ to: "/painel/integracoes", search: {} }); } }); } diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 6536f40..c4f2583 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -86,9 +86,9 @@ function SignupPage() { toast.success("Conta criada!"); if (plan && plan !== "enterprise") { // TODO Etapa 3: chamar create-checkout-session com plan/cycle - navigate({ to: "/painel" }); + navigate({ to: "/painel", search: {} }); } else { - navigate({ to: "/painel" }); + navigate({ to: "/painel", search: {} }); } return; } From 1c29014f6e09340ba47c40160151d621392632d9 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:28:43 +0000 Subject: [PATCH 8/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../mika/telegram/StepConfiguring.tsx | 67 ----------------- src/components/mika/telegram/StepNaming.tsx | 71 ------------------- 2 files changed, 138 deletions(-) delete mode 100644 src/components/mika/telegram/StepConfiguring.tsx delete mode 100644 src/components/mika/telegram/StepNaming.tsx diff --git a/src/components/mika/telegram/StepConfiguring.tsx b/src/components/mika/telegram/StepConfiguring.tsx deleted file mode 100644 index 049812a..0000000 --- a/src/components/mika/telegram/StepConfiguring.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Loader2, AlertCircle } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { invokeFunction } from "@/lib/invoke-function"; - -interface Props { - onConfigured: () => void; - onSkip: () => void; -} - -export function StepConfiguring({ onConfigured, onSkip }: Props) { - const [state, setState] = useState<"loading" | "error">("loading"); - const [error, setError] = useState(null); - const [attempt, setAttempt] = useState(0); - - useEffect(() => { - let cancelled = false; - setState("loading"); - setError(null); - - (async () => { - const { error: err } = await invokeFunction("configure-telegram-webhook"); - if (cancelled) return; - if (err) { - setError(err.message); - setState("error"); - return; - } - window.setTimeout(() => { - if (!cancelled) onConfigured(); - }, 800); - })(); - - return () => { - cancelled = true; - }; - }, [attempt, onConfigured]); - - return ( -
- {state === "loading" && ( - <> - -

- Configurando recebimento de mensagens... -

- - )} - - {state === "error" && ( -
- -

Falha ao configurar webhook

- {error &&

{error}

} -
- - -
-
- )} -
- ); -} diff --git a/src/components/mika/telegram/StepNaming.tsx b/src/components/mika/telegram/StepNaming.tsx deleted file mode 100644 index e85ab35..0000000 --- a/src/components/mika/telegram/StepNaming.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Copy, Check } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { toast } from "sonner"; - -interface Props { - suggestedName: string; - suggestedUsername: string; - onNext: () => void; -} - -export function StepNaming({ suggestedName, suggestedUsername, onNext }: Props) { - return ( -
-

Escolha os nomes

-

- O BotFather vai pedir dois nomes. Use nossas sugestões se quiser. -

- -
- - -
- -
- -
-
- ); -} - -function CopyField({ label, value, help }: { label: string; value: string; help?: string }) { - const [copied, setCopied] = useState(false); - - async function copy() { - try { - await navigator.clipboard.writeText(value); - setCopied(true); - toast.success("Copiado para a área de transferência"); - window.setTimeout(() => setCopied(false), 1500); - } catch { - toast.error("Não foi possível copiar"); - } - } - - return ( -
- -
- - -
- {help &&

{help}

} -
- ); -}