Eu Construí um Aplicativo de Roteiro de Viagem para Minha Família — com um Assistente de IA Dentro Dele

Publicado em
11 julho 2026
Por
Jacob Lloyd — escrito com ajuda de IA, depois do projeto
Tempo de leitura
11 min de leitura

Em termos simples: Fiz um sitezinho só para a viagem da minha família ao Japão — ele mostra o plano de cada dia, o tempo, mapas, e funciona no celular mesmo sem sinal. Também tem um botãozinho de ajudante de IA que responde perguntas sobre a viagem e ajusta o plano. Este artigo é a história honesta de como construí tudo isso com bastante ajuda de IA: o que funcionou, o que quebrou, e como transformei uma exportação frágil de ferramenta de design em algo seguro de editar e seguro de colocar na web. Você pode baixar uma amostra higienizada e abrir em qualquer navegador.

Minha família fez uma viagem ao Japão, e eu queria que todo mundo tivesse o
plano no bolso — cada dia detalhado, mapas a um toque de distância, o tempo, e
os pequenos detalhes fáceis de esquecer quando você está com jet lag parado
numa estação de trem. Então construí um pequeno aplicativo web só para nós: um
roteiro privado, protegido por PIN, com uma programação dia a dia,
previsão do tempo ao vivo, suporte offline e — porque sou eu — um assistente
de IA
embutido que responde perguntas sobre a viagem e até faz pequenas
edições.

Esta é a história honesta da construção: as partes que funcionaram, as partes
que quebraram de formas que eu não esperava, e como uma exportação frágil de
ferramenta de design virou algo com que me senti à vontade para colocar na web
pública. No final tem uma amostra higienizada para você baixar e abrir em
qualquer navegador.

tl;dr

  • O que é: um aplicativo web de roteiro mobile-first — dias, paradas, previsão do tempo ao vivo, ajudantes de moeda + fuso horário, offline/instalável, com uma aba de assistente de IA.
  • A ideia central: uma exportação de ferramenta de design linda mas frágil virou segura de editar assim que o cronograma inteiro passou a ser orientado por um pequeno arquivo de dados, com uma etapa de build regenerando o aplicativo.
  • O portão: um PIN do lado do cliente é encenação — o de verdade é uma sessão do lado do servidor com bloqueio limitado por taxa.
  • A IA: um modelo local self-hosted, mantido com uma coleira curta (responde perguntas; faz só edições pequenas e protegidas).
  • A lição que se repetiu: a única forma de saber se uma mudança funcionou era renderizar o aplicativo em um navegador de verdade e olhar — nunca confiar que o arquivo "deveria" estar bem.

Como começou: uma caixa-preta linda

Desenhei o visual em uma ferramenta de design de IA e exportei. O que saiu foi
um único arquivo HTML autocontido — um aplicativo React completo com fontes e
imagens embutidas — com cerca de 14 MB em um arquivo só. Ficou fantástico e
funcionou no primeiro duplo-clique.

Também era uma caixa-preta. O aplicativo inteiro, minificado, vivia dentro de
um blob gigante, e o cronograma estava enterrado lá dentro. Editar a viagem
manualmente significava fazer cirurgia em código gerado por máquina. Tudo bem
uma vez; é um pesadelo toda vez que os planos mudam — e numa viagem em família,
os planos mudam todo dia.

Então a primeira decisão de verdade foi arquitetural: parar de editar a
exportação.

A única ideia que tornou tudo o resto possível

Em vez de mexer no bundle de 14 MB, mudei o cronograma inteiro para um
pequeno, legível arquivo de dados — um único content.json com os dias,
as paradas, as anotações de tempo, fotos e custos por parada. Depois, uma
pequena etapa de build lê esse arquivo e regenera o aplicativo a partir da
exportação intocada.

Isso mudou o caráter inteiro do projeto. Agora “adicionar uma parada” ou
“corrigir um horário” é uma edição de duas linhas em um arquivo de dados, que
um humano — ou um assistente de IA — pode fazer com segurança, seguida de um
rebuild. A parte frágil (o aplicativo de 14 MB) nunca é tocada à mão. Todo
widget que adicionei depois — um cartão de tempo ao vivo, um relógio duplo
seu horário ↔ Tóquio, um conversor de moeda bidirecional, paradas
expansíveis com fotos e anotações de estacionamento — é gerado a partir dessa
mesma build orientada a dados.

A pegadinha que engoliu todo widget: o aplicativo se reconstrói sozinho ao iniciar

Aqui está a armadilha que mais me custou tempo, registrada para que não custe
nada do seu.

O aplicativo exportado reconstrói a página inteira quando inicializa. Se
você injeta sua própria marcação no HTML estático — um relógio, um banner,
qualquer coisa — ela é apagada no instante em que o aplicativo monta. Os
IDs dos seus elementos somem, getElementById retorna null para sempre, e
seu widget simplesmente nunca aparece, sem aviso. Pior: se você “verifica”
procurando no arquivo HTML salvo, sua marcação está bem ali, então parece
estar tudo certo. Só que ela nunca sobrevive até a tela.

