feat(welcome): tela de boas-vindas com login Google/Email + URL hardcoded v1.6.0
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run
UX simplificada drasticamente — usuário não precisa mais saber URL/token: - Tela de boas-vindas full-screen quando não logado - 3 botões grandes: Google, Email, Servidor próprio (avançado) - URL hardcoded https://shivao.pontualtech.work como padrão - Auto-conecta WebSocket + Google Calendar status após login - Pull inicial automático pra puxar dados existentes da conta Backend (server/src/index.js): - Endpoint POST /api/auth/google: recebe credential (Google ID token), valida via tokeninfo do Google, confere aud == GOOGLE_CLIENT_ID, cria user automático com email do Google se não existe, retorna JWT access+refresh tokens - Reusa GOOGLE_CLIENT_ID/SECRET já configurados no Coolify Frontend (app/diario-bordo.html): - Modal welcome com Google Sign-In via @google/gsi/client (script async) - Tabs Login/Signup pro fluxo email - Form avançado pra power users self-hosters - Skip pra modo offline - Once dismissed, fica oculto (localStorage flag) Service Worker bumped pra v1.6.0 (invalida cache antigo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c76b50b45b
commit
b48afaa84f
6 changed files with 539 additions and 5 deletions
|
|
@ -11,6 +11,7 @@
|
|||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg">
|
||||
<link rel="apple-touch-icon" href="/icon.svg">
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
<title>Diário de Bordo</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
|
@ -528,6 +529,65 @@ header{
|
|||
.fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)}
|
||||
.fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)}
|
||||
|
||||
/* === Welcome Screen === */
|
||||
.welcome-screen{
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
background:linear-gradient(135deg,#0e2a3d 0%,#1a3d54 50%,#0a1f2d 100%);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
padding:20px;overflow-y:auto;
|
||||
}
|
||||
.welcome-card{
|
||||
background:var(--bg-paper);
|
||||
border:1px solid rgba(212,160,74,.4);
|
||||
padding:32px 28px;max-width:380px;width:100%;
|
||||
box-shadow:0 30px 80px rgba(0,0,0,.5);
|
||||
}
|
||||
.welcome-logo{
|
||||
font-size:64px;text-align:center;margin-bottom:8px;
|
||||
filter:drop-shadow(0 4px 8px rgba(212,160,74,.3));
|
||||
}
|
||||
.welcome-title{
|
||||
font-family:var(--f-display);font-style:italic;font-weight:500;
|
||||
font-size:28px;color:var(--ink-deep);
|
||||
text-align:center;margin:0 0 8px;letter-spacing:-.01em;
|
||||
}
|
||||
.welcome-tagline{
|
||||
font-family:var(--f-display);font-style:italic;
|
||||
color:var(--sepia);text-align:center;
|
||||
font-size:14px;line-height:1.45;
|
||||
margin:0 0 24px;
|
||||
}
|
||||
.welcome-buttons{display:flex;flex-direction:column;gap:8px}
|
||||
.welcome-btn{
|
||||
display:flex;align-items:center;justify-content:center;gap:10px;
|
||||
padding:13px 16px;border:1px solid var(--rule);
|
||||
background:var(--bg-canvas);color:var(--ink-deep);
|
||||
font-family:var(--f-body);font-size:14.5px;font-weight:500;
|
||||
cursor:pointer;width:100%;
|
||||
transition:all .15s;text-align:center;
|
||||
}
|
||||
.welcome-btn:hover{background:var(--bg-aged);border-color:var(--brass)}
|
||||
.welcome-btn:active{transform:scale(.98)}
|
||||
.welcome-btn-google{background:#fff;border-color:#dadce0}
|
||||
.welcome-btn-google:hover{background:#f8f9fa;box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
||||
.welcome-btn-email{}
|
||||
.welcome-btn-primary{background:var(--brass);color:var(--bg-paper);border-color:var(--brass-deep);font-weight:600}
|
||||
.welcome-btn-primary:hover{background:var(--brass-deep)}
|
||||
.welcome-btn-text{
|
||||
background:transparent;border:none;color:var(--sepia);
|
||||
font-size:12.5px;text-decoration:underline;
|
||||
}
|
||||
.welcome-btn-text:hover{color:var(--ink-deep);background:transparent}
|
||||
.welcome-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--rule)}
|
||||
.welcome-tab{
|
||||
flex:1;background:transparent;border:none;
|
||||
padding:10px;font-family:var(--f-mono);font-size:11px;
|
||||
letter-spacing:.12em;text-transform:uppercase;color:var(--sepia);
|
||||
cursor:pointer;border-bottom:2px solid transparent;
|
||||
margin-bottom:-1px;
|
||||
}
|
||||
.welcome-tab.active{color:var(--brass);border-bottom-color:var(--brass)}
|
||||
|
||||
/* === Sync Indicator === */
|
||||
.sync-indicator{
|
||||
display:inline-block;font-size:10px;
|
||||
|
|
@ -2007,6 +2067,55 @@ Hora: {HORA}</textarea>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Welcome / Login Screen -->
|
||||
<div class="welcome-screen" id="welcome-screen" style="display:none">
|
||||
<div class="welcome-card">
|
||||
<div class="welcome-logo">⚓</div>
|
||||
<h1 class="welcome-title">Shivão · Diário de Bordo</h1>
|
||||
<p class="welcome-tagline">Sua viagem, suas fotos, sua âncora — em qualquer dispositivo.</p>
|
||||
|
||||
<div class="welcome-buttons">
|
||||
<button class="welcome-btn welcome-btn-google" id="welcome-google" onclick="welcomeGoogleClick()">
|
||||
<svg viewBox="0 0 18 18" width="18" height="18"><path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"/><path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z"/><path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/><path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"/></svg>
|
||||
<span>Entrar com Google</span>
|
||||
</button>
|
||||
<button class="welcome-btn welcome-btn-email" onclick="welcomeShowEmail()">
|
||||
<span style="font-size:18px">✉️</span>
|
||||
<span>Continuar com email</span>
|
||||
</button>
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeShowAdvanced()">
|
||||
⚙️ Configurar servidor próprio (avançado)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Email form (hidden initially) -->
|
||||
<div id="welcome-email-form" style="display:none;margin-top:18px">
|
||||
<div class="welcome-tabs">
|
||||
<button class="welcome-tab active" id="we-tab-login" onclick="welcomeSwitchTab('login')">Entrar</button>
|
||||
<button class="welcome-tab" id="we-tab-signup" onclick="welcomeSwitchTab('signup')">Criar conta</button>
|
||||
</div>
|
||||
<div class="field"><input type="email" id="we-email" placeholder="seu@email.com" autocomplete="email"></div>
|
||||
<div class="field" id="we-name-field" style="display:none"><input type="text" id="we-name" placeholder="Seu nome" autocomplete="name"></div>
|
||||
<div class="field"><input type="password" id="we-pwd" placeholder="senha (mín 8 caracteres)" autocomplete="current-password"></div>
|
||||
<button class="welcome-btn welcome-btn-primary" id="we-submit" onclick="welcomeEmailSubmit()">Entrar</button>
|
||||
<div id="we-msg" style="margin-top:8px;font-size:12px;color:var(--storm);text-align:center"></div>
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeBack()" style="margin-top:8px">← Voltar</button>
|
||||
</div>
|
||||
|
||||
<!-- Advanced (server URL/token) form -->
|
||||
<div id="welcome-advanced-form" style="display:none;margin-top:18px">
|
||||
<p style="font-size:12px;color:var(--sepia);text-align:center;margin-bottom:12px">Pra usar com seu próprio servidor Shivão self-hosted.</p>
|
||||
<div class="field"><input type="url" id="we-srv-url" placeholder="https://seu-shivao.com"></div>
|
||||
<div class="field"><input type="password" id="we-srv-token" placeholder="BOAT_TOKEN do servidor"></div>
|
||||
<button class="welcome-btn welcome-btn-primary" onclick="welcomeAdvancedSubmit()">Conectar</button>
|
||||
<div id="we-srv-msg" style="margin-top:8px;font-size:12px;color:var(--storm);text-align:center"></div>
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeBack()" style="margin-top:8px">← Voltar</button>
|
||||
</div>
|
||||
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeSkip()" style="margin-top:14px;font-size:11px;opacity:.6">Usar offline (sem sync entre dispositivos)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="zone-toast" id="zone-toast"></div>
|
||||
|
||||
|
|
@ -2220,6 +2329,8 @@ function dbDelete(id){return new Promise((r,j)=>{const q=db.transaction('media',
|
|||
function dbAll(){return new Promise((r,j)=>{const q=db.transaction('media','readonly').objectStore('media').getAll();q.onsuccess=()=>r(q.result);q.onerror=()=>j(q.error)})}
|
||||
|
||||
function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'};state.boats=d.boats||[];state.activeBoatId=d.activeBoatId||null;state.units=d.units||'metric'}}catch(e){console.warn(e)}
|
||||
// Default cloud URL pra usuários social/email (não precisam digitar nada)
|
||||
if(!state.cloud.url&&typeof DEFAULT_CLOUD_URL!=='undefined')state.cloud.url=DEFAULT_CLOUD_URL;
|
||||
migrateBoatsSchema();
|
||||
// popular checklists padrão se vazio
|
||||
if(!state.checklists.length){
|
||||
|
|
@ -3024,7 +3135,11 @@ async function updateStorageInfo(){
|
|||
if(cloudConfigured()){rtConnect();refreshGoogleStatus()}
|
||||
// tenta auto-fetch do tempo após pequeno delay
|
||||
setTimeout(maybeAutoFetchWeather,3000);
|
||||
// Welcome screen — só pra usuários sem login
|
||||
setTimeout(maybeShowWelcome,300);
|
||||
})();
|
||||
// Re-tenta init Google Sign-In quando o script async carrega
|
||||
window.addEventListener('load',()=>setTimeout(()=>{if(document.getElementById('welcome-screen').style.display==='flex')initGoogleSignIn()},500));
|
||||
document.addEventListener('visibilitychange',async()=>{
|
||||
if(document.visibilityState==='visible'){
|
||||
if(tracking.active&&!tracking.wakeLock)await requestWakeLock();
|
||||
|
|
@ -3679,6 +3794,137 @@ async function cloudFetch(path,opts={}){
|
|||
return r;
|
||||
}
|
||||
|
||||
// ============ WELCOME / LOGIN SCREEN ============
|
||||
// Default cloud URL — usuários não-avançados não precisam configurar nada
|
||||
const DEFAULT_CLOUD_URL='https://shivao.pontualtech.work';
|
||||
const GOOGLE_CLIENT_ID_FRONTEND='989184529532-uceun7l7a12e63fdrkilnh8vml0v0lv4.apps.googleusercontent.com';
|
||||
|
||||
function maybeShowWelcome(){
|
||||
const ws=document.getElementById('welcome-screen');
|
||||
if(!ws)return;
|
||||
// Mostra se: nuvem não configurada E user não está logado E nunca dispensou
|
||||
const dismissed=localStorage.getItem('shivao_welcome_dismissed')==='1';
|
||||
const needsSetup=(!cloudConfigured()||!state.auth)&&!dismissed;
|
||||
ws.style.display=needsSetup?'flex':'none';
|
||||
// Se mostrar, prepara o Google Sign-In quando carregar
|
||||
if(needsSetup&&window.google?.accounts?.id){
|
||||
initGoogleSignIn();
|
||||
}
|
||||
}
|
||||
|
||||
function welcomeShowEmail(){
|
||||
document.getElementById('welcome-email-form').style.display='block';
|
||||
document.querySelector('.welcome-buttons').style.display='none';
|
||||
document.getElementById('welcome-advanced-form').style.display='none';
|
||||
}
|
||||
function welcomeShowAdvanced(){
|
||||
document.getElementById('welcome-advanced-form').style.display='block';
|
||||
document.querySelector('.welcome-buttons').style.display='none';
|
||||
document.getElementById('welcome-email-form').style.display='none';
|
||||
}
|
||||
function welcomeBack(){
|
||||
document.querySelector('.welcome-buttons').style.display='flex';
|
||||
document.getElementById('welcome-email-form').style.display='none';
|
||||
document.getElementById('welcome-advanced-form').style.display='none';
|
||||
}
|
||||
function welcomeSwitchTab(t){
|
||||
document.getElementById('we-tab-login').classList.toggle('active',t==='login');
|
||||
document.getElementById('we-tab-signup').classList.toggle('active',t==='signup');
|
||||
document.getElementById('we-name-field').style.display=t==='signup'?'block':'none';
|
||||
document.getElementById('we-submit').textContent=t==='login'?'Entrar':'Criar conta';
|
||||
}
|
||||
function welcomeSkip(){
|
||||
if(!confirm('Continuar sem sincronização? Seus dados ficam só neste dispositivo.'))return;
|
||||
localStorage.setItem('shivao_welcome_dismissed','1');
|
||||
document.getElementById('welcome-screen').style.display='none';
|
||||
}
|
||||
async function welcomeEmailSubmit(){
|
||||
const email=document.getElementById('we-email').value.trim();
|
||||
const pwd=document.getElementById('we-pwd').value;
|
||||
const name=document.getElementById('we-name').value.trim();
|
||||
const isSignup=document.getElementById('we-tab-signup').classList.contains('active');
|
||||
const msg=document.getElementById('we-msg');
|
||||
msg.style.color='var(--storm)';msg.textContent='';
|
||||
if(!email||!pwd){msg.textContent='Preencha email e senha';return}
|
||||
if(isSignup&&pwd.length<8){msg.textContent='Senha mínima: 8 caracteres';return}
|
||||
// Garante URL hardcoded
|
||||
state.cloud.url=DEFAULT_CLOUD_URL;
|
||||
saveState();
|
||||
try{
|
||||
if(isSignup)await authSignup(email,pwd,name);
|
||||
else await authLogin(email,pwd);
|
||||
msg.style.color='var(--algae)';msg.textContent='Logado! Sincronizando...';
|
||||
welcomeFinish();
|
||||
}catch(e){msg.textContent=e.message}
|
||||
}
|
||||
async function welcomeAdvancedSubmit(){
|
||||
const url=document.getElementById('we-srv-url').value.trim();
|
||||
const tok=document.getElementById('we-srv-token').value.trim();
|
||||
const msg=document.getElementById('we-srv-msg');
|
||||
if(!url||!tok){msg.textContent='Preencha URL e token';return}
|
||||
state.cloud={url:url.replace(/\/$/,''),token:tok,lastSync:0};
|
||||
saveState();
|
||||
msg.style.color='var(--algae)';msg.textContent='Conectando...';
|
||||
welcomeFinish();
|
||||
}
|
||||
function welcomeFinish(){
|
||||
document.getElementById('welcome-screen').style.display='none';
|
||||
// Conecta sync automaticamente
|
||||
rtConnect();
|
||||
refreshGoogleStatus();
|
||||
// Pull inicial pra puxar dados existentes da conta
|
||||
setTimeout(()=>autoPullNow().catch(()=>{}),500);
|
||||
}
|
||||
|
||||
function initGoogleSignIn(){
|
||||
if(!window.google?.accounts?.id||window._gsiInited)return;
|
||||
try{
|
||||
window.google.accounts.id.initialize({
|
||||
client_id:GOOGLE_CLIENT_ID_FRONTEND,
|
||||
callback:onGoogleCredential,
|
||||
auto_select:false,
|
||||
ux_mode:'popup',
|
||||
});
|
||||
window._gsiInited=true;
|
||||
}catch(e){console.warn('[gsi] init failed',e)}
|
||||
}
|
||||
function welcomeGoogleClick(){
|
||||
if(!window.google?.accounts?.id){
|
||||
toast('Google Sign-In ainda carregando, aguarde 2s');
|
||||
return;
|
||||
}
|
||||
initGoogleSignIn();
|
||||
try{
|
||||
window.google.accounts.id.prompt(()=>{});
|
||||
// Fallback: render botão visível ao lado caso prompt seja bloqueado
|
||||
}catch(e){
|
||||
console.warn('[gsi] prompt failed',e);
|
||||
toast('Erro: '+e.message);
|
||||
}
|
||||
}
|
||||
async function onGoogleCredential(resp){
|
||||
if(!resp?.credential){toast('Sem credential do Google');return}
|
||||
state.cloud.url=DEFAULT_CLOUD_URL;
|
||||
saveState();
|
||||
try{
|
||||
const r=await fetch(cloudUrl('/api/auth/google'),{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({credential:resp.credential}),
|
||||
});
|
||||
const j=await r.json();
|
||||
if(!r.ok)throw new Error(j.error||'login failed');
|
||||
state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user};
|
||||
saveState();
|
||||
toast('Bem-vindo, '+(j.user.name||j.user.email));
|
||||
welcomeFinish();
|
||||
if(typeof renderAuthBox==='function')renderAuthBox();
|
||||
}catch(e){
|
||||
console.warn('[google login]',e);
|
||||
toast('Falha login Google: '+e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ REALTIME SYNC (WebSocket + auto push/pull) ============
|
||||
// Conecta ao /ws do servidor, escuta notificações de mudança de outros devices
|
||||
// e dispara pull/push automáticos. Reconnect com backoff exponencial.
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ android {
|
|||
applicationId "br.com.pontualtech.shivao"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 6
|
||||
versionName "1.5.0"
|
||||
versionCode 7
|
||||
versionName "1.6.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "shivao-mobile",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg">
|
||||
<link rel="apple-touch-icon" href="/icon.svg">
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
<title>Diário de Bordo</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
|
@ -528,6 +529,65 @@ header{
|
|||
.fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)}
|
||||
.fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)}
|
||||
|
||||
/* === Welcome Screen === */
|
||||
.welcome-screen{
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
background:linear-gradient(135deg,#0e2a3d 0%,#1a3d54 50%,#0a1f2d 100%);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
padding:20px;overflow-y:auto;
|
||||
}
|
||||
.welcome-card{
|
||||
background:var(--bg-paper);
|
||||
border:1px solid rgba(212,160,74,.4);
|
||||
padding:32px 28px;max-width:380px;width:100%;
|
||||
box-shadow:0 30px 80px rgba(0,0,0,.5);
|
||||
}
|
||||
.welcome-logo{
|
||||
font-size:64px;text-align:center;margin-bottom:8px;
|
||||
filter:drop-shadow(0 4px 8px rgba(212,160,74,.3));
|
||||
}
|
||||
.welcome-title{
|
||||
font-family:var(--f-display);font-style:italic;font-weight:500;
|
||||
font-size:28px;color:var(--ink-deep);
|
||||
text-align:center;margin:0 0 8px;letter-spacing:-.01em;
|
||||
}
|
||||
.welcome-tagline{
|
||||
font-family:var(--f-display);font-style:italic;
|
||||
color:var(--sepia);text-align:center;
|
||||
font-size:14px;line-height:1.45;
|
||||
margin:0 0 24px;
|
||||
}
|
||||
.welcome-buttons{display:flex;flex-direction:column;gap:8px}
|
||||
.welcome-btn{
|
||||
display:flex;align-items:center;justify-content:center;gap:10px;
|
||||
padding:13px 16px;border:1px solid var(--rule);
|
||||
background:var(--bg-canvas);color:var(--ink-deep);
|
||||
font-family:var(--f-body);font-size:14.5px;font-weight:500;
|
||||
cursor:pointer;width:100%;
|
||||
transition:all .15s;text-align:center;
|
||||
}
|
||||
.welcome-btn:hover{background:var(--bg-aged);border-color:var(--brass)}
|
||||
.welcome-btn:active{transform:scale(.98)}
|
||||
.welcome-btn-google{background:#fff;border-color:#dadce0}
|
||||
.welcome-btn-google:hover{background:#f8f9fa;box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
||||
.welcome-btn-email{}
|
||||
.welcome-btn-primary{background:var(--brass);color:var(--bg-paper);border-color:var(--brass-deep);font-weight:600}
|
||||
.welcome-btn-primary:hover{background:var(--brass-deep)}
|
||||
.welcome-btn-text{
|
||||
background:transparent;border:none;color:var(--sepia);
|
||||
font-size:12.5px;text-decoration:underline;
|
||||
}
|
||||
.welcome-btn-text:hover{color:var(--ink-deep);background:transparent}
|
||||
.welcome-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--rule)}
|
||||
.welcome-tab{
|
||||
flex:1;background:transparent;border:none;
|
||||
padding:10px;font-family:var(--f-mono);font-size:11px;
|
||||
letter-spacing:.12em;text-transform:uppercase;color:var(--sepia);
|
||||
cursor:pointer;border-bottom:2px solid transparent;
|
||||
margin-bottom:-1px;
|
||||
}
|
||||
.welcome-tab.active{color:var(--brass);border-bottom-color:var(--brass)}
|
||||
|
||||
/* === Sync Indicator === */
|
||||
.sync-indicator{
|
||||
display:inline-block;font-size:10px;
|
||||
|
|
@ -2007,6 +2067,55 @@ Hora: {HORA}</textarea>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Welcome / Login Screen -->
|
||||
<div class="welcome-screen" id="welcome-screen" style="display:none">
|
||||
<div class="welcome-card">
|
||||
<div class="welcome-logo">⚓</div>
|
||||
<h1 class="welcome-title">Shivão · Diário de Bordo</h1>
|
||||
<p class="welcome-tagline">Sua viagem, suas fotos, sua âncora — em qualquer dispositivo.</p>
|
||||
|
||||
<div class="welcome-buttons">
|
||||
<button class="welcome-btn welcome-btn-google" id="welcome-google" onclick="welcomeGoogleClick()">
|
||||
<svg viewBox="0 0 18 18" width="18" height="18"><path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"/><path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z"/><path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/><path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"/></svg>
|
||||
<span>Entrar com Google</span>
|
||||
</button>
|
||||
<button class="welcome-btn welcome-btn-email" onclick="welcomeShowEmail()">
|
||||
<span style="font-size:18px">✉️</span>
|
||||
<span>Continuar com email</span>
|
||||
</button>
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeShowAdvanced()">
|
||||
⚙️ Configurar servidor próprio (avançado)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Email form (hidden initially) -->
|
||||
<div id="welcome-email-form" style="display:none;margin-top:18px">
|
||||
<div class="welcome-tabs">
|
||||
<button class="welcome-tab active" id="we-tab-login" onclick="welcomeSwitchTab('login')">Entrar</button>
|
||||
<button class="welcome-tab" id="we-tab-signup" onclick="welcomeSwitchTab('signup')">Criar conta</button>
|
||||
</div>
|
||||
<div class="field"><input type="email" id="we-email" placeholder="seu@email.com" autocomplete="email"></div>
|
||||
<div class="field" id="we-name-field" style="display:none"><input type="text" id="we-name" placeholder="Seu nome" autocomplete="name"></div>
|
||||
<div class="field"><input type="password" id="we-pwd" placeholder="senha (mín 8 caracteres)" autocomplete="current-password"></div>
|
||||
<button class="welcome-btn welcome-btn-primary" id="we-submit" onclick="welcomeEmailSubmit()">Entrar</button>
|
||||
<div id="we-msg" style="margin-top:8px;font-size:12px;color:var(--storm);text-align:center"></div>
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeBack()" style="margin-top:8px">← Voltar</button>
|
||||
</div>
|
||||
|
||||
<!-- Advanced (server URL/token) form -->
|
||||
<div id="welcome-advanced-form" style="display:none;margin-top:18px">
|
||||
<p style="font-size:12px;color:var(--sepia);text-align:center;margin-bottom:12px">Pra usar com seu próprio servidor Shivão self-hosted.</p>
|
||||
<div class="field"><input type="url" id="we-srv-url" placeholder="https://seu-shivao.com"></div>
|
||||
<div class="field"><input type="password" id="we-srv-token" placeholder="BOAT_TOKEN do servidor"></div>
|
||||
<button class="welcome-btn welcome-btn-primary" onclick="welcomeAdvancedSubmit()">Conectar</button>
|
||||
<div id="we-srv-msg" style="margin-top:8px;font-size:12px;color:var(--storm);text-align:center"></div>
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeBack()" style="margin-top:8px">← Voltar</button>
|
||||
</div>
|
||||
|
||||
<button class="welcome-btn welcome-btn-text" onclick="welcomeSkip()" style="margin-top:14px;font-size:11px;opacity:.6">Usar offline (sem sync entre dispositivos)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="zone-toast" id="zone-toast"></div>
|
||||
|
||||
|
|
@ -2220,6 +2329,8 @@ function dbDelete(id){return new Promise((r,j)=>{const q=db.transaction('media',
|
|||
function dbAll(){return new Promise((r,j)=>{const q=db.transaction('media','readonly').objectStore('media').getAll();q.onsuccess=()=>r(q.result);q.onerror=()=>j(q.error)})}
|
||||
|
||||
function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'};state.boats=d.boats||[];state.activeBoatId=d.activeBoatId||null;state.units=d.units||'metric'}}catch(e){console.warn(e)}
|
||||
// Default cloud URL pra usuários social/email (não precisam digitar nada)
|
||||
if(!state.cloud.url&&typeof DEFAULT_CLOUD_URL!=='undefined')state.cloud.url=DEFAULT_CLOUD_URL;
|
||||
migrateBoatsSchema();
|
||||
// popular checklists padrão se vazio
|
||||
if(!state.checklists.length){
|
||||
|
|
@ -3024,7 +3135,11 @@ async function updateStorageInfo(){
|
|||
if(cloudConfigured()){rtConnect();refreshGoogleStatus()}
|
||||
// tenta auto-fetch do tempo após pequeno delay
|
||||
setTimeout(maybeAutoFetchWeather,3000);
|
||||
// Welcome screen — só pra usuários sem login
|
||||
setTimeout(maybeShowWelcome,300);
|
||||
})();
|
||||
// Re-tenta init Google Sign-In quando o script async carrega
|
||||
window.addEventListener('load',()=>setTimeout(()=>{if(document.getElementById('welcome-screen').style.display==='flex')initGoogleSignIn()},500));
|
||||
document.addEventListener('visibilitychange',async()=>{
|
||||
if(document.visibilityState==='visible'){
|
||||
if(tracking.active&&!tracking.wakeLock)await requestWakeLock();
|
||||
|
|
@ -3679,6 +3794,137 @@ async function cloudFetch(path,opts={}){
|
|||
return r;
|
||||
}
|
||||
|
||||
// ============ WELCOME / LOGIN SCREEN ============
|
||||
// Default cloud URL — usuários não-avançados não precisam configurar nada
|
||||
const DEFAULT_CLOUD_URL='https://shivao.pontualtech.work';
|
||||
const GOOGLE_CLIENT_ID_FRONTEND='989184529532-uceun7l7a12e63fdrkilnh8vml0v0lv4.apps.googleusercontent.com';
|
||||
|
||||
function maybeShowWelcome(){
|
||||
const ws=document.getElementById('welcome-screen');
|
||||
if(!ws)return;
|
||||
// Mostra se: nuvem não configurada E user não está logado E nunca dispensou
|
||||
const dismissed=localStorage.getItem('shivao_welcome_dismissed')==='1';
|
||||
const needsSetup=(!cloudConfigured()||!state.auth)&&!dismissed;
|
||||
ws.style.display=needsSetup?'flex':'none';
|
||||
// Se mostrar, prepara o Google Sign-In quando carregar
|
||||
if(needsSetup&&window.google?.accounts?.id){
|
||||
initGoogleSignIn();
|
||||
}
|
||||
}
|
||||
|
||||
function welcomeShowEmail(){
|
||||
document.getElementById('welcome-email-form').style.display='block';
|
||||
document.querySelector('.welcome-buttons').style.display='none';
|
||||
document.getElementById('welcome-advanced-form').style.display='none';
|
||||
}
|
||||
function welcomeShowAdvanced(){
|
||||
document.getElementById('welcome-advanced-form').style.display='block';
|
||||
document.querySelector('.welcome-buttons').style.display='none';
|
||||
document.getElementById('welcome-email-form').style.display='none';
|
||||
}
|
||||
function welcomeBack(){
|
||||
document.querySelector('.welcome-buttons').style.display='flex';
|
||||
document.getElementById('welcome-email-form').style.display='none';
|
||||
document.getElementById('welcome-advanced-form').style.display='none';
|
||||
}
|
||||
function welcomeSwitchTab(t){
|
||||
document.getElementById('we-tab-login').classList.toggle('active',t==='login');
|
||||
document.getElementById('we-tab-signup').classList.toggle('active',t==='signup');
|
||||
document.getElementById('we-name-field').style.display=t==='signup'?'block':'none';
|
||||
document.getElementById('we-submit').textContent=t==='login'?'Entrar':'Criar conta';
|
||||
}
|
||||
function welcomeSkip(){
|
||||
if(!confirm('Continuar sem sincronização? Seus dados ficam só neste dispositivo.'))return;
|
||||
localStorage.setItem('shivao_welcome_dismissed','1');
|
||||
document.getElementById('welcome-screen').style.display='none';
|
||||
}
|
||||
async function welcomeEmailSubmit(){
|
||||
const email=document.getElementById('we-email').value.trim();
|
||||
const pwd=document.getElementById('we-pwd').value;
|
||||
const name=document.getElementById('we-name').value.trim();
|
||||
const isSignup=document.getElementById('we-tab-signup').classList.contains('active');
|
||||
const msg=document.getElementById('we-msg');
|
||||
msg.style.color='var(--storm)';msg.textContent='';
|
||||
if(!email||!pwd){msg.textContent='Preencha email e senha';return}
|
||||
if(isSignup&&pwd.length<8){msg.textContent='Senha mínima: 8 caracteres';return}
|
||||
// Garante URL hardcoded
|
||||
state.cloud.url=DEFAULT_CLOUD_URL;
|
||||
saveState();
|
||||
try{
|
||||
if(isSignup)await authSignup(email,pwd,name);
|
||||
else await authLogin(email,pwd);
|
||||
msg.style.color='var(--algae)';msg.textContent='Logado! Sincronizando...';
|
||||
welcomeFinish();
|
||||
}catch(e){msg.textContent=e.message}
|
||||
}
|
||||
async function welcomeAdvancedSubmit(){
|
||||
const url=document.getElementById('we-srv-url').value.trim();
|
||||
const tok=document.getElementById('we-srv-token').value.trim();
|
||||
const msg=document.getElementById('we-srv-msg');
|
||||
if(!url||!tok){msg.textContent='Preencha URL e token';return}
|
||||
state.cloud={url:url.replace(/\/$/,''),token:tok,lastSync:0};
|
||||
saveState();
|
||||
msg.style.color='var(--algae)';msg.textContent='Conectando...';
|
||||
welcomeFinish();
|
||||
}
|
||||
function welcomeFinish(){
|
||||
document.getElementById('welcome-screen').style.display='none';
|
||||
// Conecta sync automaticamente
|
||||
rtConnect();
|
||||
refreshGoogleStatus();
|
||||
// Pull inicial pra puxar dados existentes da conta
|
||||
setTimeout(()=>autoPullNow().catch(()=>{}),500);
|
||||
}
|
||||
|
||||
function initGoogleSignIn(){
|
||||
if(!window.google?.accounts?.id||window._gsiInited)return;
|
||||
try{
|
||||
window.google.accounts.id.initialize({
|
||||
client_id:GOOGLE_CLIENT_ID_FRONTEND,
|
||||
callback:onGoogleCredential,
|
||||
auto_select:false,
|
||||
ux_mode:'popup',
|
||||
});
|
||||
window._gsiInited=true;
|
||||
}catch(e){console.warn('[gsi] init failed',e)}
|
||||
}
|
||||
function welcomeGoogleClick(){
|
||||
if(!window.google?.accounts?.id){
|
||||
toast('Google Sign-In ainda carregando, aguarde 2s');
|
||||
return;
|
||||
}
|
||||
initGoogleSignIn();
|
||||
try{
|
||||
window.google.accounts.id.prompt(()=>{});
|
||||
// Fallback: render botão visível ao lado caso prompt seja bloqueado
|
||||
}catch(e){
|
||||
console.warn('[gsi] prompt failed',e);
|
||||
toast('Erro: '+e.message);
|
||||
}
|
||||
}
|
||||
async function onGoogleCredential(resp){
|
||||
if(!resp?.credential){toast('Sem credential do Google');return}
|
||||
state.cloud.url=DEFAULT_CLOUD_URL;
|
||||
saveState();
|
||||
try{
|
||||
const r=await fetch(cloudUrl('/api/auth/google'),{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({credential:resp.credential}),
|
||||
});
|
||||
const j=await r.json();
|
||||
if(!r.ok)throw new Error(j.error||'login failed');
|
||||
state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user};
|
||||
saveState();
|
||||
toast('Bem-vindo, '+(j.user.name||j.user.email));
|
||||
welcomeFinish();
|
||||
if(typeof renderAuthBox==='function')renderAuthBox();
|
||||
}catch(e){
|
||||
console.warn('[google login]',e);
|
||||
toast('Falha login Google: '+e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ REALTIME SYNC (WebSocket + auto push/pull) ============
|
||||
// Conecta ao /ws do servidor, escuta notificações de mudança de outros devices
|
||||
// e dispara pull/push automáticos. Reconnect com backoff exponencial.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Shivao Service Worker — offline real
|
||||
// Estratégia: shell precachado, tiles cache-first, windy network-first, /api passa direto.
|
||||
// Versão usada nos cache names — bumpa essa string pra invalidar caches antigos em deploys.
|
||||
const VERSION = 'shivao-v1.5.0';
|
||||
const VERSION = 'shivao-v1.6.0';
|
||||
const SHELL_CACHE = `shivao-shell-${VERSION}`;
|
||||
const TILES_CACHE = 'shivao-tiles-v1'; // separado pra não invalidar tiles em update do shell
|
||||
const WINDY_CACHE = `shivao-windy-${VERSION}`;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,48 @@ app.get('/api/auth/me', requireAuth, (req, res) => {
|
|||
res.json(req.user);
|
||||
});
|
||||
|
||||
// Login com Google (Sign-In) — recebe ID token, valida no Google, cria/loga user
|
||||
app.post('/api/auth/google', async (req, res) => {
|
||||
const { credential } = req.body || {};
|
||||
if (!credential) return res.status(400).json({ error: 'credential (Google ID token) required' });
|
||||
try {
|
||||
// Valida o ID token via tokeninfo endpoint do Google (sem dependência adicional)
|
||||
const r = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(credential)}`);
|
||||
if (!r.ok) return res.status(401).json({ error: 'invalid_google_token' });
|
||||
const info = await r.json();
|
||||
// Confere que aud = nosso CLIENT_ID
|
||||
const expectedAud = process.env.GOOGLE_CLIENT_ID || '';
|
||||
if (expectedAud && info.aud !== expectedAud) {
|
||||
return res.status(401).json({ error: 'token_audience_mismatch' });
|
||||
}
|
||||
if (!info.email_verified || info.email_verified === 'false') {
|
||||
return res.status(403).json({ error: 'email_not_verified' });
|
||||
}
|
||||
const email = info.email;
|
||||
const name = info.name || email.split('@')[0];
|
||||
let user = db.findUserByEmail(email);
|
||||
if (!user) {
|
||||
// Auto-cria com senha aleatória inutilizável (login só via Google daqui pra frente)
|
||||
const randomPwd = 'google-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
||||
const hash = await hashPassword(randomPwd);
|
||||
const id = db.createUser(email, hash, name);
|
||||
db.audit(id, 'user_signup_google', 'user', String(id), { email }, req.ip);
|
||||
user = db.findUserById(id);
|
||||
}
|
||||
db.updateLastLogin(user.id);
|
||||
db.audit(user.id, 'user_login_google', 'user', String(user.id), {}, req.ip);
|
||||
const safe = db.findUserById(user.id);
|
||||
res.json({
|
||||
user: safe,
|
||||
accessToken: signAccessToken(safe),
|
||||
refreshToken: signRefreshToken(safe),
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[auth/google]', e.message);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Plans + license info
|
||||
app.get('/api/license', requireAuth, (req, res) => {
|
||||
const lic = db.getActiveLicense(req.user.id) || { plan: 'free', status: 'active', expires_at: null };
|
||||
|
|
@ -267,7 +309,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
|
|||
});
|
||||
|
||||
// Atalho: /apk redireciona pra última APK release no Forgejo
|
||||
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.5.0/Shivao-v1.5.0.apk';
|
||||
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.6.0/Shivao-v1.6.0.apk';
|
||||
app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL));
|
||||
|
||||
// Página A4 imprimível com QR Code + instruções (cola no barco/marina)
|
||||
|
|
|
|||
Loading…
Reference in a new issue