fix: clean runtime docs and deterministic hydration (#8)

This commit is contained in:
Felipe Domingues 2026-05-29 07:22:49 -03:00 committed by GitHub
parent 6b3943807c
commit c626c555db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 40 additions and 40 deletions

View file

@ -24,8 +24,10 @@ Já configuradas via `.env` (gerado automaticamente pelo Lovable Cloud):
- `PADDLE_API_KEY` / `PADDLE_WEBHOOK_SECRET` - `PADDLE_API_KEY` / `PADDLE_WEBHOOK_SECRET`
- `SUPABASE_SERVICE_ROLE_KEY` - `SUPABASE_SERVICE_ROLE_KEY`
- `RAILWAY_API_TOKEN` — token da Railway Public API, usado por `provision-agent`/`suspend-agent`/`resume-agent` para criar e gerenciar containers Hermes. - `RAILWAY_API_TOKEN` — token da Railway Public API, usado por `provision-agent`/`suspend-agent`/`resume-agent` para criar e gerenciar containers Hermes.
- `OLLAMA_API_KEY` — chave do Ollama Cloud, injetada como env var no container Hermes (imagem custom `ghcr.io/domfelipe/hermes-agent-custom:latest`). - `OLLAMA_API_KEY` — chave do Ollama Cloud, injetada como env var no container Hermes.
- `HERMES_API_SERVER_KEY` — token usado pelo API server interno do Hermes (`API_SERVER_KEY`). Valor atual: `HermesRailwayKey2026SecureToken123456`. - `HERMES_RUNTIME_IMAGE` — opcional; imagem Docker usada no Railway. Default: `ghcr.io/domfelipe/hermes-agent-custom:latest`.
- `HERMES_API_SERVER_KEY` — token usado pelo API server interno do Hermes (`API_SERVER_KEY`). Gere um valor forte por ambiente e nunca versione no repo.
- `INTERNAL_FUNCTION_SECRET` — segredo server-to-server usado pelo runtime Hermes para chamar Edge Functions internas, como `create-cronjob-from-agent` e `create-skill-from-agent`.
- `TELEGRAM_MANAGER_BOT_TOKEN` — token do bot manager (`@mika_managerbot`) usado para criar bots dos clientes em 1 toque via Bot Management Mode do BotFather. - `TELEGRAM_MANAGER_BOT_TOKEN` — token do bot manager (`@mika_managerbot`) usado para criar bots dos clientes em 1 toque via Bot Management Mode do BotFather.
- `TELEGRAM_MANAGER_BOT_USERNAME` — username do bot manager, padrão `mika_managerbot` (sem `@`). - `TELEGRAM_MANAGER_BOT_USERNAME` — username do bot manager, padrão `mika_managerbot` (sem `@`).
@ -42,18 +44,21 @@ Já configuradas via `.env` (gerado automaticamente pelo Lovable Cloud):
``` ```
Resposta esperada: `{ "webhook_set": "...", "telegram_response": { "ok": true } }`. Resposta esperada: `{ "webhook_set": "...", "telegram_response": { "ok": true } }`.
> **Imagem Docker custom**: o container roda `ghcr.io/domfelipe/hermes-agent-custom:latest`, que já contém o `SOUL.md` embutido. Por isso não passamos mais `HERMES_SOUL_OVERRIDE` via env var, e o Dockerfile define o `CMD` (sem `startCommand` no Railway). Secrets removidos do projeto: `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `HERMES_SOUL_OVERRIDE`. > **Imagem Docker custom**: por padrão o container roda `ghcr.io/domfelipe/hermes-agent-custom:latest`, mas `HERMES_RUNTIME_IMAGE` pode apontar para uma tag de branch/sha durante smoke. A imagem contém um `SOUL.md` base, e o `provision-agent` pode injetar `HERMES_SOUL_OVERRIDE` para aplicar a identidade do agente e as instruções obrigatórias de runtime.
> **Suspend/Resume via flag, não via stop.** Railway não permite parar containers sem redeploy. As Edge Functions `suspend-agent`/`resume-agent` usam a env var `HERMES_SUSPENDED` + `serviceInstanceRedeploy`:
>
> - `suspend` → upsert `HERMES_SUSPENDED=true` e redeploy → container entra em `sleep infinity`.
> - `resume` → upsert `HERMES_SUSPENDED=""` e redeploy → container sobe normal.
>
> Isso depende do start command verificar a flag. Novos serviços já são provisionados com o comando correto (`HERMES_START_COMMAND` em `_shared/railway.ts`):
>
> ```bash
> /bin/bash -c 'if [ "$HERMES_SUSPENDED" = "true" ]; then echo "Agent suspended" && sleep infinity; fi && if [ -n "$HERMES_SOUL_OVERRIDE" ]; then echo "$HERMES_SOUL_OVERRIDE" > /opt/data/SOUL.md; fi && /opt/hermes/docker/entrypoint.sh gateway run'
> ```
>
> **Para serviços Railway existentes (provisionados antes desta mudança):** use `admin-backfill-runtime` para reconciliar envs, imagem e start command. Não atualize manualmente secrets no Railway.
> **Suspend/Resume via flag, não via stop.** Railway não permite parar containers sem redeploy. As Edge Functions `suspend-agent`/`resume-agent` usam a env var `HERMES_SUSPENDED` + `serviceInstanceRedeploy`:
> - `suspend` → upsert `HERMES_SUSPENDED=true` e redeploy → container entra em `sleep infinity`.
> - `resume` → upsert `HERMES_SUSPENDED=""` e redeploy → container sobe normal.
>
> Isso depende do start command verificar a flag. Novos serviços já são provisionados com o comando correto (`HERMES_START_COMMAND` em `_shared/railway.ts`):
> ```bash
> /bin/bash -c 'if [ "$HERMES_SUSPENDED" = "true" ]; then echo "Agent suspended" && sleep infinity; fi && if [ -n "$HERMES_SOUL_OVERRIDE" ]; then echo "$HERMES_SOUL_OVERRIDE" > /opt/data/SOUL.md; fi && /opt/hermes/docker/entrypoint.sh gateway run'
> ```
> **Para serviços Railway existentes (provisionados antes desta mudança):** atualize manualmente o Start Command via Railway UI/Agent para o comando acima — caso contrário `suspend-agent` apenas marcará `status='suspended'` no banco mas o container continuará rodando.
- `OPENROUTER_API_KEY`**obrigatório**. Injetado em cada container Hermes provisionado para que o agente possa chamar os modelos `openrouter/google/gemma-4-*-it`. Sem isso o `provision-agent` retorna 500.
- `ADMIN_TELEGRAM_BOT_TOKEN` — token do bot de admin (ex: `@mika_test2_bot`) usado pelo `payments-webhook` para enviar notificações de novos clientes, falhas de pagamento e cancelamentos. - `ADMIN_TELEGRAM_BOT_TOKEN` — token do bot de admin (ex: `@mika_test2_bot`) usado pelo `payments-webhook` para enviar notificações de novos clientes, falhas de pagamento e cancelamentos.
- `ADMIN_TELEGRAM_CHAT_ID` — chat ID do admin que recebe as notificações (ex: `179720882`). - `ADMIN_TELEGRAM_CHAT_ID` — chat ID do admin que recebe as notificações (ex: `179720882`).
- (opcional, Fase 5) credenciais SSH para Hermes - (opcional, Fase 5) credenciais SSH para Hermes
@ -155,13 +160,13 @@ Para cada provider abaixo, crie um OAuth app e configure a **Redirect URI**:
https://smsarmgoirlcedmqvdgc.supabase.co/functions/v1/oauth-callback https://smsarmgoirlcedmqvdgc.supabase.co/functions/v1/oauth-callback
``` ```
| Provider | Console | Scopes mínimos | | Provider | Console | Scopes mínimos |
|----------|---------|----------------| | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Google Workspace** | [console.cloud.google.com](https://console.cloud.google.com) → APIs & Services → Credentials → OAuth 2.0 Client ID (Web app) | `openid email profile https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/calendar` | | **Google Workspace** | [console.cloud.google.com](https://console.cloud.google.com) → APIs & Services → Credentials → OAuth 2.0 Client ID (Web app) | `openid email profile https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/calendar` |
| **Microsoft 365** | [Azure Portal](https://portal.azure.com) → App registrations → New registration → Web | `openid email profile offline_access Mail.Read Calendars.ReadWrite` | | **Microsoft 365** | [Azure Portal](https://portal.azure.com) → App registrations → New registration → Web | `openid email profile offline_access Mail.Read Calendars.ReadWrite` |
| **Notion** | [notion.so/my-integrations](https://www.notion.so/my-integrations) → New integration (Public) | (definidos na integração) | | **Notion** | [notion.so/my-integrations](https://www.notion.so/my-integrations) → New integration (Public) | (definidos na integração) |
| **Todoist** | [developer.todoist.com](https://developer.todoist.com/appconsole.html) → App management → Create app | `data:read_write` | | **Todoist** | [developer.todoist.com](https://developer.todoist.com/appconsole.html) → App management → Create app | `data:read_write` |
| **Cal.com** | [app.cal.com/settings/developer](https://app.cal.com/settings/developer/oauth-clients) → New OAuth Client | `READ_BOOKING WRITE_BOOKING READ_PROFILE` | | **Cal.com** | [app.cal.com/settings/developer](https://app.cal.com/settings/developer/oauth-clients) → New OAuth Client | `READ_BOOKING WRITE_BOOKING READ_PROFILE` |
#### 2. Adicionar os 10 secrets no Lovable Cloud #### 2. Adicionar os 10 secrets no Lovable Cloud
@ -292,4 +297,3 @@ bun dev # dev server (porta 8080)
bun run build # build de produção bun run build # build de produção
bun run typecheck bun run typecheck
``` ```

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import { useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { MessageCircle, ExternalLink, CheckCircle2 } from "lucide-react"; import { MessageCircle, ExternalLink, CheckCircle2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -14,6 +13,11 @@ interface Props {
} }
const EMOJIS = ["🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀"]; const EMOJIS = ["🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀"];
const EMOJI_POSITIONS = EMOJIS.map((_, index) => ({
x: ((index * 73) % 320) - 160,
y: ((index * 47) % 240) - 120,
rotate: ((index * 31) % 80) - 40,
}));
export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinish }: Props) { export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinish }: Props) {
const { received } = useTelegramFirstMessage({ const { received } = useTelegramFirstMessage({
@ -22,15 +26,7 @@ export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinis
enabled: true, enabled: true,
}); });
const positions = useMemo( const positions = EMOJI_POSITIONS;
() =>
EMOJIS.map(() => ({
x: (Math.random() - 0.5) * 320,
y: (Math.random() - 0.5) * 240,
rotate: (Math.random() - 0.5) * 80,
})),
[],
);
return ( return (
<div className="px-6 py-8 max-w-xl mx-auto"> <div className="px-6 py-8 max-w-xl mx-auto">
@ -56,11 +52,7 @@ export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinis
</div> </div>
<Button asChild size="lg" className="mt-6"> <Button asChild size="lg" className="mt-6">
<a <a href={`https://t.me/${botUsername}`} target="_blank" rel="noopener noreferrer">
href={`https://t.me/${botUsername}`}
target="_blank"
rel="noopener noreferrer"
>
Abrir meu bot no Telegram <ExternalLink className="ml-2 h-4 w-4" /> Abrir meu bot no Telegram <ExternalLink className="ml-2 h-4 w-4" />
</a> </a>
</Button> </Button>
@ -103,8 +95,8 @@ export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinis
</div> </div>
<h2 className="mt-4 text-2xl font-bold tracking-tight">🎉 Mika conectado!</h2> <h2 className="mt-4 text-2xl font-bold tracking-tight">🎉 Mika conectado!</h2>
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
Você recebeu a primeira resposta do seu agente. Ele ainda está em modo de teste, Você recebeu a primeira resposta do seu agente. Ele ainda está em modo de teste, mas
mas em breve vai responder de verdade. em breve vai responder de verdade.
</p> </p>
<Button size="lg" className="mt-8 min-w-56" onClick={onFinish}> <Button size="lg" className="mt-8 min-w-56" onClick={onFinish}>

View file

@ -638,10 +638,14 @@ const SidebarMenuSkeleton = React.forwardRef<
showIcon?: boolean; showIcon?: boolean;
} }
>(({ className, showIcon = false, ...props }, ref) => { >(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%. const id = React.useId();
const width = React.useMemo(() => { const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`; let hash = 0;
}, []); for (let i = 0; i < id.length; i += 1) {
hash = (hash * 31 + id.charCodeAt(i)) % 40;
}
return `${hash + 50}%`;
}, [id]);
return ( return (
<div <div