• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bymosbot / mosbot-dashboard / 22777470071

06 Mar 2026 06:53PM UTC coverage: 95.012% (-0.9%) from 95.918%
22777470071

Pull #6

github

web-flow
Merge 37a2a8c3a into 7e23d2514
Pull Request #6: sync: roll up fork main into upstream main

630 of 761 branches covered (82.79%)

Branch coverage included in aggregate %.

136 of 154 new or added lines in 6 files covered. (88.31%)

2 existing lines in 1 file now uncovered.

3180 of 3249 relevant lines covered (97.88%)

4.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.9
/src/api/client.js
1
import axios from 'axios';
1✔
2
import logger from '../utils/logger';
1✔
3

4
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';
1!
5

6
// Retry configuration
1✔
7
const MAX_RETRIES = 3;
1✔
8
// OpenClaw gateway proxy endpoints can be slow; use longer timeout than default (10s)
1✔
9
const OPENCLAW_TIMEOUT = 30000;
1✔
10
const RETRY_DELAY_BASE = 1000; // Base delay in milliseconds
1✔
11
const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504]; // Status codes that should be retried
1✔
12

13
// Calculate exponential backoff delay with jitter
1✔
14
const getRetryDelay = (attempt) => {
1✔
15
  const exponentialDelay = RETRY_DELAY_BASE * Math.pow(2, attempt);
36✔
16
  const jitter = Math.random() * 0.3 * exponentialDelay; // Add up to 30% jitter
36✔
17
  return exponentialDelay + jitter;
36✔
18
};
36✔
19

20
// Check if error should be retried
1✔
21
const shouldRetry = (error, retryCount) => {
1✔
22
  // Don't retry if we've exceeded max retries
42✔
23
  if (retryCount >= MAX_RETRIES) {
42✔
24
    return false;
1✔
25
  }
1✔
26

27
  // Don't retry if request config explicitly disables retries
41✔
28
  if (error.config?.__retryDisabled === true) {
42!
29
    return false;
×
30
  }
✔
31

32
  // Retry on network errors (no response received)
41✔
33
  if (!error.response) {
42✔
34
    return true;
30✔
35
  }
30✔
36

37
  // Retry on specific server errors (5xx) and some client errors (408, 429)
11✔
38
  const status = error.response.status;
11✔
39
  return RETRYABLE_STATUS_CODES.includes(status);
11✔
40
};
11✔
41

42
const shouldSuppressErrorLogging = (error) => {
1✔
43
  const config = error.config || {};
6!
44
  if (config.__suppressErrorLogging === true) {
6!
NEW
45
    return true;
×
NEW
46
  }
×
47

48
  const status = error.response?.status;
6✔
49
  const suppressedStatuses = config.__suppressErrorStatuses;
6✔
50
  return (
6✔
51
    Number.isInteger(status) &&
6✔
52
    Array.isArray(suppressedStatuses) &&
6✔
53
    suppressedStatuses.includes(status)
1✔
54
  );
6✔
55
};
6✔
56

57
export const api = axios.create({
1✔
58
  baseURL: API_URL,
1✔
59
  headers: {
1✔
60
    'Content-Type': 'application/json',
1✔
61
  },
1✔
62
  timeout: 10000,
1✔
63
  withCredentials: true, // Required to send cookies (including Cloudflare Access auth cookie)
1✔
64
});
1✔
65

66
// Request interceptor
1✔
67
api.interceptors.request.use(
1✔
68
  (config) => {
1✔
69
    // Add any auth tokens here if needed
31✔
70
    const token = localStorage.getItem('auth_token');
31✔
71
    if (token) {
31✔
72
      config.headers.Authorization = `Bearer ${token}`;
1✔
73
    }
1✔
74

75
    return config;
31✔
76
  },
1✔
77
  (error) => {
1✔
78
    return Promise.reject(error);
1✔
79
  },
1✔
80
);
1✔
81

