diff --git a/app/diario-bordo.html b/app/diario-bordo.html
index ffa4a2a..80906e1 100644
--- a/app/diario-bordo.html
+++ b/app/diario-bordo.html
@@ -3141,8 +3141,51 @@ async function testWindyKey(){
}
}
-// ===== Service Worker (offline real) =====
+// ===== Capacitor adapter (detecta nativo + usa APIs nativas com fallback Web) =====
+const isNative=()=>!!(window.Capacitor&&window.Capacitor.isNativePlatform&&window.Capacitor.isNativePlatform());
+const nativePlatform=()=>(window.Capacitor&&window.Capacitor.getPlatform)?window.Capacitor.getPlatform():'web';
+
+// Geolocation: Capacitor.Geolocation (background-capable) > navigator.geolocation
+async function nativeWatchPosition(onUpdate,onError,opts){
+ if(isNative()&&window.Capacitor.Plugins.Geolocation){
+ try{
+ const{Geolocation}=window.Capacitor.Plugins;
+ // Pede permission upfront
+ const perm=await Geolocation.requestPermissions({permissions:['location']}).catch(()=>({location:'denied'}));
+ if(perm.location==='denied'){onError({code:1,PERMISSION_DENIED:1,message:'Permissão negada'});return null}
+ const id=await Geolocation.watchPosition({enableHighAccuracy:!!opts?.enableHighAccuracy,timeout:opts?.timeout||15000,maximumAge:opts?.maximumAge||0},(pos,err)=>{
+ if(err){onError({code:err.code||3,message:err.message||'GPS erro',PERMISSION_DENIED:1,POSITION_UNAVAILABLE:2,TIMEOUT:3});return}
+ onUpdate({coords:{latitude:pos.coords.latitude,longitude:pos.coords.longitude,accuracy:pos.coords.accuracy,speed:pos.coords.speed},timestamp:pos.timestamp});
+ });
+ return{native:true,id};
+ }catch(e){console.warn('[native gps] fallback to web:',e.message)}
+ }
+ // Fallback web
+ const id=navigator.geolocation.watchPosition(onUpdate,onError,opts);
+ return{native:false,id};
+}
+async function nativeClearWatch(handle){
+ if(!handle)return;
+ if(handle.native&&isNative()){try{await window.Capacitor.Plugins.Geolocation.clearWatch({id:handle.id})}catch(e){}}
+ else if(handle.id!=null)navigator.geolocation.clearWatch(handle.id);
+}
+
+// Local notifications: nativo no Android, fallback toast no web
+async function nativeNotify(title,body,id){
+ if(isNative()&&window.Capacitor.Plugins.LocalNotifications){
+ try{
+ const{LocalNotifications}=window.Capacitor.Plugins;
+ await LocalNotifications.requestPermissions().catch(()=>{});
+ await LocalNotifications.schedule({notifications:[{id:id||Date.now()%2147483647,title,body,sound:null,smallIcon:'ic_stat_icon_config_sample'}]});
+ return;
+ }catch(e){console.warn('[native notify]',e.message)}
+ }
+ toast(title+(body?': '+body:''));
+}
+
+// ===== Service Worker (offline real — só registra no Web, no Capacitor o WebView usa cache nativo) =====
function initServiceWorker(){
+ if(isNative())return; // Capacitor não usa SW (tem cache próprio + APIs nativas)
if(!('serviceWorker' in navigator))return;
navigator.serviceWorker.register('/sw.js').then(reg=>{
console.log('[SW] registered:',reg.scope);
diff --git a/mobile/.gitignore b/mobile/.gitignore
new file mode 100644
index 0000000..db88c09
--- /dev/null
+++ b/mobile/.gitignore
@@ -0,0 +1,19 @@
+# Capacitor
+node_modules/
+www/
+android/.gradle/
+android/local.properties
+android/app/release/
+android/app/build/
+android/build/
+android/.idea/
+android/captures/
+android/*.iml
+ios/App/build/
+ios/App/Pods/
+ios/App/output/
+
+# Keystore (CRÍTICO — nunca commitar chaves privadas)
+*.keystore
+*.jks
+keystore-passwords.txt
diff --git a/mobile/README.md b/mobile/README.md
new file mode 100644
index 0000000..1373209
--- /dev/null
+++ b/mobile/README.md
@@ -0,0 +1,213 @@
+# Shivao Mobile · Capacitor Android (e iOS futuro)
+
+Wrapper Capacitor pra empacotar o Shivao web como app nativo Android (.apk pra sideload, .aab pra Play Store) e iOS no futuro.
+
+**Arquitetura:** o frontend HTML mora em `app/diario-bordo.html` (1 fonte de verdade). Um script (`scripts/sync-html.mjs`) copia pra `server/public/index.html` (Express serve) e `mobile/www/index.html` (Capacitor empacota).
+
+---
+
+## 🛠️ Pré-requisitos (instalar 1 vez)
+
+### Windows (recomendado pra você)
+
+1. **JDK 17+** — Adoptium Temurin é a melhor escolha:
+ ```
+ winget install EclipseAdoptium.Temurin.17.JDK
+ ```
+ Verificar: `java -version` deve mostrar `17.x`
+
+2. **Android Studio** — instala SDK + emulador + tudo:
+ ```
+ winget install Google.AndroidStudio
+ ```
+ Após abrir, ir em **Tools → SDK Manager** e instalar:
+ - Android SDK Platform 34 (Android 14)
+ - Android SDK Build-Tools 34.0.0
+ - Android SDK Command-line Tools (latest)
+ - Android SDK Platform-Tools
+
+3. **Variáveis de ambiente** (Painel de Controle → Sistema → Variáveis de Ambiente):
+ - `JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-17.0.x` (ajuste path)
+ - `ANDROID_HOME=C:\Users\pontu\AppData\Local\Android\Sdk`
+ - Adicionar ao PATH: `%JAVA_HOME%\bin`, `%ANDROID_HOME%\platform-tools`
+
+4. Reiniciar terminal. Verificar:
+ ```bash
+ java -version
+ adb --version
+ ```
+
+**Tempo total de instalação:** ~30-45 min (download de ~3GB).
+
+---
+
+## 🚀 Build inicial (faz 1 vez)
+
+```bash
+cd mobile
+
+# Instala deps Capacitor + plugins
+npm install
+
+# Sincroniza HTML do app/ pra mobile/www/
+node ../scripts/sync-html.mjs
+
+# Adiciona projeto Android (gera mobile/android/)
+npx cap add android
+
+# Sincroniza plugins nativos no projeto Android
+npx cap sync android
+```
+
+### Permissões Android (editar manualmente após `cap add android`)
+
+Abra `mobile/android/app/src/main/AndroidManifest.xml` e adicione DENTRO do ``:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 🔐 Keystore PERMANENTE (CRÍTICO — gera 1 vez, guarda pra sempre)
+
+```bash
+cd mobile/android/app
+keytool -genkey -v -keystore shivao-release.keystore -alias shivao -keyalg RSA -keysize 2048 -validity 10000 \
+ -dname "CN=Karlao, O=PontualTech, L=Sao Paulo, ST=SP, C=BR"
+# Senha forte (mínimo 8 chars). ANOTE em local seguro — perda = perda permanente do app na Play Store.
+```
+
+**BACKUP IMEDIATO:**
+```bash
+cp shivao-release.keystore C:/Users/pontu/Downloads/Shivao-keystore-backup/shivao-release.keystore
+echo "Senha keystore: SUA_SENHA_AQUI" >> C:/Users/pontu/Downloads/Shivao-keystore-backup/keystore-passwords.txt
+echo "Alias: shivao" >> C:/Users/pontu/Downloads/Shivao-keystore-backup/keystore-passwords.txt
+```
+
+E também faça upload em Drive privado, HD externo etc — perdeu = não publica update na Play Store nunca mais.
+
+### Configurar signing no Gradle
+
+Edite `mobile/android/app/build.gradle`, dentro de `android { ... }`:
+
+```gradle
+signingConfigs {
+ release {
+ storeFile file('shivao-release.keystore')
+ storePassword System.getenv('SHIVAO_KEYSTORE_PWD') ?: 'SUA_SENHA_AQUI'
+ keyAlias 'shivao'
+ keyPassword System.getenv('SHIVAO_KEY_PWD') ?: 'SUA_SENHA_AQUI'
+ }
+}
+buildTypes {
+ release {
+ signingConfig signingConfigs.release
+ minifyEnabled false
+ }
+}
+```
+
+---
+
+## 📦 Build do APK (sideload) e AAB (Play Store)
+
+```bash
+cd mobile
+
+# Sincroniza última versão do HTML antes de build
+npm run sync
+
+# APK assinado pra sideload (~3-5 min primeira vez, ~1 min em rebuilds)
+npm run android:build:apk
+# Output: mobile/android/app/build/outputs/apk/release/app-release.apk
+
+# AAB pra Play Store (formato preferido)
+npm run android:build:aab
+# Output: mobile/android/app/build/outputs/bundle/release/app-release.aab
+```
+
+---
+
+## 🌐 Alternativa: Build na nuvem (sem instalar nada local)
+
+Se não quiser instalar 3GB de Android Studio:
+
+### Opção A: Codemagic (mais simples)
+- 500min grátis/mês, suficiente pra ~10 builds
+- Conecta ao Forgejo, push detecta e roda
+- Configura assinatura no painel
+
+### Opção B: Container Docker no Coolify (mais self-hosted)
+- Subir imagem `mingc/android-build-box` no Coolify
+- Cron job ou webhook do Forgejo dispara build
+- APK/AAB salvo em volume persistente
+
+Ambas opções: vamos preparar quando você decidir.
+
+---
+
+## 🎮 Submeter Play Store
+
+1. **Conta Google Play Developer:** $25 1× em https://play.google.com/console
+2. Após aprovado (~24h), criar novo app:
+ - Nome: "Shivao - Diário de Bordo"
+ - Categoria: "Maps & Navigation"
+ - Idioma: pt-BR
+3. **Subir AAB:** Production → Releases → Create new release → upload `app-release.aab`
+4. **Conteúdo obrigatório:**
+ - Política de privacidade (URL pública — vou criar uma rota `/politica-privacidade` no servidor)
+ - Screenshots: mínimo 2 (vou gerar via Playwright se quiser)
+ - Ícone 512×512 (vou gerar do icon.svg)
+ - Descrição curta (80 chars) e completa
+5. **Content rating:** preencher questionário (~5min)
+6. **Pricing:** Free (cobrança vai por dentro do app via Asaas, não Google IAP — economia de 30%)
+7. **Submit pra review:** Google revisa em 1-7 dias
+
+---
+
+## 📱 iOS (FASE 4 futura)
+
+Requer:
+- Mac (mini M4 ~R$5k mais barato)
+- Apple Developer Program $99/ano
+- Xcode
+
+```bash
+npx cap add ios
+npx cap sync ios
+npx cap open ios # abre Xcode
+# Build Archive → Distribute → App Store Connect
+```
+
+---
+
+## 🐛 Troubleshooting
+
+**`adb: command not found`** — adicione `%ANDROID_HOME%\platform-tools` ao PATH
+
+**`SDK location not found`** — crie `mobile/android/local.properties`:
+```
+sdk.dir=C:\\Users\\pontu\\AppData\\Local\\Android\\Sdk
+```
+
+**Build muito lento** — primeiro build baixa Gradle + dependências (~500MB). Próximos builds usam cache.
+
+**APK muito grande** — habilitar minify em `build.gradle`:
+```gradle
+buildTypes.release.minifyEnabled true
+buildTypes.release.shrinkResources true
+```
diff --git a/mobile/capacitor.config.json b/mobile/capacitor.config.json
new file mode 100644
index 0000000..9ade570
--- /dev/null
+++ b/mobile/capacitor.config.json
@@ -0,0 +1,44 @@
+{
+ "appId": "br.com.pontualtech.shivao",
+ "appName": "Shivao",
+ "webDir": "www",
+ "bundledWebRuntime": false,
+ "android": {
+ "allowMixedContent": false,
+ "captureInput": true,
+ "webContentsDebuggingEnabled": false,
+ "backgroundColor": "#0e2a3dFF"
+ },
+ "ios": {
+ "contentInset": "automatic",
+ "scrollEnabled": true,
+ "backgroundColor": "#0e2a3dFF"
+ },
+ "server": {
+ "androidScheme": "https",
+ "iosScheme": "capacitor",
+ "cleartext": false,
+ "url": "https://shivao.pontualtech.work",
+ "allowNavigation": [
+ "shivao.pontualtech.work",
+ "tile.openstreetmap.org",
+ "*.tile.openstreetmap.org",
+ "api.windy.com",
+ "api.open-meteo.com",
+ "marine-api.open-meteo.com",
+ "fonts.googleapis.com",
+ "fonts.gstatic.com",
+ "cdnjs.cloudflare.com"
+ ]
+ },
+ "plugins": {
+ "Geolocation": {
+ "permissions": ["location"]
+ },
+ "LocalNotifications": {
+ "smallIcon": "ic_stat_icon_config_sample",
+ "iconColor": "#a07832",
+ "sound": "alarme.wav"
+ }
+ }
+}
diff --git a/mobile/package.json b/mobile/package.json
new file mode 100644
index 0000000..6af3aa9
--- /dev/null
+++ b/mobile/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "shivao-mobile",
+ "version": "1.2.0",
+ "description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
+ "main": "index.js",
+ "type": "module",
+ "scripts": {
+ "sync": "node ../scripts/sync-html.mjs && npx cap sync",
+ "android:open": "npx cap open android",
+ "android:build:apk": "cd android && ./gradlew assembleRelease",
+ "android:build:aab": "cd android && ./gradlew bundleRelease",
+ "ios:open": "npx cap open ios"
+ },
+ "dependencies": {
+ "@capacitor/android": "^6.1.2",
+ "@capacitor/app": "^6.0.1",
+ "@capacitor/core": "^6.1.2",
+ "@capacitor/geolocation": "^6.0.1",
+ "@capacitor/local-notifications": "^6.1.0",
+ "@capacitor/network": "^6.0.2",
+ "@capacitor/preferences": "^6.0.2",
+ "@capacitor/status-bar": "^6.0.1"
+ },
+ "devDependencies": {
+ "@capacitor/cli": "^6.1.2"
+ }
+}
diff --git a/scripts/sync-html.mjs b/scripts/sync-html.mjs
new file mode 100644
index 0000000..fef6d95
--- /dev/null
+++ b/scripts/sync-html.mjs
@@ -0,0 +1,25 @@
+// Sync único do HTML do Shivão pra todos os destinos.
+// Fonte de verdade: app/diario-bordo.html
+// Destinos: server/public/index.html (Express) + mobile/www/index.html (Capacitor)
+import { copyFile, mkdir } from 'node:fs/promises';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const root = resolve(__dirname, '..');
+
+const src = resolve(root, 'app/diario-bordo.html');
+const targets = [
+ resolve(root, 'server/public/index.html'),
+ resolve(root, 'mobile/www/index.html'),
+];
+
+async function sync() {
+ for (const t of targets) {
+ await mkdir(dirname(t), { recursive: true });
+ await copyFile(src, t);
+ console.log(`✓ ${src} → ${t}`);
+ }
+}
+
+sync().catch(e => { console.error(e); process.exit(1); });
diff --git a/server/public/index.html b/server/public/index.html
index 4c9f482..9469b2a 100644
--- a/server/public/index.html
+++ b/server/public/index.html
@@ -3118,8 +3118,47 @@ async function testWindyKey(){
}
}
-// ===== Service Worker (offline real) =====
+// ===== Capacitor adapter =====
+const isNative=()=>!!(window.Capacitor&&window.Capacitor.isNativePlatform&&window.Capacitor.isNativePlatform());
+const nativePlatform=()=>(window.Capacitor&&window.Capacitor.getPlatform)?window.Capacitor.getPlatform():'web';
+
+async function nativeWatchPosition(onUpdate,onError,opts){
+ if(isNative()&&window.Capacitor.Plugins.Geolocation){
+ try{
+ const{Geolocation}=window.Capacitor.Plugins;
+ const perm=await Geolocation.requestPermissions({permissions:['location']}).catch(()=>({location:'denied'}));
+ if(perm.location==='denied'){onError({code:1,PERMISSION_DENIED:1,message:'Permissão negada'});return null}
+ const id=await Geolocation.watchPosition({enableHighAccuracy:!!opts?.enableHighAccuracy,timeout:opts?.timeout||15000,maximumAge:opts?.maximumAge||0},(pos,err)=>{
+ if(err){onError({code:err.code||3,message:err.message||'GPS erro',PERMISSION_DENIED:1,POSITION_UNAVAILABLE:2,TIMEOUT:3});return}
+ onUpdate({coords:{latitude:pos.coords.latitude,longitude:pos.coords.longitude,accuracy:pos.coords.accuracy,speed:pos.coords.speed},timestamp:pos.timestamp});
+ });
+ return{native:true,id};
+ }catch(e){console.warn('[native gps] fallback to web:',e.message)}
+ }
+ const id=navigator.geolocation.watchPosition(onUpdate,onError,opts);
+ return{native:false,id};
+}
+async function nativeClearWatch(handle){
+ if(!handle)return;
+ if(handle.native&&isNative()){try{await window.Capacitor.Plugins.Geolocation.clearWatch({id:handle.id})}catch(e){}}
+ else if(handle.id!=null)navigator.geolocation.clearWatch(handle.id);
+}
+
+async function nativeNotify(title,body,id){
+ if(isNative()&&window.Capacitor.Plugins.LocalNotifications){
+ try{
+ const{LocalNotifications}=window.Capacitor.Plugins;
+ await LocalNotifications.requestPermissions().catch(()=>{});
+ await LocalNotifications.schedule({notifications:[{id:id||Date.now()%2147483647,title,body,sound:null,smallIcon:'ic_stat_icon_config_sample'}]});
+ return;
+ }catch(e){console.warn('[native notify]',e.message)}
+ }
+ toast(title+(body?': '+body:''));
+}
+
+// ===== Service Worker (apenas Web — Capacitor usa cache nativo) =====
function initServiceWorker(){
+ if(isNative())return;
if(!('serviceWorker' in navigator))return;
navigator.serviceWorker.register('/sw.js').then(reg=>{
console.log('[SW] registered:',reg.scope);