@magpiecloud/mags 1.8.13 → 1.8.15
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/README.md +95 -378
- package/bin/mags.js +196 -104
- package/index.js +6 -52
- package/package.json +22 -4
- package/API.md +0 -388
- package/Mags-API.postman_collection.json +0 -374
- package/QUICKSTART.md +0 -295
- package/deploy-page.sh +0 -171
- package/mags +0 -0
- package/mags.sh +0 -270
- package/nodejs/README.md +0 -197
- package/nodejs/bin/mags.js +0 -1146
- package/nodejs/index.js +0 -642
- package/nodejs/package.json +0 -42
- package/python/INTEGRATION.md +0 -800
- package/python/README.md +0 -161
- package/python/dist/magpie_mags-1.3.5-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.3.5.tar.gz +0 -0
- package/python/examples/demo.py +0 -181
- package/python/pyproject.toml +0 -39
- package/python/src/magpie_mags.egg-info/PKG-INFO +0 -182
- package/python/src/magpie_mags.egg-info/SOURCES.txt +0 -9
- package/python/src/magpie_mags.egg-info/dependency_links.txt +0 -1
- package/python/src/magpie_mags.egg-info/requires.txt +0 -1
- package/python/src/magpie_mags.egg-info/top_level.txt +0 -1
- package/python/src/mags/__init__.py +0 -6
- package/python/src/mags/client.py +0 -573
- package/python/test_sdk.py +0 -78
- package/skill.md +0 -153
- package/website/api.html +0 -1095
- package/website/claude-skill.html +0 -481
- package/website/cookbook/hn-marketing.html +0 -410
- package/website/cookbook/hn-marketing.sh +0 -42
- package/website/cookbook.html +0 -282
- package/website/env.js +0 -4
- package/website/index.html +0 -801
- package/website/llms.txt +0 -334
- package/website/login.html +0 -108
- package/website/mags.md +0 -210
- package/website/script.js +0 -453
- package/website/styles.css +0 -908
- package/website/tokens.html +0 -169
- package/website/usage.html +0 -185
package/website/script.js
DELETED
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
/* ── Tab switching ──────────────────────────────────────── */
|
|
2
|
-
|
|
3
|
-
document.addEventListener('click', (e) => {
|
|
4
|
-
const tab = e.target.closest('.tab[data-tab]');
|
|
5
|
-
if (!tab) return;
|
|
6
|
-
|
|
7
|
-
const group = tab.closest('.tab-group');
|
|
8
|
-
if (!group) return;
|
|
9
|
-
|
|
10
|
-
const tabName = tab.getAttribute('data-tab');
|
|
11
|
-
|
|
12
|
-
// Deactivate all tabs in this group
|
|
13
|
-
group.querySelectorAll('.tab-bar .tab').forEach((t) => t.classList.remove('active'));
|
|
14
|
-
tab.classList.add('active');
|
|
15
|
-
|
|
16
|
-
// Show matching content, hide others
|
|
17
|
-
group.querySelectorAll('.tab-content').forEach((panel) => {
|
|
18
|
-
panel.classList.toggle('active', panel.getAttribute('data-tab') === tabName);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
/* ── Pattern card expand/collapse ──────────────────────── */
|
|
23
|
-
|
|
24
|
-
document.addEventListener('click', (e) => {
|
|
25
|
-
const header = e.target.closest('.pattern-header');
|
|
26
|
-
if (!header) return;
|
|
27
|
-
|
|
28
|
-
const card = header.closest('.pattern-card');
|
|
29
|
-
if (!card) return;
|
|
30
|
-
|
|
31
|
-
card.classList.toggle('open');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
/* ── Reveal observer ───────────────────────────────────── */
|
|
35
|
-
|
|
36
|
-
const revealItems = Array.from(document.querySelectorAll('[data-reveal]'));
|
|
37
|
-
|
|
38
|
-
if ('IntersectionObserver' in window) {
|
|
39
|
-
const observer = new IntersectionObserver(
|
|
40
|
-
(entries) => {
|
|
41
|
-
entries.forEach((entry) => {
|
|
42
|
-
if (entry.isIntersecting) {
|
|
43
|
-
entry.target.classList.add('visible');
|
|
44
|
-
observer.unobserve(entry.target);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
{ threshold: 0.2 }
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
revealItems.forEach((item) => observer.observe(item));
|
|
52
|
-
} else {
|
|
53
|
-
revealItems.forEach((item) => item.classList.add('visible'));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const apiBaseMeta = document.querySelector('meta[name="api-base"]');
|
|
57
|
-
const authBaseMeta = document.querySelector('meta[name="auth-base"]');
|
|
58
|
-
const envConfig = window.MAGS_ENV || {};
|
|
59
|
-
const API_BASE = envConfig.API_BASE || apiBaseMeta?.content?.trim() || '';
|
|
60
|
-
const AUTH_BASE = envConfig.AUTH_BASE || authBaseMeta?.content?.trim() || API_BASE || '';
|
|
61
|
-
|
|
62
|
-
const withBase = (path, base) => {
|
|
63
|
-
if (!path) return '';
|
|
64
|
-
if (path.startsWith('http')) return path;
|
|
65
|
-
if (!base) return path;
|
|
66
|
-
return `${base}${path}`;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const ACCESS_TOKEN_KEY = 'microvm-access-token';
|
|
70
|
-
const REFRESH_TOKEN_KEY = 'microvm-refresh-token';
|
|
71
|
-
|
|
72
|
-
const tokenStore = {
|
|
73
|
-
getAccessToken() {
|
|
74
|
-
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
|
75
|
-
},
|
|
76
|
-
getRefreshToken() {
|
|
77
|
-
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
|
78
|
-
},
|
|
79
|
-
setTokens(tokens) {
|
|
80
|
-
if (!tokens?.accessToken || !tokens?.refreshToken) return;
|
|
81
|
-
localStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken);
|
|
82
|
-
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken);
|
|
83
|
-
},
|
|
84
|
-
clear() {
|
|
85
|
-
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
|
86
|
-
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/* ── Extract tokens from URL after OAuth callback redirect ── */
|
|
91
|
-
(function () {
|
|
92
|
-
var params = new URLSearchParams(window.location.search);
|
|
93
|
-
var token = params.get('token');
|
|
94
|
-
var refresh = params.get('refresh');
|
|
95
|
-
if (token && refresh) {
|
|
96
|
-
tokenStore.setTokens({ accessToken: token, refreshToken: refresh });
|
|
97
|
-
params.delete('token');
|
|
98
|
-
params.delete('refresh');
|
|
99
|
-
var clean = window.location.pathname;
|
|
100
|
-
var remaining = params.toString();
|
|
101
|
-
if (remaining) clean += '?' + remaining;
|
|
102
|
-
window.history.replaceState({}, '', clean);
|
|
103
|
-
}
|
|
104
|
-
})();
|
|
105
|
-
|
|
106
|
-
/* ── Redirect to login for protected pages when not authed ── */
|
|
107
|
-
(function () {
|
|
108
|
-
if (!document.querySelector('[data-auth-required]')) return;
|
|
109
|
-
if (tokenStore.getAccessToken()) return;
|
|
110
|
-
var next = window.location.pathname + window.location.search;
|
|
111
|
-
window.location.replace('login.html?next=' + encodeURIComponent(next));
|
|
112
|
-
})();
|
|
113
|
-
|
|
114
|
-
const refreshTokens = async () => {
|
|
115
|
-
const refreshToken = tokenStore.getRefreshToken();
|
|
116
|
-
if (!refreshToken) return null;
|
|
117
|
-
|
|
118
|
-
const response = await fetch('/api/v1/auth/refresh', {
|
|
119
|
-
method: 'POST',
|
|
120
|
-
headers: { 'Content-Type': 'application/json' },
|
|
121
|
-
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
122
|
-
credentials: 'include',
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
if (!response.ok) {
|
|
126
|
-
tokenStore.clear();
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const data = await response.json();
|
|
131
|
-
tokenStore.setTokens({
|
|
132
|
-
accessToken: data.access_token,
|
|
133
|
-
refreshToken: data.refresh_token,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
return data;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const apiRequest = async (path, options = {}) => {
|
|
140
|
-
const headers = new Headers(options.headers || {});
|
|
141
|
-
if (options.body && !headers.has('Content-Type')) {
|
|
142
|
-
headers.set('Content-Type', 'application/json');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const accessToken = tokenStore.getAccessToken();
|
|
146
|
-
if (accessToken) {
|
|
147
|
-
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const response = await fetch(withBase(path, API_BASE), {
|
|
151
|
-
...options,
|
|
152
|
-
headers,
|
|
153
|
-
credentials: 'include',
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (response.status === 401 && tokenStore.getRefreshToken() && !options._retry) {
|
|
157
|
-
const refreshed = await refreshTokens();
|
|
158
|
-
if (refreshed) {
|
|
159
|
-
return apiRequest(path, { ...options, _retry: true });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return response;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const apiJson = async (path, options = {}) => {
|
|
167
|
-
const response = await apiRequest(path, options);
|
|
168
|
-
let data = null;
|
|
169
|
-
try {
|
|
170
|
-
data = await response.json();
|
|
171
|
-
} catch (error) {
|
|
172
|
-
data = null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!response.ok) {
|
|
176
|
-
throw new Error(data?.error || 'Request failed');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return data;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const copyButtons = Array.from(document.querySelectorAll('.copy-button'));
|
|
183
|
-
|
|
184
|
-
copyButtons.forEach((button) => {
|
|
185
|
-
button.addEventListener('click', async () => {
|
|
186
|
-
const text = button.getAttribute('data-copy') || '';
|
|
187
|
-
if (!text) return;
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
await navigator.clipboard.writeText(text);
|
|
191
|
-
button.classList.add('copied');
|
|
192
|
-
const original = button.textContent || 'Copy';
|
|
193
|
-
button.textContent = 'Copied';
|
|
194
|
-
setTimeout(() => {
|
|
195
|
-
button.textContent = original;
|
|
196
|
-
button.classList.remove('copied');
|
|
197
|
-
}, 1400);
|
|
198
|
-
} catch (error) {
|
|
199
|
-
button.textContent = 'Failed';
|
|
200
|
-
setTimeout(() => {
|
|
201
|
-
button.textContent = 'Copy';
|
|
202
|
-
}, 1400);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const googleLogin = document.querySelector('[data-google-login]');
|
|
208
|
-
if (googleLogin) {
|
|
209
|
-
googleLogin.addEventListener('click', () => {
|
|
210
|
-
var url = withBase('/auth/google', AUTH_BASE);
|
|
211
|
-
var next = new URLSearchParams(window.location.search).get('next');
|
|
212
|
-
if (next) url += (url.includes('?') ? '&' : '?') + 'next=' + encodeURIComponent(next);
|
|
213
|
-
window.location.href = url;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const authLinks = Array.from(document.querySelectorAll('[data-auth-link]'));
|
|
218
|
-
authLinks.forEach((link) => {
|
|
219
|
-
const path = link.getAttribute('data-auth-link');
|
|
220
|
-
if (!path) return;
|
|
221
|
-
link.setAttribute('href', withBase(path, AUTH_BASE));
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const loginForm = document.querySelector('[data-login-form]');
|
|
225
|
-
if (loginForm) {
|
|
226
|
-
const message = loginForm.querySelector('[data-login-message]');
|
|
227
|
-
const submitButton = loginForm.querySelector('button[type="submit"]');
|
|
228
|
-
|
|
229
|
-
loginForm.addEventListener('submit', async (event) => {
|
|
230
|
-
event.preventDefault();
|
|
231
|
-
if (message) message.textContent = '';
|
|
232
|
-
if (submitButton) {
|
|
233
|
-
submitButton.disabled = true;
|
|
234
|
-
submitButton.textContent = 'Signing in...';
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const formData = new FormData(loginForm);
|
|
238
|
-
const payload = {
|
|
239
|
-
email: formData.get('email'),
|
|
240
|
-
password: formData.get('password'),
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const data = await apiJson('/api/v1/auth/login', {
|
|
245
|
-
method: 'POST',
|
|
246
|
-
body: JSON.stringify(payload),
|
|
247
|
-
});
|
|
248
|
-
tokenStore.setTokens({
|
|
249
|
-
accessToken: data.access_token,
|
|
250
|
-
refreshToken: data.refresh_token,
|
|
251
|
-
});
|
|
252
|
-
if (message) {
|
|
253
|
-
message.textContent = 'Signed in. Redirecting...';
|
|
254
|
-
}
|
|
255
|
-
loginForm.reset();
|
|
256
|
-
var dest = new URLSearchParams(window.location.search).get('next') || 'usage.html';
|
|
257
|
-
setTimeout(() => {
|
|
258
|
-
window.location.href = dest;
|
|
259
|
-
}, 600);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
if (message) {
|
|
262
|
-
const fallback = withBase('/auth/login', AUTH_BASE);
|
|
263
|
-
message.textContent = error.message?.includes('Failed to fetch')
|
|
264
|
-
? `Login failed. Open ${fallback}`
|
|
265
|
-
: error.message || 'Login failed.';
|
|
266
|
-
}
|
|
267
|
-
} finally {
|
|
268
|
-
if (submitButton) {
|
|
269
|
-
submitButton.disabled = false;
|
|
270
|
-
submitButton.textContent = 'Sign in';
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const authStatus = document.querySelector('[data-auth-status]');
|
|
277
|
-
if (authStatus) {
|
|
278
|
-
apiJson('/api/v1/auth/me')
|
|
279
|
-
.then((user) => {
|
|
280
|
-
authStatus.textContent = `Signed in as ${user.email}.`;
|
|
281
|
-
})
|
|
282
|
-
.catch(() => {
|
|
283
|
-
authStatus.textContent = 'Sign in to view usage data.';
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const usageStats = document.querySelectorAll('[data-stat]');
|
|
288
|
-
const jobsBody = document.querySelector('[data-jobs-body]');
|
|
289
|
-
const jobsEmpty = document.querySelector('[data-jobs-empty]');
|
|
290
|
-
const authRequired = document.querySelector('[data-auth-required]');
|
|
291
|
-
|
|
292
|
-
const formatNumber = (value, suffix = '') => {
|
|
293
|
-
if (value === null || value === undefined) return '—';
|
|
294
|
-
const num = Number(value);
|
|
295
|
-
if (Number.isNaN(num)) return '—';
|
|
296
|
-
return `${num.toFixed(2)}${suffix}`;
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const formatDate = (value) => {
|
|
300
|
-
if (!value) return '—';
|
|
301
|
-
const date = new Date(value);
|
|
302
|
-
if (Number.isNaN(date.getTime())) return '—';
|
|
303
|
-
return date.toLocaleString();
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const renderJobs = (jobs) => {
|
|
307
|
-
if (!jobsBody || !jobsEmpty) return;
|
|
308
|
-
jobsBody.innerHTML = '';
|
|
309
|
-
if (!jobs?.length) {
|
|
310
|
-
jobsBody.appendChild(jobsEmpty);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
jobs.forEach((job) => {
|
|
315
|
-
const row = document.createElement('div');
|
|
316
|
-
row.className = 'table-row';
|
|
317
|
-
|
|
318
|
-
const name = job.name || job.request_id || 'Untitled';
|
|
319
|
-
const status = job.status || 'unknown';
|
|
320
|
-
const created = formatDate(job.created_at);
|
|
321
|
-
const durationMs = job.script_duration_ms || job.total_duration_ms || 0;
|
|
322
|
-
const duration = durationMs ? `${(durationMs / 1000).toFixed(1)}s` : '—';
|
|
323
|
-
const logsUrl = job.request_id
|
|
324
|
-
? withBase(`/api/v1/mags-jobs/${job.request_id}/logs`, API_BASE)
|
|
325
|
-
: '#';
|
|
326
|
-
|
|
327
|
-
row.innerHTML = `
|
|
328
|
-
<span>${name}</span>
|
|
329
|
-
<span><span class="status ${status}">${status}</span></span>
|
|
330
|
-
<span>${created}</span>
|
|
331
|
-
<span>${duration}</span>
|
|
332
|
-
<span><a class="text-link" href="${logsUrl}" target="_blank" rel="noreferrer">Logs</a></span>
|
|
333
|
-
`;
|
|
334
|
-
|
|
335
|
-
jobsBody.appendChild(row);
|
|
336
|
-
});
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
const loadUsage = async () => {
|
|
340
|
-
if (!usageStats.length && !jobsBody) return;
|
|
341
|
-
try {
|
|
342
|
-
if (authRequired) authRequired.classList.add('hidden');
|
|
343
|
-
|
|
344
|
-
const usage = await apiJson('/api/v1/mags-jobs/usage');
|
|
345
|
-
usageStats.forEach((stat) => {
|
|
346
|
-
const key = stat.getAttribute('data-stat');
|
|
347
|
-
if (!key) return;
|
|
348
|
-
if (key === 'cpu_seconds') stat.textContent = formatNumber(usage.cpu_seconds, 's');
|
|
349
|
-
if (key === 'memory_gb_seconds') stat.textContent = formatNumber(usage.memory_gb_seconds, ' GB-s');
|
|
350
|
-
if (key === 'storage_gb_seconds') stat.textContent = formatNumber(usage.storage_gb_seconds, ' GB-s');
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const jobs = await apiJson('/api/v1/mags-jobs?page=1&page_size=10');
|
|
354
|
-
renderJobs(jobs.jobs);
|
|
355
|
-
} catch (error) {
|
|
356
|
-
if (authRequired) authRequired.classList.remove('hidden');
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const tokenTable = document.querySelector('[data-token-body]');
|
|
361
|
-
const tokenEmpty = document.querySelector('[data-token-empty]');
|
|
362
|
-
const tokenForm = document.querySelector('[data-token-form]');
|
|
363
|
-
const tokenMessage = document.querySelector('[data-token-message]');
|
|
364
|
-
const tokenResult = document.querySelector('[data-token-result]');
|
|
365
|
-
const tokenValue = document.querySelector('[data-token-value]');
|
|
366
|
-
|
|
367
|
-
const renderTokens = (tokens) => {
|
|
368
|
-
if (!tokenTable || !tokenEmpty) return;
|
|
369
|
-
tokenTable.innerHTML = '';
|
|
370
|
-
if (!tokens?.length) {
|
|
371
|
-
tokenTable.appendChild(tokenEmpty);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
tokens.forEach((token) => {
|
|
376
|
-
const row = document.createElement('div');
|
|
377
|
-
row.className = 'table-row';
|
|
378
|
-
const created = formatDate(token.created_at);
|
|
379
|
-
const expires = token.expires_at ? formatDate(token.expires_at) : 'Never';
|
|
380
|
-
const status = token.active ? 'active' : 'revoked';
|
|
381
|
-
row.innerHTML = `
|
|
382
|
-
<span>${token.name}</span>
|
|
383
|
-
<span>${created}</span>
|
|
384
|
-
<span>${expires}</span>
|
|
385
|
-
<span><span class="status ${token.active ? 'running' : 'error'}">${status}</span></span>
|
|
386
|
-
<span><button class="revoke-button" data-revoke-token="${token.id}" type="button">Revoke</button></span>
|
|
387
|
-
`;
|
|
388
|
-
tokenTable.appendChild(row);
|
|
389
|
-
});
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const loadTokens = async () => {
|
|
393
|
-
if (!tokenTable) return;
|
|
394
|
-
try {
|
|
395
|
-
if (authRequired) authRequired.classList.add('hidden');
|
|
396
|
-
const data = await apiJson('/api/v1/tokens');
|
|
397
|
-
renderTokens(data.tokens);
|
|
398
|
-
} catch (error) {
|
|
399
|
-
if (authRequired) authRequired.classList.remove('hidden');
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
if (tokenForm) {
|
|
404
|
-
tokenForm.addEventListener('submit', async (event) => {
|
|
405
|
-
event.preventDefault();
|
|
406
|
-
if (tokenMessage) tokenMessage.textContent = '';
|
|
407
|
-
const formData = new FormData(tokenForm);
|
|
408
|
-
const payload = {
|
|
409
|
-
name: formData.get('name'),
|
|
410
|
-
};
|
|
411
|
-
const expires = formData.get('expires_in');
|
|
412
|
-
if (expires) payload.expires_in = Number(expires);
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
const data = await apiJson('/api/v1/tokens', {
|
|
416
|
-
method: 'POST',
|
|
417
|
-
body: JSON.stringify(payload),
|
|
418
|
-
});
|
|
419
|
-
if (tokenValue) {
|
|
420
|
-
tokenValue.textContent = data.token;
|
|
421
|
-
}
|
|
422
|
-
if (tokenResult) {
|
|
423
|
-
tokenResult.classList.remove('hidden');
|
|
424
|
-
}
|
|
425
|
-
const copyButton = tokenResult?.querySelector('.copy-button');
|
|
426
|
-
if (copyButton) {
|
|
427
|
-
copyButton.setAttribute('data-copy', data.token);
|
|
428
|
-
}
|
|
429
|
-
if (tokenMessage) tokenMessage.textContent = 'Token created.';
|
|
430
|
-
tokenForm.reset();
|
|
431
|
-
loadTokens();
|
|
432
|
-
} catch (error) {
|
|
433
|
-
if (tokenMessage) tokenMessage.textContent = error.message || 'Failed to create token.';
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
document.addEventListener('click', async (event) => {
|
|
439
|
-
const revokeButton = event.target.closest('[data-revoke-token]');
|
|
440
|
-
if (!revokeButton) return;
|
|
441
|
-
const tokenId = revokeButton.getAttribute('data-revoke-token');
|
|
442
|
-
if (!tokenId) return;
|
|
443
|
-
if (!confirm('Revoke this token?')) return;
|
|
444
|
-
try {
|
|
445
|
-
await apiJson(`/api/v1/tokens/${tokenId}`, { method: 'DELETE' });
|
|
446
|
-
loadTokens();
|
|
447
|
-
} catch (error) {
|
|
448
|
-
if (tokenMessage) tokenMessage.textContent = error.message || 'Failed to revoke token.';
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
loadUsage();
|
|
453
|
-
loadTokens();
|