# To AI Agent: Implement Offline-First Architecture for Mobile App & Web Browsers (Offline Photo Upload & Offline Map Routing) ## 1. Context & Architectural Strategy We are implementing an **Offline-First Capabilities Layer** for both browser environments (Chrome Android, iOS Safari) and mobile application wrappers. The application must remain functional when network connectivity is lost ($Network = 0$). ### Core Requirements: 1. **Offline Photo Upload Queue:** When a user uploads a photo without an active internet connection, the system must not throw a network exception. Instead, it must store the image file (Blob/ArrayBuffer) and its accompanying metadata (GPS, Timestamp) into a browser-native transactional database (**IndexedDB**). 2. **Background Sync Resume:** As soon as the device regains cellular/Wi-Fi data telemetry, a background synchronization worker must automatically trigger, reading the IndexedDB queue and completing the multi-part upload streams to the Debian server sequentially. 3. **Offline Map Navigation Routing:** Cache critical operational Map Layout Tiles and routing geometries via a persistent client-side caching framework (**Service Workers + Cache API**). --- ## 2. Technical Offline System Stack Overview [Offline User Interaction] │ ├──➔ (Photo Upload) ──➔ Save Blob & EXIF Metadata ──➔ [ IndexedDB Storage ] │ │ (Device re-connects) │ ▼ │ [ Background Sync ] ──➔ POST to Server │ └──➔ (Map Routing) ──➔ Request Map Assets ──➔ [ Service Worker Cache API ] ──➔ Render View ## 3. Code Refactoring Blueprint ### Step 1: Implement Offline Storage Queue Service (`offlineQueue.ts`) Create a localized database management layer at `frontend/src/utils/offlineQueue.ts` leveraging IndexedDB to hold pending image uploads: ```typescript import { openDB, IDBPDatabase } from 'idb'; const DB_NAME = 'yotrip-offline-db'; const STORE_NAME = 'pending-uploads'; interface OfflinePhoto { id: string; fileBlob: Blob; fileName: string; latitude: string | null; longitude: string | null; capturedAt: string | null; timestamp: number; } // Initialize the local IndexedDB container safely const getDB = (): Promise => { return openDB(DB_NAME, 1, { upgrade(db) { if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id' }); } }, }); }; // 1. STASH UPLOAD METADATA INTO OFFLINE STORAGE export const queueOfflineUpload = async (photoData: Omit) => { const db = await getDB(); const id = crypto.randomUUID(); const item: OfflinePhoto = { ...photoData, id, timestamp: Date.now(), }; await db.put(STORE_NAME, item); console.log(`📦 Photo [${id}] stashed safely into local IndexedDB queue for offline background sync.`); return id; }; // 2. RETRIEVE ALL PENDING FILES ONCE ONLINE export const getPendingUploads = async (): Promise => { const db = await getDB(); return db.getAll(STORE_NAME); }; // 3. REMOVE COMPLETED TRANSACTION FROM QUEUE export const removePendingUpload = async (id: string) => { const db = await getDB(); await db.delete(STORE_NAME, id); }; ### Step 2: Implement Background Synchronization Sync Engine (backgroundSync.ts) Create an active network monitoring service that intercepts reconnection signals and synchronizes the local database records: import { getPendingUploads, removePendingUpload } from './offlineQueue'; import { api } from '../services/api'; export const executeBackgroundSyncEngine = async (refreshMapPins: () => void) => { const pendingItems = await getPendingUploads(); if (pendingItems.length === 0) return; console.log(`🔄 Internet restored! Syncing [${pendingItems.length}] pending items to yotrip.labz.io.vn...`); for (const item of pendingItems) { const formData = new FormData(); formData.append('photo', item.fileBlob, item.fileName); if (item.latitude && item.longitude) { formData.append('latitude', item.latitude); formData.append('longitude', item.longitude); } if (item.capturedAt) { formData.append('capturedAt', item.capturedAt); } try { // Dispatch payload to Debian production server endpoint const response = await api.post('/photos/upload-with-meta', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); if (response.status === 200 || response.status === 201) { // Remove item from local DB once server confirms safe receipt await removePendingUpload(item.id); console.log(`✅ Offline photo [${item.id}] synchronized successfully.`); } } catch (error) { console.error(`✖ Failed to synchronize item [${item.id}]. Will retry on next connectivity cycle:`, error); break; // Stop loop if server goes down mid-transit to protect execution queues } } // Trigger reactive map layer update refreshMapPins(); }; // Global Connectivity Listener initialization hook export const initNetworkStatusListeners = (refreshMapPins: () => void) => { window.addEventListener('online', () => executeBackgroundSyncEngine(refreshMapPins)); // Guard check on application startup in case network recovered while app was closed if (navigator.onLine) { executeBackgroundSyncEngine(refreshMapPins); } }; ### Step 3: Configure Service Worker for Offline Map Tiles Cache (sw.js) Configure your service worker script block (e.g., public/sw.js) to intercept and cache vector navigation maps or stylesheet assets statically: const CACHE_NAME = 'yotrip-static-map-v1'; const MAP_TILE_PATTERN = /tiles\.maps\.lincoln|openstreetmap|mapbox/; self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll([ '/', '/index.html', '/src/main.tsx', '/manifest.json', '/assets/offline-map-placeholder.png' // Fallback image asset ]); }) ); }); // NETWORK-FIRST FALLBACK TO CACHE STRATEGY FOR OFFLINE ROUTING MAPS self.addEventListener('fetch', (event) => { const requestUrl = event.request.url; if (MAP_TILE_PATTERN.test(requestUrl)) { event.respondWith( fetch(event.request) .then((response) => { // Clone and update cache with newly acquired dynamic tile data layers const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseClone); }); return response; }) .catch(() => { // If offline, serve map tiles seamlessly straight from client-side Cache API return caches.match(event.request); }) ); } }); ## 4. Quality Verification & Acceptance Criteria for AI Agent[ ] Airplane Mode Upload Stability: Toggle Airplane mode ($Network = 0$) inside the Android Emulator/iOS Safari responsive inspector. Upload an image. The UI must transition seamlessly, displaying an "Ảnh đã được lưu tạm ngoại tuyến" notification banner without runtime exceptions.[ ] Automatic Queue Flushing Check: Disable Airplane mode. Verify through the browser network inspector tool that a series of asynchronous multi-part POST network requests fire automatically towards https://yotrip.labz.io.vn/api/ without requiring user interaction.[ ] Offline Map Cache Test: Clear your browser network connectivity states. Navigate around the map interface canvas. Previously inspected map sections and routing data blocks must remain fully rendered from the Service Worker cache layer.