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 (
+
+
+
+
+
+
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 (
-
+
Voltar
@@ -84,7 +84,7 @@ function IntegrationDetailPage() {
return (
-
+
Voltar
@@ -93,7 +93,7 @@ function IntegrationDetailPage() {
Você ainda não conectou {mcp.name}.
- Conectar
+ Conectar
@@ -154,7 +154,7 @@ function IntegrationDetailPage() {
return (
-
+
Voltar
@@ -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}
}
-
- setAttempt((a) => a + 1)}>Tentar novamente
-
- Pular (configurar depois)
-
-
-
- )}
-
- );
-}
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.
-
-
-
-
-
-
-
-
- Próximo: colar o token
-
-
- );
-}
-
-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 (
-
-
-
-
-
- {copied ? : }
-
-
- {help &&
{help}
}
-
- );
-}