> Part of the PWA DevGuide (split from pwa_dev_style.md on 2026-07-02 — see that file for the index; ADR-099).

## 3. State Architecture

### 3.1 Global Variables

```js
let F = {}               // Form data accumulator — all step fields merged here
let frontPhotos  = []    // [{blob, prev, cmt, src}]
let insidePhotos = []    // [{blob, prev, cmt, src}]
let addlPhotos   = []    // [{blob, prev, desc, src}]
let brandings    = []    // [{type, w, h, qty, quality, blob, prev, fromCamera}]
let videoBlob    = null  // File object for video
let submissionDoc = null // Blob — mandatory document
let gpsCoords    = null  // {lat, lng, acc} — set on new recce start + manual capture
let stores       = []    // Loaded from IndexedDB / server — all store list entries
let selectedStore = null // Currently selected store object from picker
let manualMode   = false // true when user enters store manually
let DB                   // IndexedDB instance (RecceDB v4)
let imgQuality   = 'compressed'  // 'compressed' | 'hd'
let geoStampEnabled = true       // GPS stamp on camera photos
let brands = ['LENOVO','HP','PHILIPS','LIEBHERR']  // Editable brand list
let _storeFiles  = []    // Tracked uploaded files [{name, count, brand}]
let _pendingParse = null // Parsed store rows waiting brand assignment
let _adminUnlocked = false  // true for hub empIds: harish, pramod, rakesh
let _adminRows   = []    // Server rows for admin view
let _myServerRows = []   // Server rows for current field agent
```

### 3.2 Constants

```js
const BTYPE = [
  'Glow Sign Board','Inshop Branding','ACP Board','One Way Vision',
  'Fabric Board','Lollypop','Non Lit Flex','Double Side GSB',
  'Clip on Board','Translit','Vinyl Printing',
  'Roll up Standee','Sunboard Standee'              // added 2026-05-05
]
const CACHE_VER = 'recce-v6'          // bump with every release (triggers update banner)
const _RECCE_ADMIN_IDS = ['harish','pramod','rakesh']
const DEFAULT_GAS_URL = 'https://script.google.com/...'   // Sheet sync GAS
const DEFAULT_STORES_URL = 'https://srv1111289.hstgr.cloud/recce/stores.json'
const DEFAULT_ADMIN_HASH = '42d372e...'   // SHA-256 of default admin password
const _RC_BASE = '/db'                    // PostgREST base
const _RC_READ   = { 'Accept-Profile': 'recce' }
const _RC_WRITE  = { 'Content-Type': 'application/json', 'Content-Profile': 'recce', 'Accept-Profile': 'recce' }
const _RC_UPSERT = { ..._RC_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' }
```

---

## 4. Database (IndexedDB)

**DB name:** `RecceDB` **version:** 4

| Store | Key | Contents |
|---|---|---|
| `subs` | `id` | Submitted recce entries (full data including base64 photos) |
| `cfg` | `k` | App config key-value pairs |
| `draft` | `k` | Active draft + photos (keys: `current`, `photos`) |

### 4.1 Config Keys (cfg store)

| Key | Type | Description |
|---|---|---|
| `stores` | JSON string | Cached store list array |
| `store-files` | JSON string | Uploaded file tracking array |
| `srv-url` | string | Server URL for auto-fetching stores.json |
| `gas-url` | string | Google Apps Script URL (Sheet sync — legacy, fire-and-forget) |
| `slides-gas-url` | string | Google Apps Script URL (Slides generator — current, awaited via proxy) |
| `img-quality` | string | `'compressed'` or `'hd'` |
| `geo-stamp` | string | `'1'` = enabled, `'0'` = disabled (null = enabled by default) |
| `brands` | JSON string | Brand list array |
| `admin-cfg-url` | string | URL to remote admin.json (password/hash) |
| `admin-hash` | string | Local SHA-256 admin password hash |
| `last-sync` | ISO string | Timestamp of last successful sync |

### 4.2 Submission Entry Schema

```js
{
  id:             'RC_<timestamp>_<4-char-random>',  // 'RC_1714900000000_AB3C'
  timestamp:      ISO string,     // Created at (immutable)
  editedAt:       ISO string,     // Set only on edits
  status:         'pending' | 'synced' | 'failed',
  gasAt:          ISO string,     // When GAS was fired (set during sync)
  syncedAt:       ISO string,     // When Postgres confirmed sync
  viewUrl:        string | null,  // Server-hosted HTML report URL
  slideUrl:       string | null,  // Google Slides URL (if generated)
  submittedBy:    empId,          // From hub session
  submittedByName: name,
  data: {
    date:         'YYYY-MM-DD',
    storeName:    string,
    brand:        string,
    address:      string,
    mobile:       string,
    city:         string,
    gpsCoords:    {lat, lng, acc} | null,
    hasVideo:     boolean,
    videoNote:    string | null,  // e.g. 'Video local (2.1MB) — share via WhatsApp'
    videoCmt:     string,
    hasBranding:  boolean,
    frontPhotos:  [{b64, cmt}],
    insidePhotos: [{b64, cmt}],
    brandings:    [{type, w, h, qty, sqft, photoB64}],
    addlPhotos:   [{b64, desc}],
    docB64:       string,         // Mandatory document as base64
  }
}
```

### 4.3 Draft Schema

```js
// key: 'current'
{
  k: 'current', ts: timestamp,
  F: { date, brand, storeName, address, mobile, hasVideo, hasBranding, editingId? },
  selectedStore: object | null,
  manualMode: boolean,
  gpsCoords: {lat, lng, acc} | null,
  fCnt: number,  // frontPhotos count (for resume banner)
  iCnt: number,  // insidePhotos count
  bCnt: number,  // brandings count
}

// key: 'photos'
{
  k: 'photos',
  front:  [{b64, cmt}],
  inside: [{b64, cmt}],
  addl:   [{b64, desc}],
  brand:  [{type, w, h, qty, b64}],
}
```

---

