Eu Construí um Aplicativo de Roteiro de Viagem para Minha Família — com um Assistente de IA Dentro Dele
- Categoria
- IA e LLM Local
- 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:
- 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. - 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 nodocument(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:
- Coloque o plano em um arquivo de dados. Dias, paradas, anotações,
tempo — um único arquivo legível que é a fonte única da verdade. - 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. - 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. - 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. - Torne-o offline-first se ele viajar — armazene em cache só depois de
um desbloqueio de verdade. - 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 nodocument. - “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.