尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Aplicacao de leitura dinamica: importa artigos de qualquer URL (ou aceita texto colado) e apresenta o conteudo em ritmo controlado, com acompanhamento de velocidade e progresso. Next.js 16 (App Router) + React 19 + PostgreSQL via Drizzle ORM. Requisitos: Node.js 20+ e um PostgreSQL (local ou gerenciado). A aplicacao sobe em. O seed cria a conta [email protected] / demo1234. Para exibir essa dica na tela de login durante testes, defina NEXT_PUBLIC_DEMO_HINT=true (o valor e lido no momento do build).查看完整 README ↗
Aplicacao de leitura dinamica: importa artigos de qualquer URL (ou aceita texto colado) e apresenta o conteudo em ritmo controlado, com acompanhamento de velocidade e progresso.
Next.js 16 (App Router) + React 19 + PostgreSQL via Drizzle ORM.
Requisitos: Node.js 22 (o mesmo de engines no package.json) e um PostgreSQL (local ou gerenciado).
npm install
cp .env.example .env.local
npm run secret # gera o JWT_SECRET; cole no .env.local
npm run db:migrate # cria as tabelas
npm run db:seed # opcional: dados de demonstracao
npm run dev
A aplicacao sobe em http://localhost:3000.
O seed cria a conta [email protected] / demo1234. Para exibir essa dica na
tela de login durante testes, defina NEXT_PUBLIC_DEMO_HINT=true (o valor e
lido no momento do build).
Postgres gerenciado, plano gratuito sem cartao. A conexao vem de
DATABASE_URL: em desenvolvimento pelo .env.local, em producao pela variavel
de ambiente do host.
Use a Pooled connection do Neon — o host termina em -pooler. Em ambiente
serverless cada requisicao pode abrir uma conexao nova, e o endpoint direto
esgota o limite rapido.
O TLS e resolvido automaticamente: a verificacao de certificado liga quando o
host nao e local, respeitando o sslmode da connection string.
As migrations sao aplicadas antes do build (vercel-build), entao o schema
acompanha o deploy sem passo manual.
Um banco gratuito hiberna apos alguns minutos sem uso. A primeira requisicao depois disso leva alguns segundos, o que e esperado.
Em producao: https://rk-leitura.vercel.app.
O projeto rk-leitura esta ligado a este repositorio, com DATABASE_URL e
JWT_SECRET definidos como variaveis sensiveis e as funcoes na regiao gru1
(Sao Paulo) - perto de quem usa e do banco. Cada push na main gera um deploy.
O script vercel-build aplica as migrations antes de compilar, so no deploy de
Production (VERCEL_ENV=production), entao o schema acompanha o deploy sem
passo manual. Se as migrations falharem, o build para e o deploy anterior
continua no ar. Os deploys de Preview usam o mesmo banco de producao e por isso
nao migram (scripts/migrate-on-deploy.mjs): uma migration de PR ainda nao
aprovado nao altera o banco do app no ar.
Regra das migrations: sempre aditivas. Expandir antes, remover depois - coluna ou tabela nova entra numa versao, o codigo passa a usa-la, e o que deixou de ser usado sai numa versao seguinte, depois que nenhum deploy no ar depende dele. Como o Preview roda contra o schema de producao, um PR que depende de coluna nova so funciona por inteiro no Preview depois do merge.
Para criar o banco: projeto novo em https://neon.tech (gratuito, sem cartao),
regiao sa-east-1, e copie a Pooled connection.
Dois detalhes que valem em qualquer host:
.env.local presente vai junto e sobrescreve a
conexao de producao com a do seu Postgres local. Se GET /api/health
responder pointsToLocalhost: true, foi isso. O deploy ligado ao git nao tem
esse problema, porque parte do que esta versionado.Trocar o JWT_SECRET invalida todas as sessoes ativas. Os codigos de
recuperacao usam o RECOVERY_PEPPER como tempero; sem ele, usam o
JWT_SECRET, e troca-lo tambem invalida os codigos ja emitidos. O
/api/health informa se o RECOVERY_PEPPER esta definido. Para adota-lo sem
invalidar codigos antigos, defina-o com o valor atual do JWT_SECRET.
| Variavel | Obrigatoria | Descricao |
|---|---|---|
DATABASE_URL | sim | Connection string do Postgres (no Neon, a pooled connection). |
JWT_SECRET | sim | Chave de assinatura das sessoes, minimo 32 caracteres. |
RECOVERY_PEPPER | recomendada | Tempero dos codigos de recuperacao. Sem ela vale o JWT_SECRET, e troca-lo invalida os codigos. |
DATABASE_POOL_MAX | nao | Tamanho maximo do pool (padrao 5). |
NEXT_PUBLIC_DEMO_HINT | nao | true mostra as credenciais de demo no login. |
ANTHROPIC_API_KEY | nao | Funcoes de IA: questionario, dicionario, explicacao, perguntas ao texto, resumos, limpeza da importacao, etiquetas, sinopse e sintese. Sem ela, cada tela diz que a funcionalidade nao esta configurada e o resto do app segue igual. |
AI_USAGE_SECRET | nao | Protege GET /api/uso-ia, o uso e o custo estimado da IA por dia e por funcionalidade. Sem ela a rota responde 401. |
NEXT_PUBLIC_VAPID_KEY | nao | Chave publica do lembrete diario. |
VAPID_PRIVATE_KEY | nao | Chave privada do lembrete diario. |
VAPID_SUBJECT | nao | mailto: de contato exigido pelo protocolo de push. |
CRON_SECRET | nao | Separa o agendador de quem descobrir as rotas do cron (lembrete e acompanhamento de series e feeds). |
As tres variaveis do lembrete sao opcionais e andam juntas: sem elas, o cartao some dos Ajustes e a rota do agendador responde 401. O resto da aplicacao nao muda.
O par VAPID e gerado localmente, sem contratar nada:
node -e "const k=require('web-push').generateVAPIDKeys();console.log('NEXT_PUBLIC_VAPID_KEY='+k.publicKey);console.log('VAPID_PRIVATE_KEY='+k.privateKey)"
node -e "console.log('CRON_SECRET='+require('crypto').randomBytes(32).toString('base64url'))"
Na Vercel, VAPID_PRIVATE_KEY e CRON_SECRET entram como variaveis
sensitive; NEXT_PUBLIC_VAPID_KEY e publica por definicao, porque vai para o
navegador.
O plano Hobby da Vercel aceita um disparo por dia por cron - um
vercel.json com expressao mais frequente que isso faz a API recusar o deploy
inteiro com cron_jobs_limits_reached, e nao so ignorar o agendamento. Por
isso vercel.json marca 0 2 * * * (02:00 UTC, 23:00 em Brasilia).
Um disparo por dia entrega o lembrete sempre no mesmo horario, nao no que a
pessoa escolheu. Quem cobre a diferenca e
.github/workflows/lembretes.yml: bate na mesma rota de hora em hora, sem
custo. Para ativar, cadastre CRON_SECRET em *Settings > Secrets and variables
Actions* do repositorio, com o mesmo valor que esta na Vercel. Sem o segredo o fluxo termina sem fazer nada; o cron diario da Vercel continua valendo como rede de seguranca.
Os dois chamando a mesma rota nao duplicam nada. A regra de envio pergunta "ja passou da hora escolhida, hoje, sem leitura?" e marca o dia ao enviar, entao o lembrete sai uma vez so - e sai correto tanto de hora em hora quanto uma vez por dia.
O mesmo fluxo chama, em um segundo passo, /api/cron/acompanhamento: a
verificacao das series acompanhadas e dos feeds assinados. Cada origem e
consultada no maximo a cada 6 horas, entao a frequencia horaria nao
sobrecarrega os sites. Essa rota nao esta no vercel.json, pelo mesmo limite
de um disparo por dia do plano Hobby; sem CRON_SECRET no GitHub, series e
feeds simplesmente nao sao verificados.
O agendamento do GitHub tem duas limitacoes conhecidas, ambas aceitaveis aqui: execucao agendada pode atrasar alguns minutos quando a fila esta cheia, e o GitHub desativa fluxos agendados em repositorio sem commit ha 60 dias.
NEXT_PUBLIC_BASE_URLA variavel foi removida. Ela existia porque a rota de cadastro checava se o e-mail ja estava em uso fazendo uma requisicao HTTP para a propria aplicacao:
await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"}/api/auth/check`, ...)
Isso trazia tres problemas: dependia da variavel estar correta em producao (com
o padrao localhost:3000, o cadastro quebrava silenciosamente no deploy),
gastava uma ida e volta de rede para uma consulta que o processo ja podia fazer,
e expunha /api/auth/check, um endpoint publico que respondia se um e-mail
estava cadastrado — ou seja, permitia enumerar contas.
Hoje o cadastro consulta o banco diretamente e o endpoint /api/auth/check nao
existe mais. Nao ha nada a configurar no lugar.
| Comando | O que faz |
|---|---|
npm run dev | Servidor de desenvolvimento. |
npm run build / npm start | Build e execucao em producao. |
npm run lint / npm run typecheck | ESLint e TypeScript. |
npm test / npm run test:watch | Testes das regras puras (Vitest). |
npm run test:e2e | Testes no navegador (Playwright), contra a aplicacao com banco. |
npm run db:generate | Gera migration a partir do schema. |
npm run db:migrate | Aplica as migrations pendentes. |
npm run db:push | Sincroniza o schema sem migration (so em desenvolvimento). |
npm run db:seed | Dados de demonstracao. |
npm run secret | Gera um JWT_SECRET. |
src/
app/
(app)/ telas autenticadas: dashboard, textos, leitor, historico, ajustes
(auth)/ login e cadastro
api/ rotas HTTP
components/ navegacao, providers e primitivas de interface
db/ schema, conexao, migrations e seed
hooks/ useResource, useWakeLock
lib/ auth, sessao, parser, leitura, rate limit, busca protegida
middleware.ts protecao de rotas no servidor
drizzle/ migrations SQL versionadas
docs/backlog.md backlog de produto, com o status de cada story
tests/ testes das regras puras
e2e/ testes no navegador: fluxo principal e acessibilidade
JWT_SECRET em cookie httpOnly. A aplicacao
falha na inicializacao se a variavel nao existir — nao ha chave padrao.useEffect no cliente.texts.content em tempo de execucao. Nao ha tabela
com uma linha por palavra.reading_mode, words_per_chunk,
paragraph_pause e dim_lines ficam no banco sem uso.pause(motivo): freio, folha aberta, aba
escondida, marco, ponto de parada e descanso atualizam os mesmos
acumuladores. O prazo de cada palavra e absoluto (inicio + duracao), entao o
custo de render nao se acumula e mudar a velocidade no meio nao reinicia a
palavra. A pagina fica montada e memoizada por baixo do Word Runner, e o
rodape tem altura fixa: play e freio nao refazem a paginacao.src/lib/pacing.ts, pauses.ts, sentences.ts): peso
lexical normalizado (tamanho, digito, nome proprio, titulo, palavra ja
consultada) mais uma pausa depois da palavra - virgula, fim de frase e fim
de paragrafo, pela lei P300 x (duracao/200)^0,8 com P300 = 160/360/520 ms.
Um segmentador de frase unico (abreviaturas, tratamentos e iniciais por
idioma) serve as pausas, a navegacao por frase, o recuo ao retomar, o
destaque de frase e a narracao. A rampa de aquecimento e o recuo ao retomar
sao proporcionais ao tempo parado.runner, narracao, pagina),
contabilizada em src/lib/reader-session.ts. As pausas de pontuacao e o
atraso da rampa saem do tempo gravado: o ppm do historico e o das palavras,
e as previsoes de tempo (predictRunnerMs, ) somam as pausas
de volta. Virar a pagina para a frente conta a pagina quando o tempo nela e
plausivel (teto de 2 min por pagina). A sessao e gravada ao esconder a aba
e no , com ou sem leitura correndo, junto com os freios e recuos
(sinal para a sugestao de reduzir a velocidade).pauseOverheadpagehidearticleBody declarado em JSON-LD,
depois o container marcado com itemprop="articleBody" (microdado
schema.org), depois <article> e main, e so por ultimo o palpite pelo
maior container. O microdado e o caminho usado no Literotica, e com ele o
bloco de anuncios que vinha antes do conto some por completo. Quando o
container e exato, nenhum bloco e descartado por tamanho: em ficcao as falas
de dialogo sao curtas e o filtro antigo apagava boa parte do texto.<p> para cada um
(titulos com role="heading"). A regua de medicao monta os mesmos
paragrafos, senao a quebra de pagina calcularia uma altura menor que a real.?page= incrementado. A base e sempre a URL importada, que conta como pagina
1, nunca a da ultima busca - assim o parametro nao se acumula a cada chamada.
Nenhum desfecho esperado vira erro: "nao ha mais paginas", "a origem nao
respondeu" e "veio a mesma pagina de novo" voltam com 200 e um status
proprio, e a tela apenas informa. A mesma protecao anti-SSRF da importacao
vale aqui, e paginas repetidas sao detectadas para nao duplicar o texto./api/texts e /api/reading-sessions aceitam page e
perPage e devolvem o total. /api/stats calcula as somas do painel no
banco, em vez de baixar o historico inteiro para somar no cliente.lib/safe-fetch.ts, que resolve o
DNS e recusa enderecos de rede interna, revalidando cada redirecionamento.share_target, entao o
app instalado aparece na folha de compartilhamento do sistema: tocar em
Compartilhar no navegador manda o link para /compartilhar, que importa e
abre o leitor. Um endereco que ja esta na biblioteca nao e buscado de novo -
a leitura abre onde parou, e lib/source-url.ts cuida para que rastreamento,
barra final e fragmento nao facam o mesmo conto virar duas entradas. Quando a
origem manda uma selecao em vez de um link, o texto cai no formulario de
texto colado.share_target usa GET, entao a navegacao
em si nao grava nada: a importacao sai de POST /api/share. E ela so comeca
sozinha quando o cabecalho Sec-Fetch-Site indica que a navegacao nao veio de
outro site. O cookie de sessao e SameSite=Lax e acompanha navegacao de topo,
entao sem essa checagem qualquer pagina poderia apontar para /compartilhar e
fazer o servidor buscar um endereco escolhido por ela. Vindo de fora, a tela
espera um toque.drizzle/ e a unica fonte, aplicada por npm run db:migrate
— localmente a mao e, no deploy, pelo vercel-build antes do next build.lib/queries.ts guarda essas
consultas e serve tanto as telas quanto as rotas de API, para nao existirem
duas versoes da mesma pergunta ao banco.loading.tsx. Alem de responder
ao toque na hora, ele permite que o servidor envie a estrutura da pagina antes
de a consulta terminar - o que importa porque o banco do plano gratuito
hiberna e a primeira consulta depois disso demora.GET /api/health responde se o banco esta acessivel e
quais variaveis estao presentes, sem expor nenhum valor.e2e/ percorre o fluxo principal (cadastro, texto
colado, leitura ate o fim, historico), a pagina e o Word Runner, o motor do
leitor (folhas pausam, aba escondida grava, pagina virada conta, toque duplo
no freio), a leitura guiada por tempo livre, a aprendizagem (lacunas,
revisao de destaques, busca no conteudo, lote de links) e a verificacao de
acessibilidade com axe nos temas claro,
escuro e de alto contraste. Roda na CI em um job proprio, com Postgres de
servico. Localmente, npm run test:e2e reaproveita o servidor que estiver
na porta 3200 (ou E2E_PORT); PW_CHROMIUM_PATH aponta um Chromium ja
instalado. Cada teste se apresenta com um IP proprio em x-forwarded-for,
para o limite de cadastros por IP nao barrar a suite.requireSession e no
layout autenticado.POST /api/erros.src/lib/ai.ts:
chave, modelo por tarefa, fallback de recusa, traducao de erro, registro de
uso (ai_usage) e consentimento da conta. Modelos da familia 5.5 por
tarefa: Opus no questionario e nas perguntas ao texto, Sonnet na explicacao
e nos resumos, Haiku no dicionario, na limpeza da importacao, nas etiquetas
e na sinopse. O Haiku nao tem fallback no servidor, entao a recusa cai na
alternativa local de cada tela.speed_settings.ai_enabled). A rota recusa com 403 e consent, e
a folha que pediu mostra o aviso no lugar da resposta. Ajustes > Recursos
de IA liga, desliga e mostra o uso do dia.lang da pagina ou do dc:language do EPUB,
portugues quando ausente. Decide a voz da narracao e o pedido ao modelo no
dicionario e no questionario.leitura-vozes, que o service worker e a saida da conta preservam, e a
sintese roda em um Web Worker (public/piper-worker.js). O motor (ONNX
Runtime Web e o conversor de fonemas em wasm) e copiado de node_modules
para public/tts no postinstall; a pasta nao e versionada.src/lib/xray.ts) e
guarda marcadores de posicao (tabela bookmarks). Um salto maior que uma
pagina deixa o atalho "Voltar para onde parou". O teclado muda a velocidade
(setas para cima e para baixo), anda por pagina ou por frase (setas), abre o
significado da palavra atual (D), destaca a frase (H); "?" mostra a ajuda.
As regras ficam em src/lib/navigation.ts.recovery_codes, so o
hash) redefinem a senha em /recuperar. Cada login cria uma linha em
auth_sessions e o token leva o id dela: Ajustes lista os aparelhos e
desconecta um sem derrubar os outros.src/lib/backup.ts) e volta pela restauracao em Ajustes, em lotes de ate
3 MB com desfazer. Arquivos .docx sao convertidos em Markdown no navegador.
A tela de destaques baixa o texto inteiro anotado.