82
// Response interceptor with retry logic
1✔
83
api.interceptors.response.use(
1✔
84
  (response) => {
1✔
85
    return response;
5✔
86
  },
1✔
87
  async (error) => {
1✔
88
    const config = error.config;
42✔
89

90
    // Initialize retry count if not present
42✔
91
    if (!config.__retryCount) {
42✔
92
      config.__retryCount = 0;
35✔
93
    }
35✔
94

95
    // Check if we should retry this request
42✔
96
    if (shouldRetry(error, config.__retryCount)) {
42✔
97
      config.__retryCount += 1;
36✔
98
      const delay = getRetryDelay(config.__retryCount - 1);
36✔
99

100
      // Wait for the calculated delay before retrying
36✔
101
      await new Promise((resolve) => setTimeout(resolve, delay));
36✔
102

103
      // Retry the request
13✔
104
      return api(config);
13✔
105
    }
13✔
106

107
    if (shouldSuppressErrorLogging(error)) {
13✔
108
      return Promise.reject(error);
1✔
109
    }
1✔
110

111
    // Use structured logging
5✔
112
    if (error.response) {
13✔
113
      // Server responded with error
4✔
114
      logger.error('API request failed', error, {
4✔
115
        url: config?.url,
4✔
116
        method: config?.method,
4✔
117
        status: error.response.status,
4✔
118
        retryCount: config.__retryCount || 0,
4✔
119
      });
4✔
120
    } else if (error.request) {
13✔
121
      // Request made but no response
1✔
122
      logger.error('Network request failed', error, {
1✔
123
        url: config?.url,
1✔
124
        method: config?.method,
1✔
125
        retryCount: config.__retryCount || 0,
1!
126
      });
1✔
127
    } else {
1!
128
      // Error setting up request
×
129
      logger.error('Request setup failed', error, {
×
130
        url: config?.url,
×
131
        method: config?.method,
×
132
      });
×
133
    }
✔
134
    return Promise.reject(error);
5✔
135
  },
1✔
136
);
1✔
137

138
// Instance config — non-sensitive server settings (timezone, etc.)
1✔
139
export const getInstanceConfig = async () => {
1✔
140
  const response = await api.get('/config');
1✔
141
  return response.data.data;
1✔
142
};
1✔
143

144
// Models API - get available models from OpenClaw config
1✔
145
export const getModels = async () => {
1✔
146
  try {
3✔
147
    const response = await api.get('/models');
3✔
148
    return response.data.data.models || [];
3✔
149
  } catch (error) {
3✔
150
    logger.warn('Failed to fetch models from API, using fallback', { error: error.message });
1✔
151
    return []; // Return empty array, component will use AVAILABLE_MODELS fallback
1✔
152
  }
1✔
153
};
3✔
154

155
// OpenClaw Agents API - auto-discover agents from OpenClaw configuration
1✔
156
export const getAgents = async () => {
1✔
157
  const response = await api.get('/openclaw/agents');
2✔
158
  return response.data.data;
1✔
159
};
1✔
160

161
// OpenClaw Agents Config API - get agent hierarchy and configuration
1✔
162
export const getAgentsConfig = async () => {
1✔
163
  const response = await api.get('/openclaw/agents/config');
1✔
164
  return response.data.data;
1✔
165
};
1✔
166

167
// OpenClaw Subagents API - returns object with { running, queued, completed, retention }
1✔
168
export const getSubagents = async () => {
1✔
169
  const response = await api.get('/openclaw/subagents');
1✔
170
  return response.data.data;
1✔
171
};
1✔
172

173
// Get active subagent sessions as a flat array - combines running and queued sessions
1✔
174
// This is useful for components that need to iterate over sessions (e.g., Agents, TaskManagerOverview)
1✔
175
// Normalizes field names: sessionLabel → label, status values to lowercase
1✔
176
export const getActiveSubagentSessions = async () => {
1✔
177
  const response = await api.get('/openclaw/subagents');
1✔
178
  const data = response.data.data;
1✔
179

180
  // Normalize field names for UI consumption
1✔
181
  const normalizeSession = (session, statusOverride) => ({
1✔
182
    ...session,
2✔
183
    // Normalize sessionLabel to label for UI matching
2✔
184
    label: session.sessionLabel || session.label || null,
2!
185
    // Normalize status to lowercase strings (running, queued)
2✔
186
    status: statusOverride || (session.status || '').toLowerCase(),
2!
187
    // Keep original fields for debugging/future use
2✔
188
    sessionLabel: session.sessionLabel,
2✔
189
    taskId: session.taskId || null,
2✔
190
    taskNumber: session.taskNumber || null,
2✔
191
    startedAt: session.startedAt || session.queuedAt || null,
2!
192
  });
1✔
193

194
  // Combine running and queued into a single array with normalized fields
1✔
195
  const sessions = [
1✔
196
    ...(data.running || []).map((s) => normalizeSession(s, 'running')),
1!
197
    ...(data.queued || []).map((s) => normalizeSession(s, 'queued')),
1!
198
  ];
1✔
199

200
  return sessions;
1✔
201
};
1✔
202

