GoSki
스키 트래커로 모든 라이드를 기록하고 43개 리조트 3D 트레일 지도·눈 예보·실시간 리프트 알림을 제공합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Projeto destinado ao meu controle de treinos
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Aplicativo web para gerenciar treinos de musculação com foco em hipertrofia: rotina semanal editável, execução de treino ao vivo com registro de séries, progressão dupla de carga, estatísticas, histórico, recuperação estimada por grupo muscular e biblioteca de exercícios.
Requisitos: Node.js 18.18+ (recomendado 20 ou 22) e npm.
npm install
npm run dev
Abra http://localhost:3000. Sem configurar nada, o app roda em modo local: os dados ficam no localStorage do navegador e não são sincronizados.
| Script | O que faz |
|---|---|
npm run dev | Servidor de desenvolvimento |
npm run build | Build de produção (inclui checagem de tipos) |
npm run start | Serve o build de produção |
npm run typecheck | tsc --noEmit |
npm run lint | ESLint (regras next/core-web-vitals + next/typescript) |
npm run test | Testes unitários com Vitest (cálculos, persistência, backup) |
npm run check | typecheck + lint + test + build |
npm run vapid | Gera o par de chaves VAPID para as notificações push (uma vez por projeto) |
Copie .env.example para .env.local. Nenhuma variável é obrigatória.
| Variável | Obrigatória | Descrição |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | não | URL do projeto Supabase. Ativa o modo nuvem quando presente junto da chave. |
NEXT_PUBLIC_SUPABASE_ANON_KEY | não | Chave pública anon. Nunca use a service_role no frontend. |
NEXT_PUBLIC_ALLOW_LOCAL_MODE | não | Com o Supabase configurado a conta é obrigatória. true volta a oferecer "Usar modo local neste dispositivo" na tela de login. |
NEXT_PUBLIC_VAPID_PUBLIC_KEY | não | Chave pública VAPID (Web Push). Ativa a notificação de fim de descanso com a tela bloqueada. Ver seção 11. |
VAPID_PRIVATE_KEY | não | Chave privada VAPID. Só no servidor (sem prefixo NEXT_PUBLIC_): é usada pela rota /api/rest-timer para assinar os pushes. |
VAPID_SUBJECT | não | Contato do responsável exigido pelo protocolo (mailto:... ou https://...). |
Chaves privadas nunca chegam ao navegador: a service_role do Supabase não é usada em lugar nenhum e a VAPID_PRIVATE_KEY só é lida pela rota de API. Toda a segurança de dados no Supabase vem das políticas RLS (ver migrations).
| Modo local | Supabase | |
|---|---|---|
| Quando | Variáveis ausentes (ou NEXT_PUBLIC_ALLOW_LOCAL_MODE=true e o usuário escolheu "Usar modo local") | Variáveis presentes; login obrigatório |
| Onde salva | localStorage com prefixo ironlog:v1: | Tabela public.user_documents (uma linha por chave) |
| Sincroniza entre dispositivos | Não (o app deixa isso explícito na interface) | Sim |
| Backup | Exportar/importar JSON em Perfil | Idem, além da nuvem |
A camada de persistência é isolada (lib/storage/): ambos os modos salvam os mesmos seis documentos JSON (settings, exercises, routine, sessions, activeSession, restLogs). Migrar do local para a nuvem é exportar o backup e importar após entrar na conta.
supabase/migrations/0001_init.sql. Ele cria a tabela user_documents, habilita RLS e cria políticas para que cada usuário leia/escreva somente as próprias linhas. Alternativamente, com a CLI: supabase db push..env.local:
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
npm run dev. A tela inicial passa a pedir login (ou permite continuar em modo local).vercel.json já define npm run build).NEXT_PUBLIC_SUPABASE_URL e NEXT_PUBLIC_SUPABASE_ANON_KEY para os ambientes Production/Preview. Para notificações com a tela bloqueada, adicione também NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY e VAPID_SUBJECT (seção 11).npm i -g vercel
vercel login
vercel # primeiro deploy (preview)
vercel --prod # produção
Para variáveis: vercel env add NEXT_PUBLIC_SUPABASE_URL production (repita para a chave e para preview).
Em Authentication → URL Configuration no Supabase:
https://ironlog.vercel.app). É para onde o link de confirmação de e-mail leva por padrão; se ficar em http://localhost:3000, o usuário cai no localhost após confirmar.http://localhost:3000/** (desenvolvimento) e, se quiser previews, https://*.vercel.app/**.O app envia emailRedirectTo com a origem em que o cadastro foi feito, mas o Supabase só respeita origens presentes em Redirect URLs.
Implementadas em lib/volume.ts, lib/progression.ts, lib/recovery.ts e lib/stats.ts, com testes em tests/.
+incremento (2,5 kg superiores / 5 kg inferiores, configuráveis). Caso contrário sugere manter; se a maioria das séries ficou abaixo do mínimo, sugere reduzir. Sugestões nunca são apresentadas como obrigatórias.carga × (1 + reps/30).YYYY-MM-DD local para registros de descanso.app/ rotas (App Router) — páginas finas que montam componentes
api/rest-timer/ agenda/cancela o push de fim de descanso (responde e segue viva até o envio)
components/
layout/ AppShell (sidebar/bottom nav), AuthScreen, ServiceWorkerRegister
ui/ primitivos: Button, Card, Field, Dialog, Toast, Badge, Stat…
dashboard/ week/ workout/ history/ progression/ recovery/ exercises/ settings/
charts/ Recharts (LoadProgress, WeeklyVolume, Frequency, MuscleVolume)
lib/
store/AppProvider.tsx estado global + ações + salvamento automático por chave
storage/ StorageAdapter, LocalStorageAdapter, SupabaseStorageAdapter, repository (load/validate)
validation/ esquemas Zod (entidades, formulários, backup)
push/ notificações: shared (regras), client (navegador), server (núcleo testável da rota)
hooks/ useWakeLock, useRestPush
supabase/ client (navegador, chave anon) e server (cliente por requisição com o JWT do usuário)
volume.ts progression.ts recovery.ts stats.ts records.ts session.ts csv.ts backup.ts
types/ tipos do domínio
data/ biblioteca de exercícios e rotina inicial
public/sw.js service worker: só notificações (sem cache de páginas)
supabase/migrations/ SQL + políticas RLS (0001 documentos; 0002 cronômetro de descanso)
tests/ Vitest
Persistência: o AppProvider guarda o AppData em memória e, a cada alteração, salva somente as chaves que mudaram (debounce de 400 ms; 120 ms para a sessão ativa). Também salva em pagehide/visibilitychange. Falhas de gravação aparecem em um banner com botão de tentar novamente; dados corrompidos são substituídos pelo padrão e reportados.
npm run typecheck # TypeScript strict, noUncheckedIndexedAccess
npm run lint # ESLint
npm run test # 58 testes: volume, progressão, recordes, estatísticas, recuperação, persistência, backup, CSV, demos, push
npm run build # build de produção
Smoke test de ponta a ponta (opcional): scripts/e2e-smoke.mjs percorre o fluxo real no navegador (iniciar treino, registrar séries, recarregar e conferir persistência, finalizar, histórico, progressão, recuperação, criar exercício, editar semana, trocar tema) e falha se houver erros de console. Requer o Playwright e um Edge/Chrome instalado:
npm run dev # em um terminal
npm i -D playwright # uma vez (sem baixar navegadores)
BASE_URL=http://localhost:3000 node scripts/e2e-smoke.mjs
No Windows (PowerShell): $env:BASE_URL="http://localhost:3000"; node scripts/e2e-smoke.mjs. O script usa o canal msedge; troque por chrome em chromium.launch se preferir. Há também scripts/e2e-nav.mjs (navegação mobile), scripts/e2e-auth.mjs (tela de login obrigatória) e scripts/e2e-push.mjs (service worker, card de notificações, Wake Lock e respostas da rota /api/rest-timer).
Quando o cronômetro zera, o app avisa em três camadas. Cada uma cobre um cenário diferente; nenhuma substitui a outra.
| Camada | Quando funciona | O que precisa |
|---|---|---|
| Som + vibração no cronômetro | App aberto na tela | Nada (vibração não existe no iPhone; o som exige um toque prévio na página) |
| Tela acesa (Wake Lock) | Durante todo o treino ao vivo | Ligado por padrão em Perfil → "Manter a tela acesa". Evita que o aparelho bloqueie e congele timer/som. É a forma mais confiável no iPhone. |
| Notificação local | App em segundo plano (outra aba/app), enquanto o navegador mantiver a página viva | Permissão de notificações (Perfil → Notificações do descanso) |
| Push agendado pelo servidor | Tela bloqueada ou app fechado | Permissão + conta Supabase + chaves VAPID + migration 0002_rest_timers.sql. No iPhone/iPad: app instalado na Tela de Início |
POST /api/rest-timer com a inscrição de push e o horário em que o descanso termina.rest_timers, responde 202 na hora e continua executando com after() do Next até o fim do descanso.DELETE ou novo POST). Ao acordar, a função relê o token: se mudou, não envia nada.public/sw.js) mostra a notificação "Descanso finalizado — bora para a próxima série!". Se o app estiver visível, não mostra (o cronômetro já avisou). Tocar na notificação abre /treino.Por que esse desenho: a Web não tem API de notificação agendada no cliente; setTimeout morre com a aba suspensa; o Push é a única entrega que acorda o aparelho bloqueado. Manter a função viva até o envio dispensa filas externas e cron (que têm granularidade de 1 minuto). O limite é a duração máxima da função: 300 s no plano Hobby da Vercel com Fluid Compute (maxDuration = 300 na rota), então descansos acima de 4min50s não são agendados no servidor (as outras camadas seguem funcionando). Em projetos antigos da Vercel sem Fluid Compute o limite cai para 60 s: ative em Project Settings → Functions.
npm run vapid # imprime publicKey / privateKey
.env.local: NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT=mailto:seu@email. Depois, redeploy (a chave pública entra no build).supabase/migrations/0002_rest_timers.sql.iPhone/iPad: o Safari só permite notificações para apps instalados. Compartilhar → "Adicionar à Tela de Início", abrir pelo ícone e então ativar em Perfil. O app já envia os metadados necessários (manifest com display: standalone, apple-touch-icon, apple-mobile-web-app-capable). Vibração não é suportada no iOS; o som da notificação segue o sistema.
Android (Chrome/Edge/Samsung): funciona no navegador normal ou instalado. A economia de bateria agressiva de algumas marcas pode atrasar pushes; coloque o navegador como "sem restrição" se os avisos atrasarem.
A preferência é por dispositivo (a permissão também é) e fica no localStorage, não em Settings. O service worker não faz cache de páginas, para nunca servir uma versão antiga do app.
| Sintoma | Causa provável | Solução |
|---|---|---|
| Tela de login aparece mas eu queria usar sem conta | Supabase configurado | Clique em "Usar modo local neste dispositivo" ou remova as variáveis do .env.local |
Invalid login credentials | E-mail/senha incorretos ou e-mail não confirmado | Confirme o e-mail ou desative a confirmação no Supabase durante testes |
Forbidden use of secret API key in browser ou aviso de "chave SECRETA" no topo | A chave service_role/sb_secret_ foi colocada em NEXT_PUBLIC_SUPABASE_ANON_KEY | Em Project Settings → API Keys, use a chave Publishable (sb_publishable_...) ou a anon legada (eyJ... com role anon). Atualize a variável na Vercel e faça redeploy. O app detecta a chave secreta e cai no modo local por segurança |
| Dados não aparecem em outro navegador | Modo local não sincroniza | Exporte o backup JSON e importe no outro dispositivo, ou configure o Supabase |
new row violates row-level security policy | Migration não aplicada ou usuário sem sessão | Execute supabase/migrations/0001_init.sql e faça login novamente |
| Banner "Armazenamento local cheio" | Cota do localStorage excedida | Exporte um backup e exclua sessões antigas |
| Aviso de múltiplos lockfiles no build | Outro package-lock.json em pasta superior | Já tratado com outputFileTracingRoot; pode remover o lockfile estranho |
| Build falha com erro de tipo | TypeScript strict | Rode npm run typecheck e corrija; o build não ignora erros de tipo por decisão de projeto |
| Som do cronômetro não toca no celular | Navegador bloqueia áudio sem interação | Toque em qualquer botão do timer uma vez; ou desative o som em Perfil |
| Card de notificações diz "faltam as chaves VAPID" | NEXT_PUBLIC_VAPID_PUBLIC_KEY ausente no build | Gere com npm run vapid, configure as três variáveis na Vercel e faça redeploy |
| Teste de notificação: "Aplique a migration 0002" | Tabela rest_timers não existe |
Dados exibidos são sempre os registros reais do usuário. Nenhum gráfico usa valores inventados; estados vazios orientam o que fazer primeiro.
public/exercise-media/data/exercises.tsdata/exercise-demos.tsExecute supabase/migrations/0002_rest_timers.sql no SQL Editor |
| Notificação não chega com a tela bloqueada no iPhone | App aberto no Safari, não instalado | Compartilhar → Adicionar à Tela de Início e abrir por lá; ative as notificações de novo |
| Push chega só depois de ~1 min ou não chega | Função encerrada antes do envio (sem Fluid Compute) ou descanso > 290 s | Ative Fluid Compute em Project Settings → Functions; descansos longos usam só som/notificação local |
| Notificação duplicada (local + push) | Aba voltou a ficar visível no mesmo instante em que o push chegou | Comportamento raro e inofensivo; o service worker suprime o push quando o app está visível |
| Porta 3000 ocupada | Outro processo | npm run dev -- -p 3001 |