claude-code-mux 0.6.3

High-performance, intelligent Claude Code router built in Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# LocalStorage-based State Management

The admin UI uses localStorage to manage client-side state and syncs with the server only on explicit save.

## Concept

LocalStorage-based state management uses browser localStorage as a client-side cache to minimize unnecessary communication with the server.

### Why this approach?

The server reads TOML config files and **does not reload them until restart**. Therefore:

1. **Wrong approach**: Re-fetch from server after each operation
   - Save config to server → Fetch again from server
   - Problem: Server returns **stale data** until restart

2. **Correct approach**: Use localStorage as client-side cache
   - Fetch from server once on page load → Save to localStorage
   - All operations (add/delete/edit) update localStorage only
   - Sync to server only when user clicks "Save" button

## Architecture

```
Page Load
Fetch config from server (/api/config/json)
Save to localStorage (cache)
Render UI
User actions (Add/delete Provider, Add/delete Model)
Update localStorage only (NOT server)
Update UI immediately
User clicks "Save" or "Save & Restart" button
Sync localStorage → server (/api/config/json POST)
(Optional) Restart server (/api/restart POST)
```

## Implementation

### Global State

```javascript
const appState = {
    config: null,
    loaded: false
};
```

### Core Functions

#### `saveToLocalStorage(config)`
Saves config to localStorage.

```javascript
function saveToLocalStorage(config) {
    try {
        localStorage.setItem('ccm_config', JSON.stringify(config));
        return true;
    } catch (error) {
        console.error('Failed to save to localStorage:', error);
        return false;
    }
}
```

#### `loadFromLocalStorage()`
Loads config from localStorage.

```javascript
function loadFromLocalStorage() {
    try {
        const stored = localStorage.getItem('ccm_config');
        return stored ? JSON.parse(stored) : null;
    } catch (error) {
        console.error('Failed to load from localStorage:', error);
        return null;
    }
}
```

#### `loadConfig()` - Called once on page load
Fetches config from server and saves to localStorage and appState.

```javascript
async function loadConfig() {
    try {
        const response = await fetch('/api/config/json');
        const config = await response.json();
        appState.config = config;
        appState.loaded = true;
        saveToLocalStorage(config);
        return config;
    } catch (error) {
        console.error('Failed to load config:', error);
        notifyError('Failed to load configuration');
        return null;
    }
}
```

#### `syncToServer()` - Called only from save buttons
Sends localStorage config to server.

```javascript
async function syncToServer() {
    try {
        const response = await fetch('/api/config/json', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(appState.config)
        });
        if (response.ok) {
            saveToLocalStorage(appState.config);
        }
        return response.ok;
    } catch (error) {
        console.error('Failed to sync to server:', error);
        return false;
    }
}
```

### Initialization

```javascript
window.addEventListener('DOMContentLoaded', async () => {
    await loadConfig();  // Fetch from server once
    handleRoute();
    renderOverview();
    updateLastSaved();
});
```

## Usage Examples

### Delete Provider

```javascript
async function deleteProvider(index) {
    if (!confirm('Are you sure you want to delete this Provider?')) {
        return;
    }

    try {
        // 1. Delete from state
        appState.config.providers.splice(index, 1);

        // 2. Save to localStorage only (NOT server)
        saveToLocalStorage(appState.config);

        // 3. Update UI immediately
        renderProvidersList();
        renderOverview();

        // 4. Notify save required
        notifySuccess('Provider deleted (click Save to apply)');
    } catch (error) {
        console.error('Failed to delete provider:', error);
        notifyError('Failed to delete Provider');
    }
}
```

### Add Model

```javascript
document.getElementById('add-model-form').addEventListener('submit', async function(e) {
    e.preventDefault();

    const newModel = {
        // ... collect form data
    };

    try {
        // 1. Add to state
        if (!appState.config.models) {
            appState.config.models = [];
        }
        appState.config.models.push(newModel);

        // 2. Save to localStorage only (NOT server)
        saveToLocalStorage(appState.config);

        // 3. Reset form and navigate
        notifySuccess('Model added (click Save to apply)');
        e.target.reset();
        navigate({ tab: 'models', view: null });
    } catch (error) {
        console.error('Failed to add model:', error);
        notifyError('Error adding model');
    }
});
```

### Save All

```javascript
async function saveAllConfig() {
    console.log('Saving all configuration...');

    try {
        // Sync localStorage → server
        const success = await syncToServer();

        if (success) {
            updateLastSaved();
            notifySuccess('All settings saved');
            renderOverview();
        } else {
            notifyError('Save failed');
        }
    } catch (error) {
        console.error('Failed to save all config:', error);
        notifyError('Error during save');
    }
}
```

### Save & Restart