Duas regras resolveram isso de vez:

  1. Injete no próprio template do aplicativo, não na página. Adicione seu
    widget onde o aplicativo constrói sua UI, para que ele seja renderizado
    como parte do aplicativo e sobreviva à inicialização.
  2. Conecte o comportamento com delegação de eventos no document. O
    aplicativo remonta seções inteiras conforme você troca de aba, destruindo
    e recriando seus nós. Um listener preso a um botão morre no próximo
    re-render; um único listener delegado no document (que verifica o que
    foi clicado) sobrevive a tudo.

O corolário é uma regra de teste que agora trato como lei: não verifique uma
interface web fazendo grep no arquivo ou checando por HTTP 200.
Renderize-a
em um navegador real (headless), depois que ela inicializar, e olhe de
verdade para o resultado. Essa é a única checagem que captura esse tipo de
bug — e capturei vários assim, incluindo um conversor de moeda que ficou
invisível até eu movê-lo para dentro do template.

“Autocontido” não era bem assim

A exportação alegava ser um único arquivo autocontido. Não era bem assim. Ela
silenciosamente buscava seu framework de UI de um CDN público em tempo de
execução, montado a partir de fragmentos de URL dentro do código — então uma
busca de texto simples pelo CDN não encontrava nada, e o arquivo parecia
pronto para funcionar offline, sem ser nada disso. Em uma hospedagem web com
política de segurança rígida, aquela busca escondida era bloqueada e o
aplicativo inteiro falhava ao iniciar. No meu notebook, sem essa política,
funcionava — então o problema ficou invisível até chegar ao servidor de
verdade.

A correção foi hospedar o framework eu mesmo (empacotá-lo junto com o
aplicativo para não haver busca externa) e, para a amostra para download,
embuti-lo diretamente para que o pacote inteiro seja realmente um arquivo
só, que abre a partir do disco sem internet nenhuma. Se você levar uma coisa
prática desta seção: “funciona na minha máquina” e “funciona atrás dos
cabeçalhos de segurança de um servidor web de verdade” são afirmações
diferentes — teste a segunda.

O portão: encenação vs. uma trava de verdade

A primeira versão protegia o site com um PIN no JavaScript da página. Isso é
encenação de segurança: o roteiro inteiro está sentado no código-fonte da
página, então qualquer um que consiga abrir as ferramentas de desenvolvedor
consegue lê-lo sem nunca digitar o código. Serve para esconder uma festa
surpresa de uma olhada casual; não serve para nada que você realmente se
importe.

A versão de verdade moveu o portão para o servidor: você envia o PIN, o
servidor o verifica e inicia uma sessão, e só então entrega o roteiro. Ela
também tem um bloqueio progressivo por IP — algumas tentativas erradas e
aquele endereço fica em espera por tempos cada vez maiores, de modo que
ninguém consegue metralhar todos os códigos de 6 dígitos.

Vou ser honesto sobre os limites, porque fingir o contrário é como as pessoas
se queimam: um PIN de 6 dígitos que só tem limitação de taxa é adequado para
um roteiro familiar de baixo risco, mas não tem o nível de um banco. Se você
está protegendo algo sensível, use uma frase-senha longa e um login de
verdade. Ajuste a trava ao que está atrás da porta.

O assistente de IA, com coleira curta

A parte divertida: um pequeno assistente — batizei de Yoshi — vivendo em
sua própria aba. Na versão privada da família, ele responde perguntas sobre a
viagem (“qual é o plano para o dia do aquário?”, “o que devemos levar na
mala?”) e faz pequenas edições protegidas no cronograma. O ponto principal
é que ele edita o mesmo arquivo de dados seguro, nunca o aplicativo
frágil — e está cercado: pode adicionar ou ajustar, mas não pode apagar dias,
e os voos e reservas ficam travados a ferro e fogo, de forma que nenhuma
mensagem de chat consegue tocá-los.

Fundamentalmente, o Yoshi roda em um modelo local self-hosted — uma IA
pequena no meu próprio hardware, não uma API paga na nuvem — acessado através
de um relay protegido. Isso é um assunto por si só (colocar com segurança um
chatbot hospedado localmente na web pública, e o quanto isso é lento em
hardware de consumidor), então escrevi sobre isso separadamente:
Colocando um Chatbot na Web Pública — no Meu Próprio Hardware.

Nota sobre a amostra para download abaixo: a aba do assistente está lá
para você ver onde ele mora, mas na amostra ela está deliberadamente
desligada
— não está conectada a nenhuma IA e não vai responder. Para
fazê-la funcionar na sua própria cópia, você apontaria para o seu próprio
modelo local. Isso é proposital: não vou distribuir um arquivo público que
silenciosamente liga para uma IA em execução.

