App technicien : appairage 1x (QR), géorepérage natif en arrière-plan (app fermée) -> checkpoints au hub /field/ts, scan série/MAC on-device MLKit. UI réutilisée depuis le hub (/field). APK Android buildé (debug, 19 Mo, arm64-v8a + armeabi-v7a). - apps/field-tech : Capacitor 6 + Vite ; src/main.js (appairage @capacitor/preferences + BackgroundGeolocation.addGeofences + redirection /field) ; projet android/ avec les 4 correctifs Gradle commités (force work-runtime 2.9.1, minSdk 24, repos maven xms.g + Huawei, googlePlayServicesLocationVersion 21.0.1) ; abiFilters arm (APK 32->19 Mo). - README : recette de build complète (toolchain M4, les 4 fixes, install/appairage) ; BUILD-ANDROID.md (Docker + Android Studio + release signée) ; CI-SETUP.md (runner Gitea). - CI Gitea Actions (.gitea/workflows) : android (runner Linux HORS prod, cache Gradle/npm -> artefact APK) + ios (runner macOS, workflow_dispatch, en attente compte Apple). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
3.4 KiB
JavaScript
59 lines
3.4 KiB
JavaScript
// Targo Tech — app native (Capacitor + Vite).
|
||
// Appairage 1× (QR → token tech persistant) → géorepérage natif Transistorsoft (arrière-plan, app fermée)
|
||
// → POST auto des événements geofence au hub (/field/ts) → UI réutilisée depuis le hub (/field).
|
||
import BackgroundGeolocation from '@transistorsoft/capacitor-background-geolocation'
|
||
import { BarcodeScanner } from '@capacitor-mlkit/barcode-scanning'
|
||
import { Preferences } from '@capacitor/preferences'
|
||
|
||
const HUB = 'https://msg.gigafibre.ca'
|
||
const $ = (id) => document.getElementById(id)
|
||
const getToken = async () => (await Preferences.get({ key: 'tech_token' })).value
|
||
const setToken = async (t) => Preferences.set({ key: 'tech_token', value: t })
|
||
function tokenFromUrl (u) { try { return new URL(u).searchParams.get('t') } catch (e) { const m = /[?&]t=([^&\s]+)/.exec(u || ''); return m ? decodeURIComponent(m[1]) : (u && u.includes('.') ? u : null) } }
|
||
|
||
window.pairScan = async () => {
|
||
try {
|
||
const { barcodes } = await BarcodeScanner.scan()
|
||
const v = barcodes && barcodes[0] && (barcodes[0].rawValue || barcodes[0].displayValue)
|
||
const t = v && tokenFromUrl(v)
|
||
if (t) { await setToken(t); boot() } else $('msg').textContent = 'QR non reconnu'
|
||
} catch (e) { $('msg').textContent = 'Scan annulé' }
|
||
}
|
||
window.pairPaste = async () => { const t = tokenFromUrl($('lnk').value.trim()); if (t) { await setToken(t); boot() } else $('msg').textContent = 'Lien invalide' }
|
||
window.unpair = async () => { await Preferences.remove({ key: 'tech_token' }); location.reload() }
|
||
|
||
// Géorepérage natif : enregistre les jobs du jour comme geofences ; Transistorsoft POSTe enter/exit au hub (autoSync),
|
||
// même app fermée. L'identifier de chaque geofence = le token signé du job → le hub mappe + écrit le checkpoint.
|
||
async function initGeo (token) {
|
||
await BackgroundGeolocation.ready({
|
||
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
|
||
distanceFilter: 50,
|
||
stopOnTerminate: false,
|
||
startOnBoot: true,
|
||
geofenceModeHighAccuracy: true,
|
||
locationAuthorizationRequest: 'Always',
|
||
backgroundPermissionRationale: { title: 'Suivi des interventions', message: 'Targo Tech a besoin de la localisation « toujours » pour détecter automatiquement vos arrivées/départs.' },
|
||
url: HUB + '/field/ts',
|
||
autoSync: true,
|
||
batchSync: false,
|
||
notification: { title: 'Targo Tech', text: 'Suivi des interventions actif' },
|
||
})
|
||
try {
|
||
const jobs = await fetch(HUB + '/field/tech?t=' + encodeURIComponent(token)).then(r => r.json()).then(j => j.jobs || [])
|
||
await BackgroundGeolocation.removeGeofences()
|
||
const fences = jobs.filter(j => j.lat && Math.abs(+j.lat) > 0.01).map(j => ({ identifier: j.token, latitude: +j.lat, longitude: +j.lon, radius: 200, notifyOnEntry: true, notifyOnExit: true }))
|
||
if (fences.length) await BackgroundGeolocation.addGeofences(fences)
|
||
} catch (e) { console.warn('geofences', e) }
|
||
const st = await BackgroundGeolocation.getState()
|
||
if (!st.enabled) await BackgroundGeolocation.start()
|
||
}
|
||
|
||
async function boot () {
|
||
const token = await getToken()
|
||
if (!token) { $('splash').style.display = 'none'; $('pair').style.display = 'block'; return }
|
||
try { await initGeo(token) } catch (e) { console.warn('initGeo', e) }
|
||
// Charge l'UI complète hébergée (liste/détail/carte/photo/scan IA + MLKit injecté par Capacitor)
|
||
location.replace(HUB + '/field?t=' + encodeURIComponent(token))
|
||
}
|
||
boot()
|