```javascript
async function saveAndRestart() {
    if (!confirm('Save settings and restart server?')) return;

    try {
        // 1. Save first
        await saveAllConfig();

        // 2. Wait briefly then restart
        setTimeout(async () => {
            await fetch('/api/restart', { method: 'POST' });
            notifySuccess('Server restarted');
        }, 500);
    } catch (error) {
        console.error('Failed to save and restart:', error);
        notifyError('Failed to save and restart');
    }
}
```

## Data Flow

### Page Load
```
User accesses /admin
loadConfig() called
GET /api/config/json
appState.config = response
saveToLocalStorage(config)
renderOverview()
```

### Add Provider/Model
```
User submits add form
appState.config.providers.push(newProvider)
saveToLocalStorage(appState.config)
navigate({ tab: 'providers', view: null })
renderProvidersList() (reads from localStorage)
```

### Delete Provider/Model
```
User clicks delete button
appState.config.providers.splice(index, 1)
saveToLocalStorage(appState.config)
renderProvidersList() (reads from localStorage)
```

### Save Button Click
```
User clicks "Save All"
syncToServer() called
POST /api/config/json (appState.config)
Server saves to TOML file
notifySuccess('Saved')
```

### Save & Restart Click
```
User clicks "Save & Restart"
saveAllConfig() → syncToServer()
POST /api/config/json
POST /api/restart
Server restarts → loads new config
```

## Benefits

### 1. Solves server restart problem
```javascript
// ❌ Before: Server returns stale data
appState.config.providers.push(newProvider);
await saveToServer();  // Saves to TOML
const config = await fetch('/api/config/json');  // ⚠️ Stale data!

// ✅ After: Immediate reflection via localStorage
appState.config.providers.push(newProvider);
saveToLocalStorage(appState.config);  // Immediate reflection!
renderProvidersList();  // Reads from localStorage
```

### 2. Performance improvement
- Eliminates unnecessary network requests
- Immediate UI updates
- Editable config even offline

### 3. Better user experience
- All changes immediately reflected in UI
- Explicit save prevents unintended changes
- "Unsaved changes" state clearly indicated

### 4. Data consistency
- localStorage is single source of truth
- Server sync only on explicit save
- Changes persist across page refreshes

## Caveats

### 1. Server sync required
Users must click "Save" button. Otherwise:
- Changes only in localStorage
- Changes lost on server restart
- Changes not visible from other browsers/devices

### 2. localStorage capacity limits
- Most browsers: 5-10MB limit
- CCM config typically tens of KB, so not a problem

### 3. Multi-tab sync
Current implementation assumes single tab. With simultaneous edits in multiple tabs:
- Each tab uses independent localStorage
- Last save overwrites previous save
- Future improvement: Sync tabs via `storage` event

### 4. Page refresh
```javascript
// localStorage priority on page refresh
window.addEventListener('DOMContentLoaded', async () => {
    // Re-fetch from server even with localStorage data
    // Reason: Another user may have saved changes
    await loadConfig();  // Server fetch → update localStorage
    renderOverview();
});
```

### 5. Error handling
```javascript
try {
    saveToLocalStorage(appState.config);
} catch (error) {
    // QuotaExceededError, SecurityError, etc.
    console.error('Failed to save to localStorage:', error);
    notifyError('Failed to save locally');
}
```

## Best Practices

### 1. Always show notifications
```javascript
notifySuccess('Provider added (click Save to apply)');
```
Clearly inform users that save is required.

### 2. Maintain state consistency
```javascript
// State update → localStorage save → UI update
appState.config.providers.push(newProvider);
saveToLocalStorage(appState.config);
renderProvidersList();
```

### 3. Restore on error
```javascript
const backup = JSON.parse(JSON.stringify(appState.config));
try {
    appState.config.providers.splice(index, 1);
    saveToLocalStorage(appState.config);
    renderProvidersList();
} catch (error) {
    appState.config = backup;  // Restore
    notifyError('Operation failed');
}
```

### 4. Confirm server sync
```javascript
async function syncToServer() {
    const response = await fetch('/api/config/json', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(appState.config)
    });

    if (response.ok) {
        // Only update localStorage on success
        saveToLocalStorage(appState.config);
    }

    return response.ok;
}
```

## File Location

Implementation is in:
- `src/server/admin.html` (lines 592-647, 802-827, 939-964, 1218-1273, 1319-1337)

## Related Documentation

- [URL-based State Management]./url-state-management.md - Navigation state management using URL parameters

## Future Improvements

1. **Change tracking**
   - Compare localStorage vs server state
   - Add "unsaved changes" indicator

2. **Auto-save**
   - Option: Auto-save every N seconds
   - Warn on page exit

3. **Multi-tab sync**
   ```javascript
   window.addEventListener('storage', (e) => {
       if (e.key === 'ccm_config') {
           appState.config = JSON.parse(e.newValue);
           renderOverview();
       }
   });
   ```

4. **Undo/Redo**
   - Save change history
   - Support Ctrl+Z / Ctrl+Shift+Z

5. **Optimistic UI updates**
   - Update UI before server response
   - Rollback on failure