203
// OpenClaw Cron Jobs API - get configured scheduled jobs from OpenClaw
1✔
204
export const getCronJobs = async () => {
1✔
205
  const response = await api.get('/openclaw/cron-jobs', { timeout: OPENCLAW_TIMEOUT });
3✔
206
  // Response shape: { data: { version: number, jobs: CronJob[] } }
3✔
207
  return response.data.data?.jobs ?? response.data.data ?? [];
3✔
208
};
3✔
209

210
export const getSchedulerStats = async () => {
1✔
211
  const response = await api.get('/openclaw/cron-jobs/stats', { timeout: 15000 });
9✔
212
  return response.data.data;
1✔
213
};
1✔
214

215
// Get run history for a cron job (from workspace /cron/runs/{jobId}.jsonl)
1✔
216
export const getCronJobRuns = async (jobId, { limit = 50 } = {}) => {
1✔
217
  const response = await api.get(`/openclaw/cron-jobs/${jobId}/runs`, {
1✔
218
    params: { limit },
1✔
219
    timeout: OPENCLAW_TIMEOUT,
1✔
220
  });
1✔
221
  return response.data; // { runs: [...], total: number }
1✔
222
};
1✔
223

224
// Create a new cron job (admin only)
1✔
225
export const createCronJob = async (payload) => {
1✔
226
  const response = await api.post('/openclaw/cron-jobs', payload);
1✔
227
  return response.data.data;
1✔
228
};
1✔
229

230
// Update an existing cron job (admin only) — PATCH for partial updates
1✔
231
export const updateCronJob = async (jobId, payload) => {
1✔
232
  const response = await api.patch(`/openclaw/cron-jobs/${jobId}`, payload);
1✔
233
  return response.data.data;
1✔
234
};
1✔
235

236
// Delete a cron job (admin only)
1✔
237
export const deleteCronJob = async (jobId) => {
1✔
238
  await api.delete(`/openclaw/cron-jobs/${jobId}`);
1✔
239
};
1✔
240

241
// Set enabled state for a cron job (admin only)
1✔
242
export const setCronJobEnabled = async (jobId, enabled) => {
1✔
243
  const response = await api.patch(`/openclaw/cron-jobs/${jobId}/enabled`, { enabled });
1✔
244
  return response.data.data;
1✔
245
};
1✔
246

247
// Manually trigger a cron job to run now (admin only)
1✔
248
export const triggerCronJob = async (jobId) => {
1✔
249
  const response = await api.post(`/openclaw/cron-jobs/${jobId}/run`);
1✔
250
  return response.data.data;
1✔
251
};
1✔
252

253
// Task-scoped subagents API
1✔
254
export const getTaskSubagents = async (taskId) => {
1✔
255
  const response = await api.get(`/tasks/${taskId}/subagents`);
1✔
256
  return response.data;
1✔
257
};
1✔
258

259
// Lightweight session counts for the sidebar avatar (no cost/usage enrichment).
1✔
260
// Much faster than getOpenClawSessions — only calls sessions.list on the gateway.
1✔
261
export const getOpenClawSessionStatus = async () => {
1✔
262
  const response = await api.get('/openclaw/sessions/status', { timeout: OPENCLAW_TIMEOUT });
1✔
263
  return response.data.data; // { running, active, idle, total }
1✔
264
};
1✔
265

266
// OpenClaw Sessions API - full session list with cost/token enrichment.
1✔
267
// Used by Agent Monitor; too slow for global polling.
1✔
268
export const getOpenClawSessions = async () => {
1✔
269
  const response = await api.get('/openclaw/sessions', { timeout: OPENCLAW_TIMEOUT });
2✔
270
  return {
2✔
271
    sessions: response.data.data,
2✔
272
    dailyCost: response.data.dailyCost || 0,
2✔
273
  };
2✔
274
};
2✔
275

276
// Delete an OpenClaw session by session key (admin only)
1✔
277
export const deleteSession = async (sessionKey) => {
1✔
278
  await api.delete('/openclaw/sessions', {
1✔
279
    params: { key: sessionKey },
1✔
280
  });
1✔
281
};
1✔
282

