tt-help-cli-ycl 1.3.7 → 1.3.9
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/package.json +1 -1
- package/src/watch/public/index.html +180 -31
- package/src/watch/server.mjs +34 -1
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
.header h1 { font-size: 18px; color: #fe2c55; }
|
|
12
12
|
.header .meta { font-size: 12px; color: #888; }
|
|
13
13
|
.header .status { font-size: 12px; color: #4ade80; }
|
|
14
|
+
.script-link { font-size: 12px; color: #60a5fa; text-decoration: none; padding: 2px 8px; border: 1px solid #60a5fa; border-radius: 4px; }
|
|
15
|
+
.script-link:hover { background: #60a5fa; color: #fff; }
|
|
14
16
|
.stats { display: grid; grid-template-columns: repeat(7, 1fr); gap: 12px; margin-bottom: 16px; }
|
|
15
17
|
.stat-card { background: #1a1a24; border-radius: 8px; padding: 16px; text-align: center; }
|
|
16
18
|
.stat-card .label { font-size: 12px; color: #888; margin-bottom: 8px; }
|
|
@@ -42,11 +44,22 @@
|
|
|
42
44
|
.controls button:hover { border-color: #fe2c55; color: #fff; }
|
|
43
45
|
.controls button.active { background: #fe2c55; color: #fff; border-color: #fe2c55; }
|
|
44
46
|
.add-users { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
|
45
|
-
.add-users input { flex: 1; padding: 6px 12px; border: 1px solid #333; border-radius: 6px; background: #0f0f13; color: #e0e0e0; font-size: 13px; outline: none; }
|
|
46
|
-
.add-users input:focus { border-color: #fe2c55; }
|
|
47
47
|
.add-users button { padding: 6px 16px; border: none; border-radius: 6px; background: #fe2c55; color: #fff; font-size: 13px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
48
48
|
.add-users button:hover { background: #e61944; }
|
|
49
|
-
.
|
|
49
|
+
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.65); z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
|
50
|
+
.modal { background: #1a1a24; border-radius: 12px; padding: 24px; width: 520px; max-width: 90vw; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
|
51
|
+
.modal h3 { font-size: 16px; color: #e0e0e0; margin-bottom: 6px; }
|
|
52
|
+
.modal .hint { font-size: 12px; color: #888; margin-bottom: 16px; }
|
|
53
|
+
.modal textarea { width: 100%; height: 180px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #0f0f13; color: #e0e0e0; font-size: 13px; font-family: inherit; outline: none; resize: vertical; line-height: 1.6; }
|
|
54
|
+
.modal textarea:focus { border-color: #fe2c55; }
|
|
55
|
+
.modal textarea::placeholder { color: #555; }
|
|
56
|
+
.modal .preview { margin-top: 8px; font-size: 12px; color: #60a5fa; min-height: 20px; }
|
|
57
|
+
.modal .btn-row { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
|
58
|
+
.modal .btn-row button { padding: 8px 20px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
59
|
+
.modal .btn-cancel { background: #2a2a3a; color: #ccc; }
|
|
60
|
+
.modal .btn-cancel:hover { background: #333; }
|
|
61
|
+
.modal .btn-submit { background: #fe2c55; color: #fff; }
|
|
62
|
+
.modal .btn-submit:hover { background: #e61944; }
|
|
50
63
|
.toast { position: fixed; top: 16px; right: 16px; padding: 10px 20px; border-radius: 6px; font-size: 13px; z-index: 999; transition: opacity 0.3s; }
|
|
51
64
|
.toast.success { background: #166534; color: #fff; }
|
|
52
65
|
.toast.error { background: #991b1b; color: #fff; }
|
|
@@ -82,15 +95,47 @@
|
|
|
82
95
|
::-webkit-scrollbar { width: 6px; }
|
|
83
96
|
::-webkit-scrollbar-track { background: #1a1a24; }
|
|
84
97
|
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
|
85
|
-
@media (max-width: 768px) {
|
|
98
|
+
@media (max-width: 768px) {
|
|
99
|
+
body { padding: 8px; }
|
|
100
|
+
.header { flex-direction: column; gap: 6px; align-items: flex-start; }
|
|
101
|
+
.header h1 { font-size: 16px; }
|
|
102
|
+
.stats { grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
|
103
|
+
.stat-card { padding: 10px; }
|
|
104
|
+
.stat-card .label { font-size: 11px; }
|
|
105
|
+
.stat-card .value { font-size: 18px; }
|
|
106
|
+
.charts { grid-template-columns: 1fr; }
|
|
107
|
+
.table-wrap { padding: 10px; }
|
|
108
|
+
.controls { flex-wrap: wrap; gap: 6px; }
|
|
109
|
+
.controls input { flex: 0 0 100%; width: 100%; }
|
|
110
|
+
.controls button { flex: 0 0 calc(33.33% - 4px); min-width: 0; text-align: center; white-space: nowrap; font-size: 11px; padding: 8px 4px; }
|
|
111
|
+
#batchResetBtn { flex: 0 0 100% !important; font-size: 12px !important; padding: 8px 12px !important; }
|
|
112
|
+
.controls select { flex: 0 0 100%; width: 100%; }
|
|
113
|
+
.table-scroll { max-height: none; overflow: visible; }
|
|
114
|
+
table, thead, tbody, th, td, tr { display: block; }
|
|
115
|
+
thead { display: none; }
|
|
116
|
+
tr { background: #22222e; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; border: 1px solid #2a2a3a; }
|
|
117
|
+
tr:hover { background: #2a2a3a; }
|
|
118
|
+
td { padding: 4px 0; border: none; text-align: left; position: relative; padding-left: 40%; font-size: 13px; }
|
|
119
|
+
td::before { content: attr(data-label); position: absolute; left: 0; width: 36%; text-align: right; color: #888; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
120
|
+
td.user-id { font-size: 15px; font-weight: 700; color: #60a5fa; padding-left: 0; border-bottom: 1px solid #2a2a3a; margin-bottom: 4px; padding-bottom: 6px; }
|
|
121
|
+
td.user-id::before { display: none; }
|
|
122
|
+
td.user-id:hover { color: #fe2c55; }
|
|
123
|
+
.add-users { justify-content: center; }
|
|
124
|
+
.modal { width: 95vw; padding: 16px; }
|
|
125
|
+
.modal textarea { height: 140px; }
|
|
126
|
+
}
|
|
86
127
|
</style>
|
|
87
128
|
</head>
|
|
88
129
|
<body>
|
|
89
|
-
<div class="header">
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</
|
|
130
|
+
<div class="header">
|
|
131
|
+
<h1>TikTok 采集监控</h1>
|
|
132
|
+
<div class="meta" id="fileMeta">加载中...</div>
|
|
133
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
134
|
+
<a href="/scripts/run-explore.sh" class="script-link" download>mac</a>
|
|
135
|
+
<a href="/scripts/run-explore.bat" class="script-link" download>windows</a>
|
|
136
|
+
<span class="status" id="lastUpdate">--</span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
94
139
|
<div class="stats">
|
|
95
140
|
<div class="stat-card"><div class="label">总任务</div><div class="value total" id="statTotal">0</div></div>
|
|
96
141
|
<div class="stat-card"><div class="label">处理中</div><div class="value total" id="statProcessing">0</div></div>
|
|
@@ -113,9 +158,7 @@
|
|
|
113
158
|
<div class="table-wrap">
|
|
114
159
|
<h3>用户列表</h3>
|
|
115
160
|
<div class="add-users">
|
|
116
|
-
<
|
|
117
|
-
<button onclick="addUsers()">插入队列</button>
|
|
118
|
-
<span class="hint">插入到队列最前面优先处理</span>
|
|
161
|
+
<button onclick="openAddModal()">+ 插入队列</button>
|
|
119
162
|
</div>
|
|
120
163
|
<div id="toast" class="toast" style="display:none"></div>
|
|
121
164
|
<div class="controls">
|
|
@@ -127,6 +170,10 @@
|
|
|
127
170
|
<button data-filter="error" onclick="setFilter('error')">错误</button>
|
|
128
171
|
<button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
|
|
129
172
|
<button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标用户</button>
|
|
173
|
+
<button id="batchResetBtn" onclick="batchResetErrors()" style="display:none;padding:6px 10px;border:1px solid #f87171;border-radius:6px;background:transparent;color:#f87171;font-size:12px;cursor:pointer;font-weight:600;transition:all 0.2s;white-space:nowrap;">↻ 批量重新处理 (<span id="batchResetCount">0</span>)</button>
|
|
174
|
+
<select id="locationFilter" onchange="onLocationChange()" style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
|
|
175
|
+
<option value="">全部国家</option>
|
|
176
|
+
</select>
|
|
130
177
|
</div>
|
|
131
178
|
<div class="table-scroll">
|
|
132
179
|
<table>
|
|
@@ -142,6 +189,7 @@ const COLORS = ['#fe2c55','#60a5fa','#4ade80','#facc15','#f97316','#a855f7','#ec
|
|
|
142
189
|
let currentFilter = 'all';
|
|
143
190
|
let currentStats = null;
|
|
144
191
|
let currentUsers = [];
|
|
192
|
+
let currentLocation = '';
|
|
145
193
|
let prevStatValues = {};
|
|
146
194
|
let prevUserMap = {};
|
|
147
195
|
|
|
@@ -150,6 +198,7 @@ async function fetchStats() {
|
|
|
150
198
|
const res = await fetch('/api/stats');
|
|
151
199
|
currentStats = await res.json();
|
|
152
200
|
renderStats();
|
|
201
|
+
renderLocationFilter();
|
|
153
202
|
} catch (e) {
|
|
154
203
|
document.getElementById('lastUpdate').textContent = '\u8fde\u63a5\u5931\u8d25';
|
|
155
204
|
}
|
|
@@ -165,6 +214,7 @@ async function fetchUsers() {
|
|
|
165
214
|
}
|
|
166
215
|
const search = document.getElementById('searchInput').value.trim();
|
|
167
216
|
if (search) params.set('search', search);
|
|
217
|
+
if (currentLocation) params.set('location', currentLocation);
|
|
168
218
|
params.set('limit', '200');
|
|
169
219
|
const res = await fetch('/api/users?' + params.toString());
|
|
170
220
|
const data = await res.json();
|
|
@@ -255,20 +305,31 @@ function renderTable(users) {
|
|
|
255
305
|
if (u.noVideo) extraTags.push('<span class="tag no-video">\u65e0\u89c6\u9891</span>');
|
|
256
306
|
if (u.keepFollow) extraTags.push('<span class="tag keep-follow">\u5173\u6ce8\u5df2\u4fdd\u7559</span>');
|
|
257
307
|
if (u.hasFollowData === false) extraTags.push('<span class="tag no-follow">\u5173\u6ce8\u672a\u83b7\u53d6</span>');
|
|
308
|
+
const nick = (u.nickname || '').replace(/</g, '<').replace(/>/g, '>');
|
|
309
|
+
const fans = u.followerCount != null ? formatNum(u.followerCount) : '-';
|
|
310
|
+
const videos = u.videoCount != null ? u.videoCount : '-';
|
|
311
|
+
const loc = u.locationCreated || '-';
|
|
312
|
+
const claimer = u.claimedBy || '-';
|
|
313
|
+
const claimTime = u.claimedAt ? formatTime(u.claimedAt) : '-';
|
|
314
|
+
const procTime = u.processedAt ? formatTime(u.processedAt) : '-';
|
|
258
315
|
return `<tr${rowClass}>
|
|
259
|
-
<td class="user-id">@${u.uniqueId}</td>
|
|
260
|
-
<td>${
|
|
261
|
-
<td>${
|
|
262
|
-
<td>${
|
|
263
|
-
<td>${
|
|
264
|
-
<td>${sources || '-'}</td>
|
|
265
|
-
<td>${statusTag} ${extraTags.join(' ')}</td>
|
|
266
|
-
<td style="font-size:11px;color:#888">${
|
|
267
|
-
<td style="font-size:11px;color:#888">${
|
|
268
|
-
<td style="font-size:11px;color:#888">${
|
|
316
|
+
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
317
|
+
<td data-label="昵称">${nick}</td>
|
|
318
|
+
<td data-label="粉丝">${fans}</td>
|
|
319
|
+
<td data-label="视频">${videos}</td>
|
|
320
|
+
<td data-label="国家">${loc}</td>
|
|
321
|
+
<td data-label="来源">${sources || '-'}</td>
|
|
322
|
+
<td data-label="状态">${statusTag} ${extraTags.join(' ')}</td>
|
|
323
|
+
<td data-label="接收人" style="font-size:11px;color:#888">${claimer}</td>
|
|
324
|
+
<td data-label="领取时间" style="font-size:11px;color:#888">${claimTime}</td>
|
|
325
|
+
<td data-label="提交时间" style="font-size:11px;color:#888">${procTime}</td>
|
|
269
326
|
</tr>`;
|
|
270
327
|
}).join('');
|
|
271
328
|
|
|
329
|
+
const errorCount = users.filter(u => u.status === 'error').length;
|
|
330
|
+
const countEl = document.getElementById('batchResetCount');
|
|
331
|
+
if (countEl) countEl.textContent = errorCount;
|
|
332
|
+
|
|
272
333
|
prevUserMap = newUserMap;
|
|
273
334
|
}
|
|
274
335
|
|
|
@@ -289,6 +350,24 @@ function setFilter(f) {
|
|
|
289
350
|
document.querySelectorAll('.controls button').forEach(b => {
|
|
290
351
|
b.classList.toggle('active', b.dataset.filter === f);
|
|
291
352
|
});
|
|
353
|
+
const btn = document.getElementById('batchResetBtn');
|
|
354
|
+
btn.style.display = f === 'error' ? '' : 'none';
|
|
355
|
+
fetchUsers();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function renderLocationFilter() {
|
|
359
|
+
if (!currentStats || !currentStats.countryStats) return;
|
|
360
|
+
const sel = document.getElementById('locationFilter');
|
|
361
|
+
if (!sel) return;
|
|
362
|
+
const val = sel.value;
|
|
363
|
+
const entries = currentStats.countryStats.sort((a, b) => b.count - a.count);
|
|
364
|
+
sel.innerHTML = '<option value="">全部国家</option>' +
|
|
365
|
+
entries.map(c => `<option value="${c.country}"${val === c.country ? ' selected' : ''}>${c.country} (${c.count})</option>`).join('');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function onLocationChange() {
|
|
369
|
+
const sel = document.getElementById('locationFilter');
|
|
370
|
+
currentLocation = sel.value;
|
|
292
371
|
fetchUsers();
|
|
293
372
|
}
|
|
294
373
|
|
|
@@ -296,12 +375,56 @@ document.getElementById('searchInput').addEventListener('input', () => {
|
|
|
296
375
|
fetchUsers();
|
|
297
376
|
});
|
|
298
377
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
378
|
+
function parseUsernames(raw) {
|
|
379
|
+
return raw.split(/[,,\n\r]+/).map(s => s.replace(/^@/, '').trim()).filter(Boolean);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function openAddModal() {
|
|
383
|
+
let overlay = document.getElementById('addModalOverlay');
|
|
384
|
+
if (overlay) return;
|
|
385
|
+
overlay = document.createElement('div');
|
|
386
|
+
overlay.id = 'addModalOverlay';
|
|
387
|
+
overlay.className = 'modal-overlay';
|
|
388
|
+
overlay.innerHTML = `
|
|
389
|
+
<div class="modal">
|
|
390
|
+
<h3>插入用户到队列</h3>
|
|
391
|
+
<div class="hint">每行一个用户名,或用逗号分隔。支持 @username 或 username 格式。插入到队列最前面优先处理。</div>
|
|
392
|
+
<textarea id="modalUserInput" placeholder="例如: user1 user2 user3 或:user1, user2, user3"></textarea>
|
|
393
|
+
<div class="preview" id="modalPreview"></div>
|
|
394
|
+
<div class="btn-row">
|
|
395
|
+
<button class="btn-cancel" onclick="closeAddModal()">取消</button>
|
|
396
|
+
<button class="btn-submit" onclick="submitAddUsers()">确认插入</button>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
`;
|
|
400
|
+
document.body.appendChild(overlay);
|
|
401
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) closeAddModal(); });
|
|
402
|
+
const ta = document.getElementById('modalUserInput');
|
|
403
|
+
ta.focus();
|
|
404
|
+
ta.addEventListener('input', () => {
|
|
405
|
+
const names = parseUsernames(ta.value);
|
|
406
|
+
const preview = document.getElementById('modalPreview');
|
|
407
|
+
preview.textContent = names.length ? `共 ${names.length} 个用户名` : '';
|
|
408
|
+
});
|
|
409
|
+
ta.addEventListener('keydown', e => {
|
|
410
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
submitAddUsers();
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function closeAddModal() {
|
|
418
|
+
const overlay = document.getElementById('addModalOverlay');
|
|
419
|
+
if (overlay) overlay.remove();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function submitAddUsers() {
|
|
423
|
+
const ta = document.getElementById('modalUserInput');
|
|
424
|
+
const raw = ta.value.trim();
|
|
302
425
|
if (!raw) return;
|
|
303
426
|
|
|
304
|
-
const names = raw
|
|
427
|
+
const names = parseUsernames(raw);
|
|
305
428
|
if (names.length === 0) return;
|
|
306
429
|
|
|
307
430
|
try {
|
|
@@ -312,8 +435,8 @@ async function addUsers() {
|
|
|
312
435
|
});
|
|
313
436
|
const data = await res.json();
|
|
314
437
|
if (data.error) { showToast(data.error, true); return; }
|
|
438
|
+
closeAddModal();
|
|
315
439
|
showToast(data.message || `\u5df2\u63d2\u5165 ${data.added} \u4e2a\u7528\u6237`);
|
|
316
|
-
input.value = '';
|
|
317
440
|
fetchStats();
|
|
318
441
|
fetchUsers();
|
|
319
442
|
} catch (e) {
|
|
@@ -329,8 +452,8 @@ function showToast(msg, isError) {
|
|
|
329
452
|
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
|
330
453
|
}
|
|
331
454
|
|
|
332
|
-
document.
|
|
333
|
-
if (e.key === '
|
|
455
|
+
document.addEventListener('keydown', e => {
|
|
456
|
+
if (e.key === 'Escape') closeAddModal();
|
|
334
457
|
});
|
|
335
458
|
|
|
336
459
|
document.getElementById('userTable').addEventListener('click', e => {
|
|
@@ -433,6 +556,32 @@ async function resetJob(uniqueId) {
|
|
|
433
556
|
}
|
|
434
557
|
}
|
|
435
558
|
|
|
559
|
+
async function batchResetErrors() {
|
|
560
|
+
const errorUsers = currentUsers.filter(u => u.status === 'error');
|
|
561
|
+
if (errorUsers.length === 0) {
|
|
562
|
+
showToast('\u6ca1\u6709\u9700\u8981\u91cd\u7f6e\u7684\u9519\u8bef\u7528\u6237', true);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const userIds = errorUsers.map(u => u.uniqueId);
|
|
566
|
+
try {
|
|
567
|
+
const res = await fetch('/api/jobs/batch-reset', {
|
|
568
|
+
method: 'POST',
|
|
569
|
+
headers: { 'Content-Type': 'application/json' },
|
|
570
|
+
body: JSON.stringify({ userIds })
|
|
571
|
+
});
|
|
572
|
+
const data = await res.json();
|
|
573
|
+
if (data.error) {
|
|
574
|
+
showToast(data.error, true);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
showToast(`\u5df2\u91cd\u7f6e ${data.reset} / ${data.total} \u4e2a\u7528\u6237`);
|
|
578
|
+
fetchUsers();
|
|
579
|
+
fetchStats();
|
|
580
|
+
} catch (e) {
|
|
581
|
+
showToast('\u6279\u91cf\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
436
585
|
document.getElementById('statTargetCard').addEventListener('click', async () => {
|
|
437
586
|
try {
|
|
438
587
|
const res = await fetch('/api/target-users');
|
|
@@ -460,8 +609,8 @@ document.getElementById('statTargetCard').addEventListener('click', async () =>
|
|
|
460
609
|
|
|
461
610
|
fetchStats();
|
|
462
611
|
fetchUsers();
|
|
463
|
-
setInterval(fetchStats,
|
|
464
|
-
setInterval(fetchUsers,
|
|
612
|
+
setInterval(fetchStats, 10000);
|
|
613
|
+
setInterval(fetchUsers, 10000);
|
|
465
614
|
</script>
|
|
466
615
|
</body>
|
|
467
616
|
</html>
|
package/src/watch/server.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { spawn } from 'child_process';
|
|
@@ -187,6 +187,22 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
if (req.method === 'POST' && routePath === '/api/jobs/batch-reset') {
|
|
191
|
+
const body = await readBody(req);
|
|
192
|
+
const ids = Array.isArray(body.userIds) ? body.userIds : [];
|
|
193
|
+
if (ids.length === 0) {
|
|
194
|
+
sendJSON(res, 400, { error: 'userIds 不能为空' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let count = 0;
|
|
198
|
+
for (const uid of ids) {
|
|
199
|
+
const ret = store.resetJob(uid);
|
|
200
|
+
if (ret.saved) count++;
|
|
201
|
+
}
|
|
202
|
+
sendJSON(res, 200, { reset: count, total: ids.length });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
190
206
|
const jobPinMatch = routePath.match(/^\/api\/job\/([^/]+)\/pin$/);
|
|
191
207
|
if (req.method === 'POST' && jobPinMatch) {
|
|
192
208
|
const uniqueId = jobPinMatch[1];
|
|
@@ -233,6 +249,9 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
233
249
|
(u.nickname || '').toLowerCase().includes(s)
|
|
234
250
|
);
|
|
235
251
|
}
|
|
252
|
+
if (params.location) {
|
|
253
|
+
filtered = filtered.filter(u => u.locationCreated === params.location);
|
|
254
|
+
}
|
|
236
255
|
|
|
237
256
|
const statusOrder = { processing: 0, pending: 1, done: 2, error: 3, restricted: 4 };
|
|
238
257
|
filtered.sort((a, b) => {
|
|
@@ -261,6 +280,20 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
261
280
|
return;
|
|
262
281
|
}
|
|
263
282
|
|
|
283
|
+
const scriptMatch = routePath.match(/^\/scripts\/(.+)$/);
|
|
284
|
+
if (req.method === 'GET' && scriptMatch) {
|
|
285
|
+
const batDir = join(__dirname, '../../bat');
|
|
286
|
+
const scriptFile = join(batDir, scriptMatch[1]);
|
|
287
|
+
if (existsSync(scriptFile)) {
|
|
288
|
+
const content = readFileSync(scriptFile);
|
|
289
|
+
const ext = scriptMatch[1].split('.').pop();
|
|
290
|
+
const mime = ext === 'sh' ? 'text/x-sh' : ext === 'bat' ? 'text/bat' : 'text/plain';
|
|
291
|
+
res.writeHead(200, { 'Content-Type': `${mime}; charset=utf-8` });
|
|
292
|
+
res.end(content);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
264
297
|
res.writeHead(404);
|
|
265
298
|
res.end('Not Found');
|
|
266
299
|
});
|