Offline & IndexedDB Sync Reference
Overview
The OpenClaw Dashboard includes a client-side offline layer built on IndexedDB. It enables the dashboard to function without network connectivity, queuing mutations locally and replaying them when connectivity is restored.
Architecture
The offline system is composed of five modules organized by responsibility:
┌──────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌──────────────┐ ┌───────────────────────┐ │
│ │ StateManager │───▶│ SyncManager │ │
│ │ (CRUD + UI) │ │ (queue + replay) │ │
│ └──────┬───────┘ └─────────┬─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌───────────────────────┐ │
│ │ IDBWrapper │◀───│ OfflineUIManager │ │
│ │ (storage) │ │ (status indicators) │ │
│ └──────────────┘ └───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ CryptoUtils │ (AES-GCM, optional) │
│ └──────────────┘ │
│ │
└───────────────────────┬──────────────────────────┘
│ fetch()
▼
┌────────────────┐
│ Server API │
│ /api/tasks │
└────────────────┘
| Module | File | Responsibility |
|---|---|---|
| IDBWrapper | src/offline/idb.mjs |
IndexedDB connection, schema, CRUD operations, optional encryption |
| StateManager | src/offline/state-manager.mjs |
Application state, task CRUD, persistence to IndexedDB + localStorage fallback |
| SyncManager | src/offline/sync-manager.mjs |
Operation queue, server replay, conflict resolution, periodic sync |
| OfflineUIManager | src/offline/offline-ui.mjs |
Online/offline badge, sync state indicator, error banners, conflict modals |
| Security | src/offline/utils/security.mjs |
XSS prevention, input sanitization, validation helpers |
IndexedDB Schema
Database: OpenClawDashboardDB · Version: 3
Object Stores
tasks
Stores the serialized dashboard state under the key dashboard_state, plus individual task objects for backward compatibility.
| Key Path | Type | Description |
|---|---|---|
id |
string |
Primary key ("dashboard_state" for the full state object, or a task UUID) |
Indexes:
| Index | Key Path | Unique |
|---|---|---|
category |
category |
No |
completed |
completed |
No |
updatedAt |
updatedAt |
No |
syncQueue
Stores pending mutations to be replayed when connectivity is restored.
| Key Path | Type | Description |
|---|---|---|
id |
string (auto-increment) |
Unique queue item ID ({timestamp}-{random}) |
Indexes:
| Index | Key Path | Unique |
|---|---|---|
timestamp |
timestamp |
No |
operation |
operation |
No |
apiCache
Caches API response payloads keyed by URL.
| Key Path | Type | Description |
|---|---|---|
url |
string |
Request URL (primary key) |
Indexes:
| Index | Key Path | Unique |
|---|---|---|
timestamp |
timestamp |
No |
State Schema
The dashboard state stored under key dashboard_state in the tasks object store:
{
version: 3,
theme: 'dark',
filter: 'all',
search: '',
categoryFilter: 'all',
sort: 'newest',
view: 'list',
agentViewAgent: null,
project_id: null,
categories: ['General', ...], // auto-collected from tasks
tasks: [], // normalized task objects
lastSyncTime: null, // ISO timestamp
savedViews: [], // [{id, project_id, name, filters, sort, ...}]
activeSavedViewId: null
}
Task Object Schema
Tasks are normalized to a consistent shape regardless of source (legacy localStorage, IndexedDB, or Asana server format):
{
id: string, // UUID v4
text: string, // display text
title: string, // Asana-compatible alias
description: '',
category: 'General',
labels: [],
completed: false,
status: 'backlog', // backlog | in_progress | review | completed | archived
priority: 'medium', // low | medium | high | critical
owner: null,
project_id: null,
parent_task_id: null,
dependency_ids: [],
labels: [],
start_date: null,
due_date: null,
estimated_effort: null,
actual_effort: null,
completed_at: null,
recurrence_rule: null, // daily | weekly | monthly | yearly
metadata: {},
execution_lock: false,
execution_locked_by: null,
archived_at: null,
deleted_at: null,
archived: false,
deleted: false,
createdAt: 'ISO-8601',
updatedAt: null | 'ISO-8601'
}
Data Flow
Initialization
StateManager.init()opens IndexedDB.- Loads state from
tasksstore (keydashboard_state). - Falls back to
localStorageif no IndexedDB state exists, then migrates. - Falls back to legacy
projectTaskslocalStorage key, then migrates. - If nothing found, creates default state.
- If online, initializes
SyncManager.
Write Path
- State change via
addTask(),toggleTask(),updateTask(),deleteTask(), etc. - State is mutated in-memory.
debouncedSave()batches writes (1-second debounce).- Written to IndexedDB (
tasksstore) and localStorage (backup). - If online, operation is queued in
syncQueueviaSyncManager.queueOperation().
Read Path
getState()reads from IndexedDB first.- Falls back to localStorage if IndexedDB read fails.
- Returns default state if both fail.
Sync Path
SyncManager.queueOperation()adds an item tosyncQueuein IndexedDB.- If online and not already syncing,
processQueue()starts immediately. - Items are processed in timestamp order (oldest first).
- Each item is sent as an HTTP request to
/api/tasks(or/api/tasks/:id). - On success, the queue item is deleted.
- On failure, retry with exponential backoff (up to 5 retries, max 300s delay).
- On HTTP 409, the item is marked as a conflict.
Action Queue
Each queue item has the following shape:
{
id: '1714320000000-a1b2c3d4e',
operation: 'create', // create | update | delete | ARCHIVE | RESTORE
taskId: 'uuid-or-null', // task ID for update/delete; null for create
data: { /* task fields */ },
timestamp: 1714320000000,
status: 'pending', // pending | syncing | synced | conflict | error
retries: 0,
retryAt: null // ISO timestamp for next retry attempt
}
Operation Mapping
| Operation | HTTP Method | URL | Body |
|---|---|---|---|
create |
POST | /api/tasks |
Task data |
update |
PATCH | /api/tasks/:id |
Changed fields |
delete |
DELETE | /api/tasks/:id |
— |
ARCHIVE |
POST | /api/tasks/:id/archive |
— |
RESTORE |
POST | /api/tasks/:id/restore |
— |
Retry Backoff
delay = min(1000 * 2^retries + random(0, 1000), 300_000)
| Retry | Approximate Delay |
|---|---|
| 0 | 1–2 s |
| 1 | 2–3 s |
| 2 | 4–5 s |
| 3 | 8–9 s |
| 4 | 16–17 s |
| 5+ | Marked as failed, removed from queue |
Conflict Resolution
When the server returns HTTP 409 (conflict):
- The queue item is marked with status
conflict. SyncManageremits aconflictDetectedevent.OfflineUIManagerauto-resolves using the client-wins strategy by default (configurable):- Fetches current server state.
- Replaces local data with server data.
- Removes the conflicting queue item.
- Alternative strategies are available:
client-wins(force push) andmerge(timestamp-based).
Merge Strategy
Compares updatedAt timestamps on client and server. The more recent version wins. If timestamps are equal, server wins.
Periodic Sync
- Every 30 seconds,
SyncManagerchecks thesyncQueuecount. - If items are pending and the client is online,
processQueue()runs. - On
window.onlineevent, sync triggers immediately. SyncManager.syncAll()fetches all tasks from the server (full refresh).
Offline UI Behavior
Status Badge
A fixed-position pill badge in the bottom-right corner:
| State | Color | Text |
|---|---|---|
| Online | Green (#20b26c) |
“Online” |
| Offline | Red (#ef4444) |
“Offline” |
The badge is clickable and shows a detail modal with:
- Network status
- Whether syncing is active
- Count of pending operations
Sync Status Badge
A secondary badge below the status indicator:
| State | Color | Text | Auto-hide |
|---|---|---|---|
| Syncing | Amber | “Syncing changes…” | No |
| Synced | Green | “All changes synced” | After 3 s |
| Error | Red | “Sync error occurred” | No |
Sync Now Button
- Visible when online.
- Triggers
syncManager.syncAll(). - Disabled during active sync.
Error Banner
A persistent error banner (requires #errorBanner and #errorBannerMessage elements in the HTML) displays sync failure details with Retry and Dismiss buttons.
Conflict Notification
- Uses
showNotice()if available. - Auto-resolves via client-wins strategy (configurable to server-wins or merge).
- Shows success or failure notice after resolution.
localStorage Fallback
The state is simultaneously written to localStorage under key projectDashboardState. A rotating backup is kept at projectDashboardState.backup.
- Primary load: IndexedDB
- Fallback load: localStorage → legacy
projectTaskskey → defaults - Backup: localStorage written on every save (rotation: current → backup → overwrite)
Security
XSS Prevention (src/offline/utils/security.mjs)
| Function | Purpose |
|---|---|
escapeHtml(str) |
Escapes HTML entities using the DOM parser. Use when inserting user content via innerHTML. Prefer textContent for plain text. |
sanitizeCategory(value) |
Trims and limits category names to 30 characters. |
isValidPriority(p) |
Validates against low, medium, high, critical. |
isValidStatus(s) |
Validates against backlog, in_progress, review, completed, blocked. |
sanitizeTaskText(text, max=500) |
Trims and truncates task text to max length. |
Encryption (CryptoUtils in idb.mjs)
Optional AES-256-GCM encryption using Web Crypto API:
- Key derivation: PBKDF2 with 100,000 iterations, SHA-256, fixed salt.
- Default password:
'default-dashboard-key'(not a security boundary — dashboard has no secrets). - Available but unused: The current dashboard stores no sensitive data in IndexedDB.
Note:
initEncryption()must be called explicitly. Encryption is not enabled by default.
Exported Globals
Both StateManager and OfflineUIManager are exposed on window for backward compatibility:
window.StateManager // all public methods
window.OfflineUIManager // singleton instance
Event System
SyncManager Events
| Event | Payload | Description |
|---|---|---|
online |
— | Client connected to network |
offline |
— | Client lost connectivity |
syncStart |
— | Queue processing started |
syncComplete |
{ tasks? } |
All pending items processed |
syncError |
{ item, error } |
Sync failure |
queueUpdate |
{ operation, taskId, status } |
Queue item state changed |
conflictDetected |
{ item, error } |
HTTP 409 received |
conflictResolved |
{ taskId, resolvedData } |
Conflict resolved |
queueCleared |
— | All queue items removed |
StateManager Events
| Event | Payload | Description |
|---|---|---|
load |
state | Initial state loaded |
save |
state | State persisted |
change |
state | State mutated |
clear |
state | State cleared |