Offline, instalável, e à prova de viagem internacional

O último conjunto de ajustes tratava da realidade de usar isso em uma viagem:
Wi-Fi de hotel instável, sem dados no avião, roaming que vai e volta. Então o
aplicativo virou um PWA instalável — adicione à tela inicial e ele abre
como um aplicativo de verdade — e funciona offline, servindo o roteiro
inteiro a partir do dispositivo depois de carregado uma vez. A única regra
com que tomei cuidado: ele só armazena o roteiro em cache depois de um
desbloqueio bem-sucedido, para que o cache offline nunca contorne o portão.
Online, ele sempre reverifica o portão; offline, confia na cópia que já está
no seu próprio celular. Para um plano de viagem em família, essa é a troca
certa.

(Service workers têm arestas afiadas — uma versão antiga em cache pode
continuar servindo uma página desatualizada muito depois de você ter
lançado uma correção. Aprendi a sempre publicar um worker atualizado, nunca
só apagar o antigo, para que os navegadores realmente peguem a mudança.)

Testando como se fosse de verdade, porque é

Porque a família estava usando isso durante a viagem, tratei toda mudança
como um deploy: fazer o build, renderizar a página de verdade em um navegador
headless depois da inicialização, operar as abas e botões reais, e só então
publicar. No meio da viagem, apliquei pequenas correções ao vivo desse jeito —
um selo “TODAY” que destaca o dia atual (ancorado no horário de Tóquio, não no
celular de quem está vendo), previsão do tempo atualizada, alguns ajustes de
layout — cada um verificado antes em um navegador de verdade. É a mesma lição
da pegadinha do reboot na inicialização, aprendida de novo: o arquivo
existir não é a mesma coisa que a funcionalidade funcionar.

Experimente — baixe a amostra

O download abaixo é uma amostra pública higienizada do aplicativo. Todo
nome, data, endereço e detalhe é conteúdo de demonstração fictício; é um plano
fictício de 7 dias em Tóquio. Abra o index.html em qualquer navegador — sem
instalação, sem servidor, sem conta. Funciona offline; só o cartão de tempo ao
vivo e os links de mapa acessam a internet.

Duas ressalvas honestas. Primeira: é um único arquivo de ~14 MB, porque o
aplicativo inteiro, fontes e imagens estão embutidos para rodar com zero
dependências — isso deixa o primeiro carregamento lento; uma build de
produção dividiria e comprimiria tudo isso. Segunda: como já dito, o
assistente de IA está desligado
na amostra — a aba abre e avisa isso.

Construa o seu

A caixa “implemente você mesmo” lá no topo não é um slogan — este texto foi
pensado para ser entregue direto a um assistente de IA. A receita, resumida:

  1. Coloque o plano em um arquivo de dados. Dias, paradas, anotações,
    tempo — um único arquivo legível que é a fonte única da verdade.
  2. Gere o aplicativo a partir dele. Uma etapa de build determinística
    transforma os dados na interface, então as edições são sempre nos dados,
    nunca no código gerado.
  3. Se você começar de uma exportação de ferramenta de design, respeite sua
    inicialização.
    Injete no template do aplicativo, use eventos delegados,
    e hospede você mesmo qualquer coisa que ele busque.
  4. Coloque o portão no servidor, não na página. Uma sessão mais bloqueio
    limitado por taxa vence um PIN em JavaScript sempre.
  5. Torne-o offline-first se ele viajar — armazene em cache só depois de
    um desbloqueio de verdade.
  6. Verifique em um navegador, depois da inicialização. Não “o arquivo
    existe”. A tela.

Pegadinhas, reunidas

  • A exportação reconstrói o DOM ao inicializar — qualquer coisa que você
    injete na página é apagada; injete no template do aplicativo em vez disso,
    e delegue eventos no document.
  • “Autocontido” pode não ser — uma exportação pode buscar seu framework em
    tempo de execução; hospede-o você mesmo, e teste atrás dos cabeçalhos de
    segurança do seu servidor de verdade, não só localmente.
  • Um PIN do lado do cliente é encenação — o conteúdo está na página;
    coloque o portão no servidor.
  • Um PIN de 6 dígitos só tem limitação de taxa, não é inquebrável — serve
    para baixo risco, errado para qualquer coisa sensível.
  • Service workers fazem cache agressivamente — publique um worker
    atualizado para lançar uma correção; apagar o arquivo antigo não cancela o
    registro dele.
  • “O arquivo existe” ≠ “a funcionalidade funciona” — renderize e olhe,
    sempre.

Downloads

Gratuito para uso pessoal. Se isso te economizar uma tarde, o botão do café está logo ali.


← Mais de IA e LLM Local