283
// Get full message history for a specific session
1✔
284
export const getSessionMessages = async (sessionKey, { limit = 50, includeTools = false } = {}) => {
1✔
285
  // Extract sessionId from sessionKey for the URL path
1✔
286
  // sessionKey format: "agent:coo:main" or similar
1✔
287
  // We'll use the full key as sessionId since the endpoint accepts it
1✔
288
  const sessionId = encodeURIComponent(sessionKey);
1✔
289

290
  const response = await api.get(`/openclaw/sessions/${sessionId}/messages`, {
1✔
291
    params: {
1✔
292
      key: sessionKey,
1✔
293
      limit,
1✔
294
      includeTools,
1✔
295
    },
1✔
296
  });
1✔
297
  return response.data.data;
1✔
298
};
1✔
299

300
// Usage & Cost Analytics API
1✔
301
export const getUsageAnalytics = async (range = '7d', customRange = null) => {
1✔
302
  // Get user's timezone for accurate "today" calculation
2✔
303
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
2✔
304
  const params = { timezone };
2✔
305

306
  // If custom range is provided, use startDate/endDate instead of range
2✔
307
  if (customRange && customRange.startDate && customRange.endDate) {
2✔
308
    params.startDate = customRange.startDate.toISOString();
1✔
309
    params.endDate = customRange.endDate.toISOString();
1✔
310
  } else {
1✔
311
    params.range = range;
1✔
312
  }
1✔
313

314
  const response = await api.get('/openclaw/usage', { params });
2✔
315
  return response.data.data;
2✔
316
};
2✔
317

318
// Reset Usage Data (admin only, requires password confirmation)
1✔
319
export const resetUsageData = async (password) => {
1✔
320
  const response = await api.post('/openclaw/usage/reset', { password });
1✔
321
  return response.data.data;
1✔
322
};
1✔
323

324
// Reset Activity Logs (admin only, requires password confirmation)
1✔
325
export const resetActivityLogs = async (password) => {
1✔
326
  const response = await api.post('/activity/reset', { password });
1✔
327
  return response.data.data;
1✔
328
};
1✔
329

330
// OpenClaw Config Editor API (admin/owner only)
1✔
331
export const getOpenClawConfig = async () => {
1✔
332
  const response = await api.get('/openclaw/config', { timeout: OPENCLAW_TIMEOUT });
1✔
333
  return response.data.data;
1✔
334
};
1✔
335

336
export const updateOpenClawConfig = async ({ raw, baseHash, note }) => {
1✔
337
  const response = await api.put(
1✔
338
    '/openclaw/config',
1✔
339
    { raw, baseHash, note },
1✔
340
    { timeout: OPENCLAW_TIMEOUT },
1✔
341
  );
1✔
342
  return response.data.data;
1✔
343
};
1✔
344

345
export const listOpenClawConfigBackups = async () => {
1✔
346
  const response = await api.get('/openclaw/config/backups', { timeout: OPENCLAW_TIMEOUT });
1✔
347
  return response.data.data;
1✔
348
};
1✔
349

350
export const getOpenClawConfigBackupContent = async (path) => {
1✔
351
  const response = await api.get('/openclaw/config/backups/content', {
1✔
352
    params: { path },
1✔
353
    timeout: OPENCLAW_TIMEOUT,
1✔
354
  });
1✔
355
  return response.data.data;
1✔
356
};
1✔
357

358
// Standups API
1✔
359
export const getStandups = async ({ limit = 50, offset = 0 } = {}) => {
1✔
360
  const response = await api.get('/standups', {
1✔
361
    params: { limit, offset },
1✔
362
  });
1✔
363
  return response.data;
1✔
364
};
1✔
365

366
export const getLatestStandup = async () => {
1✔
367
  const response = await api.get('/standups/latest');
1✔
368
  return response.data.data;
1✔
369
};
1✔
370

371
export const getStandupById = async (id) => {
1✔
372
  const response = await api.get(`/standups/${id}`);
1✔
373
  return response.data.data;
1✔
374
};
1✔
375

376
// Reset Standups (admin only, requires password confirmation)
1✔
377
export const resetStandups = async (password) => {
1✔
378
  const response = await api.post('/standups/reset', { password });
1✔
379
  return response.data.data;
1✔
380
};
1✔
381

382
export default api;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc