@opencoop/opencode-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +178 -0
- package/README.fa.md +221 -0
- package/README.md +245 -0
- package/dist/auth/auth-manager.d.ts +45 -0
- package/dist/auth/auth-manager.js +157 -0
- package/dist/auth/auth-manager.js.map +1 -0
- package/dist/auth/session-manager.d.ts +14 -0
- package/dist/auth/session-manager.js +50 -0
- package/dist/auth/session-manager.js.map +1 -0
- package/dist/filesystem/change-tracker.d.ts +18 -0
- package/dist/filesystem/change-tracker.js +96 -0
- package/dist/filesystem/change-tracker.js.map +1 -0
- package/dist/filesystem/file-manager.d.ts +39 -0
- package/dist/filesystem/file-manager.js +189 -0
- package/dist/filesystem/file-manager.js.map +1 -0
- package/dist/filesystem/lock-manager.d.ts +15 -0
- package/dist/filesystem/lock-manager.js +120 -0
- package/dist/filesystem/lock-manager.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/server/mcp-server.d.ts +20 -0
- package/dist/server/mcp-server.js +392 -0
- package/dist/server/mcp-server.js.map +1 -0
- package/dist/types/index.d.ts +80 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.js +38 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/database.d.ts +12 -0
- package/dist/utils/database.js +110 -0
- package/dist/utils/database.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/web/public/assets/app.js +446 -0
- package/dist/web/public/assets/style.css +639 -0
- package/dist/web/public/index.html +190 -0
- package/dist/web/public/invite.html +132 -0
- package/dist/web/server.d.ts +3 -0
- package/dist/web/server.js +150 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// ==================== STATE ====================
|
|
2
|
+
let currentPage = 'config';
|
|
3
|
+
let currentMode = null;
|
|
4
|
+
|
|
5
|
+
// ==================== NAVIGATION ====================
|
|
6
|
+
document.querySelectorAll('.nav-links a').forEach(link => {
|
|
7
|
+
link.addEventListener('click', (e) => {
|
|
8
|
+
e.preventDefault();
|
|
9
|
+
const page = link.dataset.page;
|
|
10
|
+
navigateTo(page);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function navigateTo(page) {
|
|
15
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
16
|
+
document.querySelectorAll('.nav-links a').forEach(a => a.classList.remove('active'));
|
|
17
|
+
|
|
18
|
+
document.getElementById(`page-${page}`)?.classList.add('active');
|
|
19
|
+
document.querySelector(`[data-page="${page}"]`)?.classList.add('active');
|
|
20
|
+
|
|
21
|
+
currentPage = page;
|
|
22
|
+
loadPageData(page);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadPageData(page) {
|
|
26
|
+
switch (page) {
|
|
27
|
+
case 'dashboard': await loadDashboard(); break;
|
|
28
|
+
case 'changes': await loadChanges(); break;
|
|
29
|
+
case 'team': await loadTeam(); break;
|
|
30
|
+
case 'locks': await loadLocks(); break;
|
|
31
|
+
case 'config': await loadConfig(); break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ==================== STATUS CHECK ====================
|
|
36
|
+
async function checkStatus() {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch('/api/status');
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
|
|
41
|
+
document.getElementById('statusDot')?.classList.add('online');
|
|
42
|
+
document.getElementById('statusText').textContent = `Running on port ${data.port}`;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
document.getElementById('statusDot')?.classList.remove('online');
|
|
45
|
+
document.getElementById('statusText').textContent = 'Offline';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ==================== CONFIG PAGE ====================
|
|
50
|
+
async function loadConfig() {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch('/api/config');
|
|
53
|
+
const config = await res.json();
|
|
54
|
+
|
|
55
|
+
if (config.mode) {
|
|
56
|
+
selectMode(config.mode);
|
|
57
|
+
}
|
|
58
|
+
if (config.workspacePath) {
|
|
59
|
+
document.getElementById('workspace-path').value = config.workspacePath;
|
|
60
|
+
}
|
|
61
|
+
if (config.hostUrl) {
|
|
62
|
+
document.getElementById('host-url').value = config.hostUrl;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log('Failed to load config');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function selectMode(mode) {
|
|
70
|
+
currentMode = mode;
|
|
71
|
+
document.querySelectorAll('.mode-card').forEach(c => c.classList.remove('selected'));
|
|
72
|
+
document.querySelector(`[data-mode="${mode}"]`)?.classList.add('selected');
|
|
73
|
+
|
|
74
|
+
document.getElementById('host-config').classList.toggle('hidden', mode !== 'host');
|
|
75
|
+
document.getElementById('remote-config').classList.toggle('hidden', mode !== 'remote');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function selectFolder() {
|
|
79
|
+
const path = prompt('Enter project folder path:');
|
|
80
|
+
if (path) {
|
|
81
|
+
document.getElementById('workspace-path').value = path;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function generateInvite() {
|
|
86
|
+
const workspacePath = document.getElementById('workspace-path').value;
|
|
87
|
+
if (!workspacePath) {
|
|
88
|
+
alert('Please enter a project folder path first');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch('/api/invite', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
email: 'team@opencoop.local',
|
|
98
|
+
permissions: ['read', 'write'],
|
|
99
|
+
expires_in_days: 7
|
|
100
|
+
})
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const data = await res.json();
|
|
104
|
+
if (data.success) {
|
|
105
|
+
document.getElementById('invite-link').value = data.invite.link;
|
|
106
|
+
alert('Invite link generated!');
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
alert('Error generating invite');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function copyInviteLink() {
|
|
114
|
+
const input = document.getElementById('invite-link');
|
|
115
|
+
input.select();
|
|
116
|
+
navigator.clipboard.writeText(input.value);
|
|
117
|
+
alert('Copied!');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function connectToHost() {
|
|
121
|
+
const hostUrl = document.getElementById('host-url').value;
|
|
122
|
+
if (!hostUrl) {
|
|
123
|
+
alert('Please enter the host invite link');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await fetch('/api/config', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify({ mode: 'remote', hostUrl })
|
|
132
|
+
});
|
|
133
|
+
alert('Connected! Restart OpenCode to apply.');
|
|
134
|
+
} catch (e) {
|
|
135
|
+
alert('Error connecting');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function saveConfig() {
|
|
140
|
+
const workspacePath = document.getElementById('workspace-path').value;
|
|
141
|
+
const hostUrl = document.getElementById('host-url').value;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await fetch('/api/config', {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
mode: currentMode,
|
|
149
|
+
workspacePath,
|
|
150
|
+
hostUrl
|
|
151
|
+
})
|
|
152
|
+
});
|
|
153
|
+
alert('Configuration saved!');
|
|
154
|
+
} catch (e) {
|
|
155
|
+
alert('Error saving config');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ==================== DASHBOARD PAGE ====================
|
|
160
|
+
async function loadDashboard() {
|
|
161
|
+
try {
|
|
162
|
+
const [statsRes, teamRes] = await Promise.all([
|
|
163
|
+
fetch('/api/stats'),
|
|
164
|
+
fetch('/api/team')
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const stats = await statsRes.json();
|
|
168
|
+
const team = await teamRes.json();
|
|
169
|
+
|
|
170
|
+
document.getElementById('stat-total-changes').textContent = stats.totalChanges || 0;
|
|
171
|
+
document.getElementById('stat-online-users').textContent = stats.onlineUsers || 0;
|
|
172
|
+
document.getElementById('stat-active-locks').textContent = stats.activeLocks || 0;
|
|
173
|
+
document.getElementById('stat-team-members').textContent = team.members?.length || 0;
|
|
174
|
+
|
|
175
|
+
renderActivity(stats.recentActivity || []);
|
|
176
|
+
renderOnlineUsers(stats.online || []);
|
|
177
|
+
renderChangesByUser(stats.changesByUser || {});
|
|
178
|
+
renderActiveLocks(stats.locks || []);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error('Failed to load dashboard', e);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderActivity(changes) {
|
|
185
|
+
const container = document.getElementById('recent-activity');
|
|
186
|
+
if (!changes.length) {
|
|
187
|
+
container.innerHTML = '<div class="empty-state"><p>No recent activity</p></div>';
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
container.innerHTML = changes.map(c => `
|
|
192
|
+
<div class="activity-item">
|
|
193
|
+
<span class="activity-file">${escapeHtml(c.filePath)}</span>
|
|
194
|
+
<span class="activity-action ${c.action}">${c.action}</span>
|
|
195
|
+
<span class="activity-user">${escapeHtml(c.userId)}</span>
|
|
196
|
+
<span class="activity-time">${formatTime(c.timestamp)}</span>
|
|
197
|
+
</div>
|
|
198
|
+
`).join('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderOnlineUsers(users) {
|
|
202
|
+
const container = document.getElementById('online-users');
|
|
203
|
+
if (!users.length) {
|
|
204
|
+
container.innerHTML = '<div class="empty-state"><p>No users online</p></div>';
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
container.innerHTML = users.map(u => `
|
|
209
|
+
<div class="user-item">
|
|
210
|
+
<div class="user-avatar">${(u.user_id || 'U')[0].toUpperCase()}</div>
|
|
211
|
+
<span class="user-name">${escapeHtml(u.user_id)}</span>
|
|
212
|
+
<span class="user-status online">Online</span>
|
|
213
|
+
</div>
|
|
214
|
+
`).join('');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderChangesByUser(data) {
|
|
218
|
+
const container = document.getElementById('changes-by-user');
|
|
219
|
+
const entries = Object.entries(data);
|
|
220
|
+
|
|
221
|
+
if (!entries.length) {
|
|
222
|
+
container.innerHTML = '<div class="empty-state"><p>No data</p></div>';
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const max = Math.max(...entries.map(([, v]) => v));
|
|
227
|
+
|
|
228
|
+
container.innerHTML = entries.map(([user, count]) => `
|
|
229
|
+
<div style="margin-bottom: 0.5rem;">
|
|
230
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
|
|
231
|
+
<span>${escapeHtml(user)}</span>
|
|
232
|
+
<span>${count}</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div style="background: var(--bg-primary); border-radius: 4px; height: 8px;">
|
|
235
|
+
<div style="background: var(--accent); height: 100%; width: ${(count / max) * 100}%; border-radius: 4px;"></div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
`).join('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderActiveLocks(locks) {
|
|
242
|
+
const container = document.getElementById('active-locks');
|
|
243
|
+
if (!locks.length) {
|
|
244
|
+
container.innerHTML = '<div class="empty-state"><p>No active locks</p></div>';
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
container.innerHTML = locks.map(l => `
|
|
249
|
+
<div class="activity-item">
|
|
250
|
+
<span class="activity-file">${escapeHtml(l.filePath)}</span>
|
|
251
|
+
<span class="activity-user">${escapeHtml(l.userId)}</span>
|
|
252
|
+
<span class="activity-time">${l.reason || 'No reason'}</span>
|
|
253
|
+
</div>
|
|
254
|
+
`).join('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ==================== CHANGES PAGE ====================
|
|
258
|
+
async function loadChanges() {
|
|
259
|
+
const filePath = document.getElementById('filter-file')?.value;
|
|
260
|
+
const userId = document.getElementById('filter-user')?.value;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const params = new URLSearchParams();
|
|
264
|
+
if (filePath) params.set('file_path', filePath);
|
|
265
|
+
if (userId) params.set('user_id', userId);
|
|
266
|
+
params.set('limit', '100');
|
|
267
|
+
|
|
268
|
+
const res = await fetch(`/api/changes?${params}`);
|
|
269
|
+
const data = await res.json();
|
|
270
|
+
|
|
271
|
+
renderChanges(data.changes || []);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
console.error('Failed to load changes', e);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderChanges(changes) {
|
|
278
|
+
const container = document.getElementById('changes-list');
|
|
279
|
+
if (!changes.length) {
|
|
280
|
+
container.innerHTML = '<div class="empty-state"><p>No changes recorded yet</p></div>';
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
container.innerHTML = changes.map(c => `
|
|
285
|
+
<div class="change-item">
|
|
286
|
+
<span class="change-badge ${c.action}">${c.action}</span>
|
|
287
|
+
<span class="change-path">${escapeHtml(c.filePath)}</span>
|
|
288
|
+
<span class="change-user">${escapeHtml(c.userId)}</span>
|
|
289
|
+
<span class="change-time">${formatTime(c.timestamp)}</span>
|
|
290
|
+
</div>
|
|
291
|
+
`).join('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ==================== TEAM PAGE ====================
|
|
295
|
+
async function loadTeam() {
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch('/api/team');
|
|
298
|
+
const data = await res.json();
|
|
299
|
+
|
|
300
|
+
renderTeamMembers(data.members || []);
|
|
301
|
+
renderTeamOnline(data.online || []);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
console.error('Failed to load team', e);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderTeamMembers(members) {
|
|
308
|
+
const container = document.getElementById('team-members');
|
|
309
|
+
if (!members.length) {
|
|
310
|
+
container.innerHTML = '<div class="empty-state"><p>No team members yet</p></div>';
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
container.innerHTML = members.map(m => `
|
|
315
|
+
<div class="user-item">
|
|
316
|
+
<div class="user-avatar">${(m.user_id || 'U')[0].toUpperCase()}</div>
|
|
317
|
+
<span class="user-name">${escapeHtml(m.user_id)}</span>
|
|
318
|
+
<span class="user-status">${m.permissions || 'read,write'}</span>
|
|
319
|
+
</div>
|
|
320
|
+
`).join('');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderTeamOnline(users) {
|
|
324
|
+
const container = document.getElementById('team-online');
|
|
325
|
+
if (!users.length) {
|
|
326
|
+
container.innerHTML = '<div class="empty-state"><p>No users online</p></div>';
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
container.innerHTML = users.map(u => `
|
|
331
|
+
<div class="user-item">
|
|
332
|
+
<div class="user-avatar">${(u.user_id || 'U')[0].toUpperCase()}</div>
|
|
333
|
+
<span class="user-name">${escapeHtml(u.user_id)}</span>
|
|
334
|
+
<span class="user-status online">Online</span>
|
|
335
|
+
</div>
|
|
336
|
+
`).join('');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function createInvite() {
|
|
340
|
+
const email = document.getElementById('invite-email').value;
|
|
341
|
+
const permissions = document.getElementById('invite-permissions').value.split(',');
|
|
342
|
+
const days = parseInt(document.getElementById('invite-days').value) || 7;
|
|
343
|
+
|
|
344
|
+
if (!email) {
|
|
345
|
+
alert('Please enter an email');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const res = await fetch('/api/invite', {
|
|
351
|
+
method: 'POST',
|
|
352
|
+
headers: { 'Content-Type': 'application/json' },
|
|
353
|
+
body: JSON.stringify({
|
|
354
|
+
email,
|
|
355
|
+
permissions,
|
|
356
|
+
expires_in_days: days
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const data = await res.json();
|
|
361
|
+
if (data.success) {
|
|
362
|
+
document.getElementById('generated-invite').value = data.invite.link;
|
|
363
|
+
document.getElementById('invite-result').classList.remove('hidden');
|
|
364
|
+
}
|
|
365
|
+
} catch (e) {
|
|
366
|
+
alert('Error creating invite');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function copyGeneratedInvite() {
|
|
371
|
+
const input = document.getElementById('generated-invite');
|
|
372
|
+
input.select();
|
|
373
|
+
navigator.clipboard.writeText(input.value);
|
|
374
|
+
alert('Copied!');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ==================== LOCKS PAGE ====================
|
|
378
|
+
async function loadLocks() {
|
|
379
|
+
try {
|
|
380
|
+
const res = await fetch('/api/locks');
|
|
381
|
+
const data = await res.json();
|
|
382
|
+
|
|
383
|
+
renderLocks(data.locks || []);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
console.error('Failed to load locks', e);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function renderLocks(locks) {
|
|
390
|
+
const container = document.getElementById('locks-list');
|
|
391
|
+
if (!locks.length) {
|
|
392
|
+
container.innerHTML = '<div class="empty-state"><p>No active locks</p></div>';
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
container.innerHTML = `
|
|
397
|
+
<table>
|
|
398
|
+
<thead>
|
|
399
|
+
<tr>
|
|
400
|
+
<th>File</th>
|
|
401
|
+
<th>Locked By</th>
|
|
402
|
+
<th>Reason</th>
|
|
403
|
+
<th>Acquired</th>
|
|
404
|
+
<th>Expires</th>
|
|
405
|
+
</tr>
|
|
406
|
+
</thead>
|
|
407
|
+
<tbody>
|
|
408
|
+
${locks.map(l => `
|
|
409
|
+
<tr>
|
|
410
|
+
<td>${escapeHtml(l.filePath)}</td>
|
|
411
|
+
<td>${escapeHtml(l.userId)}</td>
|
|
412
|
+
<td>${escapeHtml(l.reason || '-')}</td>
|
|
413
|
+
<td>${formatTime(l.acquiredAt)}</td>
|
|
414
|
+
<td>${formatTime(l.expiresAt)}</td>
|
|
415
|
+
</tr>
|
|
416
|
+
`).join('')}
|
|
417
|
+
</tbody>
|
|
418
|
+
</table>
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ==================== UTILITIES ====================
|
|
423
|
+
function escapeHtml(str) {
|
|
424
|
+
if (!str) return '';
|
|
425
|
+
const div = document.createElement('div');
|
|
426
|
+
div.textContent = str;
|
|
427
|
+
return div.innerHTML;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function formatTime(ts) {
|
|
431
|
+
if (!ts) return '-';
|
|
432
|
+
const d = new Date(ts);
|
|
433
|
+
const now = new Date();
|
|
434
|
+
const diff = now - d;
|
|
435
|
+
|
|
436
|
+
if (diff < 60000) return 'Just now';
|
|
437
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
438
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
439
|
+
|
|
440
|
+
return d.toLocaleDateString();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ==================== INIT ====================
|
|
444
|
+
checkStatus();
|
|
445
|
+
loadConfig();
|
|
446
|
+
setInterval(checkStatus, 30000);
|