tt-help-cli-ycl 1.3.35 → 1.3.36

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.
@@ -103,6 +103,12 @@
103
103
  color: #a78bfa;
104
104
  }
105
105
 
106
+ .stat-card .value-sub {
107
+ font-size: 12px;
108
+ font-weight: 400;
109
+ margin-top: 2px;
110
+ }
111
+
106
112
  .stat-card.clickable {
107
113
  cursor: pointer;
108
114
  }
@@ -161,10 +167,21 @@
161
167
  padding-left: 6px;
162
168
  font-size: 11px;
163
169
  color: #fff;
170
+ white-space: nowrap;
171
+ }
172
+
173
+ .bar-row .target-badge {
174
+ width: 60px;
175
+ text-align: center;
176
+ color: #4ade80;
177
+ font-weight: 600;
178
+ font-size: 12px;
179
+ flex-shrink: 0;
180
+ margin: 0 6px;
164
181
  }
165
182
 
166
183
  .bar-row .count {
167
- width: 80px;
184
+ width: 55px;
168
185
  text-align: right;
169
186
  color: #888;
170
187
  flex-shrink: 0;
@@ -814,7 +831,7 @@
814
831
  </div>
815
832
  <div class="stats">
816
833
  <div class="stat-card">
817
- <div class="label">总任务</div>
834
+ <div class="label">总用户</div>
818
835
  <div class="value total" id="statTotal">0</div>
819
836
  </div>
820
837
  <div class="stat-card">
@@ -838,11 +855,11 @@
838
855
  <div class="value error" id="statRestricted">0</div>
839
856
  </div>
840
857
  <div class="stat-card">
841
- <div class="label">预处理任务</div>
858
+ <div class="label">待补资料</div>
842
859
  <div class="value target" id="statUserUpdateTasks">0</div>
843
860
  </div>
844
861
  <div class="stat-card clickable" id="statTargetCard">
845
- <div class="label">目标用户(EU商家)</div>
862
+ <div class="label">目标商家</div>
846
863
  <div class="value target" id="statTarget">0</div>
847
864
  </div>
848
865
  </div>
@@ -889,10 +906,10 @@
889
906
  <button data-filter="all" class="active" onclick="setFilter('all')">全部</button>
890
907
  <button data-filter="processing" onclick="setFilter('processing')">处理中</button>
891
908
  <button data-filter="pending" onclick="setFilter('pending')">待处理</button>
892
- <button data-filter="done" onclick="setFilter('done')">已处理</button>
909
+ <button data-filter="done" onclick="setFilter('done')">已完成</button>
893
910
  <button data-filter="error" onclick="setFilter('error')">错误</button>
894
911
  <button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
895
- <button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标用户(EU)</button>
912
+ <button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标商家</button>
896
913
  <select id="targetLocationFilter" onchange="onTargetLocationChange()"
897
914
  style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;display:none">
898
915
  <option value="">全部目标国家</option>
@@ -917,9 +934,9 @@
917
934
  <th>猜测国家</th>
918
935
  <th>来源</th>
919
936
  <th>状态</th>
920
- <th>接收人</th>
937
+ <th>处理端</th>
921
938
  <th>领取时间</th>
922
- <th>提交时间</th>
939
+ <th>完成时间</th>
923
940
  </tr>
924
941
  </thead>
925
942
  <tbody id="userTable"></tbody>
@@ -1018,11 +1035,19 @@
1018
1035
  } catch (e) { }
1019
1036
  }
1020
1037
 
1038
+ function formatStatNum(value) {
1039
+ const num = Number(value) || 0;
1040
+ if (Math.abs(num) < 1000) return String(num);
1041
+ if (Math.abs(num) < 10000) return num.toLocaleString('zh-CN');
1042
+ const wan = num / 10000;
1043
+ return wan.toFixed(1).replace(/\.0+$/, '') + '万';
1044
+ }
1045
+
1021
1046
  function flashEl(id, value) {
1022
1047
  const el = document.getElementById(id);
1023
1048
  if (!el) return;
1024
1049
  const prev = prevStatValues[id];
1025
- el.textContent = value;
1050
+ el.textContent = formatStatNum(value);
1026
1051
  if (prev !== undefined && prev !== value) {
1027
1052
  el.classList.remove('flash-change');
1028
1053
  void el.offsetWidth;
@@ -1043,7 +1068,12 @@
1043
1068
  flashEl('statTarget', d.targetUsers);
1044
1069
  flashEl('statUserUpdateTasks', d.userUpdateTasks || 0);
1045
1070
  document.getElementById('lastUpdate').textContent = '\u66f4\u65b0\u4e8e ' + new Date().toLocaleTimeString();
1046
- document.getElementById('fileMeta').textContent = (d.processingUsers || 0) + ' \u5904\u7406\u4e2d, ' + d.totalUsers + ' \u4e2a\u7528\u6237';
1071
+ document.getElementById('fileMeta').textContent =
1072
+ formatStatNum(d.processingUsers || 0) +
1073
+ ' \u5904\u7406\u4e2d, ' +
1074
+ formatStatNum(d.totalUsers) +
1075
+ ' \u4e2a\u7528\u6237, \u5f85\u5904\u7406 ' +
1076
+ formatStatNum(d.pendingUsers || 0);
1047
1077
 
1048
1078
  renderCountryChart(d.countryStats);
1049
1079
  renderSourceChart(d.sourceStats);
@@ -1064,13 +1094,19 @@
1064
1094
  if (!filtered.length) { el.innerHTML = '<span style="color:#666;font-size:12px">\u6682\u65e0\u6570\u636e</span>'; return; }
1065
1095
  const max = filtered[0].count;
1066
1096
  const top = filtered.slice(0, 15);
1067
- el.innerHTML = top.map((c, i) => `
1097
+ el.innerHTML = top.map((c, i) => {
1098
+ const targetBadge = c.targetCount > 0
1099
+ ? `<span class="target-badge">🎯 ${c.targetCount}</span>`
1100
+ : `<span class="target-badge" style="visibility:hidden"> </span>`;
1101
+ return `
1068
1102
  <div class="bar-row">
1069
1103
  <span class="name">${c.country}</span>
1070
1104
  <div class="bar-bg"><div class="bar-fill" style="width:${(c.count / max * 100)}%;background:${COLORS[i % COLORS.length]}">${c.count}</div></div>
1105
+ ${targetBadge}
1071
1106
  <span class="count">${(currentStats ? (c.count / currentStats.totalUsers * 100).toFixed(1) : 0)}%</span>
1072
1107
  </div>
1073
- `).join('');
1108
+ `;
1109
+ }).join('');
1074
1110
  }
1075
1111
 
1076
1112
  function renderSourceChart(sources) {
@@ -1098,7 +1134,7 @@
1098
1134
  const statusTags = {
1099
1135
  restricted: '<span class="tag error">\u53d7\u9650(\u8df3\u8fc7)</span>',
1100
1136
  error: '<span class="tag error">\u9519\u8bef(\u5f85\u91cd\u8bd5)</span>',
1101
- done: '<span class="tag processed">\u5df2\u5904\u7406</span>',
1137
+ done: '<span class="tag processed">\u5df2\u5b8c\u6210</span>',
1102
1138
  processing: '<span class="tag processing">\u5904\u7406\u4e2d</span>',
1103
1139
  pending: '<span class="tag pending">\u5f85\u5904\u7406</span>',
1104
1140
  };
@@ -1128,9 +1164,9 @@
1128
1164
  <td data-label="猜测国家">${guessedLoc}</td>
1129
1165
  <td data-label="来源">${sources || '-'}</td>
1130
1166
  <td data-label="状态">${statusTag} ${extraTags.join(' ')}</td>
1131
- <td data-label="接收人" style="font-size:11px;color:#888">${claimer}</td>
1167
+ <td data-label="处理端" style="font-size:11px;color:#888">${claimer}</td>
1132
1168
  <td data-label="领取时间" style="font-size:11px;color:#888">${claimTime}</td>
1133
- <td data-label="提交时间" style="font-size:11px;color:#888">${procTime}</td>
1169
+ <td data-label="完成时间" style="font-size:11px;color:#888">${procTime}</td>
1134
1170
  </tr>`;
1135
1171
  }).join('');
1136
1172
 
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
  import { spawn } from "child_process";
8
8
  import { createStore } from "./data-store.js";
9
9
 
10
- const TARGET_LOCATIONS = ["ES", "PL", "NL", "BE", "DE", "FR", "IT", "IE"];
10
+ const TARGET_LOCATIONS = ["ES", "PL", "NL", "BE", "DE", "FR", "IT", "IE", "AT"];
11
11
 
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
 
@@ -25,202 +25,8 @@ function getLocalIP() {
25
25
  const __dirname = dirname(__filename);
26
26
  const publicDir = join(__dirname, "public");
27
27
 
28
- function parseQuery(url) {
29
- const idx = url.indexOf("?");
30
- if (idx === -1) return { path: url, params: {} };
31
- const params = {};
32
- for (const kv of url.slice(idx + 1).split("&")) {
33
- const [k, v] = kv.split("=");
34
- params[decodeURIComponent(k)] = decodeURIComponent(v || "");
35
- }
36
- return { path: url.slice(0, idx), params };
37
- }
38
-
39
- function computeStats(users) {
40
- const total = users.length;
41
- const statusCounts = {
42
- pending: 0,
43
- processing: 0,
44
- done: 0,
45
- error: 0,
46
- restricted: 0,
47
- };
48
- for (const u of users) {
49
- statusCounts[u.status] = (statusCounts[u.status] || 0) + 1;
50
- }
51
-
52
- const targetUsers = users.filter(
53
- (u) =>
54
- u.ttSeller &&
55
- u.verified === false &&
56
- TARGET_LOCATIONS.includes(u.locationCreated),
57
- ).length;
58
-
59
- const countryMap = {};
60
- for (const u of users) {
61
- if (u.status !== "done") continue;
62
- const loc = u.locationCreated || "\u672a\u77e5";
63
- countryMap[loc] = (countryMap[loc] || 0) + 1;
64
- }
65
- const countryStats = Object.entries(countryMap)
66
- .map(([country, count]) => ({ country, count }))
67
- .sort((a, b) => b.count - a.count);
68
-
69
- const sourceCounts = {
70
- seed: 0,
71
- video: 0,
72
- comment: 0,
73
- guess: 0,
74
- following: 0,
75
- follower: 0,
76
- processed: 0,
77
- restricted: 0,
78
- error: 0,
79
- noVideo: 0,
80
- };
81
- for (const u of users) {
82
- if (u.status === "restricted") {
83
- sourceCounts.restricted++;
84
- continue;
85
- }
86
- if (u.status === "error") {
87
- sourceCounts.error++;
88
- continue;
89
- }
90
- if (u.noVideo) sourceCounts.noVideo++;
91
- const sources = u.sources || [];
92
- if (u.status === "done") sourceCounts.processed++;
93
- if (sources.includes("video") && u.status !== "done") sourceCounts.video++;
94
- if (sources.includes("comment") && u.status !== "done")
95
- sourceCounts.comment++;
96
- if (sources.includes("guess") && u.status !== "done") sourceCounts.guess++;
97
- if (sources.includes("following") && u.status !== "done")
98
- sourceCounts.following++;
99
- if (sources.includes("follower") && u.status !== "done")
100
- sourceCounts.follower++;
101
- if (
102
- !sources.includes("video") &&
103
- !sources.includes("comment") &&
104
- !sources.includes("guess") &&
105
- !sources.includes("following") &&
106
- !sources.includes("follower") &&
107
- u.status !== "done"
108
- )
109
- sourceCounts.seed++;
110
- }
111
-
112
- return {
113
- totalUsers: total,
114
- processedUsers: statusCounts.done,
115
- pendingUsers: statusCounts.pending,
116
- processingUsers: statusCounts.processing,
117
- restrictedUsers: statusCounts.restricted,
118
- errorUsers: statusCounts.error,
119
- targetUsers,
120
- countryStats,
121
- sourceStats: sourceCounts,
122
- };
123
- }
124
-
125
28
  function computeStatsIncremental(st) {
126
- const quick = st.getStats();
127
- const all = st.getAllUsers();
128
- const total = all.length;
129
- const statusCounts = quick.statusCounts;
130
-
131
- const countryMap = {};
132
- const sourceCounts = {
133
- seed: 0,
134
- video: 0,
135
- comment: 0,
136
- guess: 0,
137
- following: 0,
138
- follower: 0,
139
- processed: 0,
140
- restricted: 0,
141
- error: 0,
142
- noVideo: 0,
143
- };
144
- let targetUsers = 0;
145
- let userUpdateTasks = 0;
146
- const targetCountryMap = {};
147
-
148
- for (const u of all) {
149
- // 国家统计
150
- if (u.status === "done") {
151
- const loc = u.locationCreated || "未知";
152
- countryMap[loc] = (countryMap[loc] || 0) + 1;
153
- }
154
- // 预处理任务统计(与 /api/user-update-tasks 条件一致,不做 continue 跳过)
155
- const ttSellerEmpty =
156
- u.ttSeller === null || u.ttSeller === undefined || u.ttSeller === "";
157
- const updateCountNotSet =
158
- u.userUpdateCount === null ||
159
- u.userUpdateCount === undefined ||
160
- u.userUpdateCount <= 0;
161
- if (ttSellerEmpty && updateCountNotSet) userUpdateTasks++;
162
-
163
- // 目标用户统计(按国家分组)
164
- if (
165
- u.ttSeller &&
166
- u.verified === false &&
167
- TARGET_LOCATIONS.includes(u.locationCreated)
168
- ) {
169
- targetUsers++;
170
- const loc = u.locationCreated;
171
- targetCountryMap[loc] = (targetCountryMap[loc] || 0) + 1;
172
- }
173
-
174
- // 来源统计(restricted/error 跳过后续统计)
175
- if (u.status === "restricted") {
176
- sourceCounts.restricted++;
177
- continue;
178
- }
179
- if (u.status === "error") {
180
- sourceCounts.error++;
181
- continue;
182
- }
183
- if (u.noVideo) sourceCounts.noVideo++;
184
- const sources = u.sources || [];
185
- if (u.status === "done") sourceCounts.processed++;
186
- if (sources.includes("video") && u.status !== "done") sourceCounts.video++;
187
- if (sources.includes("comment") && u.status !== "done")
188
- sourceCounts.comment++;
189
- if (sources.includes("guess") && u.status !== "done") sourceCounts.guess++;
190
- if (sources.includes("following") && u.status !== "done")
191
- sourceCounts.following++;
192
- if (sources.includes("follower") && u.status !== "done")
193
- sourceCounts.follower++;
194
- if (
195
- !sources.includes("video") &&
196
- !sources.includes("comment") &&
197
- !sources.includes("guess") &&
198
- !sources.includes("following") &&
199
- !sources.includes("follower") &&
200
- u.status !== "done"
201
- )
202
- sourceCounts.seed++;
203
- }
204
- const countryStats = Object.entries(countryMap)
205
- .map(([country, count]) => ({ country, count }))
206
- .sort((a, b) => b.count - a.count);
207
- const targetCountryStats = Object.entries(targetCountryMap)
208
- .map(([country, count]) => ({ country, count }))
209
- .sort((a, b) => b.count - a.count);
210
-
211
- return {
212
- totalUsers: total,
213
- processedUsers: statusCounts.done,
214
- pendingUsers: statusCounts.pending,
215
- processingUsers: statusCounts.processing,
216
- restrictedUsers: statusCounts.restricted,
217
- errorUsers: statusCounts.error,
218
- targetUsers,
219
- userUpdateTasks,
220
- targetCountryStats,
221
- countryStats,
222
- sourceStats: sourceCounts,
223
- };
29
+ return st.getDashboardStats(TARGET_LOCATIONS);
224
30
  }
225
31
 
226
32
  function readBody(req) {
@@ -238,6 +44,14 @@ function readBody(req) {
238
44
  });
239
45
  }
240
46
 
47
+ function parseQuery(urlString = "/") {
48
+ const url = new URL(urlString, "http://127.0.0.1");
49
+ return {
50
+ path: url.pathname,
51
+ params: Object.fromEntries(url.searchParams.entries()),
52
+ };
53
+ }
54
+
241
55
  function sendJSON(res, code, data) {
242
56
  res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
243
57
  res.end(JSON.stringify(data));
@@ -260,9 +74,9 @@ function sendCSV(res, columns, rows) {
260
74
  res.end(body);
261
75
  }
262
76
 
263
- export function startWatchServer(outputFile, port = 3000, existingStore) {
77
+ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
264
78
  return new Promise((_resolve, reject) => {
265
- const store = existingStore || createStore(outputFile);
79
+ const store = existingStore || createStore(dataAnchor);
266
80
 
267
81
  function logJob(action, detail) {
268
82
  const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
@@ -288,12 +102,9 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
288
102
  return;
289
103
  }
290
104
  const userSources = sources || ["seed"];
291
- const existingIds = new Set(
292
- store.getAllUsers().map((u) => u.uniqueId),
293
- );
294
105
  const newUsers = usernames
295
106
  .map((u) => u.replace(/^@/, "").trim())
296
- .filter((u) => u && !existingIds.has(u));
107
+ .filter((u) => u && !store.userExists(u));
297
108
  for (const nu of newUsers) {
298
109
  store.addUser({
299
110
  uniqueId: nu,
@@ -302,7 +113,6 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
302
113
  guessedLocation: guessedLocation || null,
303
114
  });
304
115
  }
305
- store.save();
306
116
  sendJSON(res, 200, {
307
117
  added: newUsers.length,
308
118
  skipped: usernames.length - newUsers.length,
@@ -330,7 +140,6 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
330
140
  return;
331
141
  }
332
142
  store.addUser(userData);
333
- store.save();
334
143
  sendJSON(res, 200, { added: true, uniqueId: userData.uniqueId });
335
144
  } catch (e) {
336
145
  sendJSON(res, 400, { error: e.message });
@@ -355,7 +164,6 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
355
164
  loggedIn,
356
165
  );
357
166
  if (job) {
358
- store.save();
359
167
  logJob("CLAIM", {
360
168
  user: job.uniqueId,
361
169
  clientId: userId,
@@ -442,6 +250,42 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
442
250
  return;
443
251
  }
444
252
 
253
+ if (req.method === "GET" && routePath === "/api/videos") {
254
+ const result = store.getVideosPage
255
+ ? store.getVideosPage(params.limit, params.offset)
256
+ : {
257
+ total: store.getVideoCount ? store.getVideoCount() : 0,
258
+ limit: Math.max(1, Math.min(200, parseInt(params.limit) || 50)),
259
+ offset: Math.max(0, parseInt(params.offset) || 0),
260
+ videos: store.getVideos
261
+ ? store
262
+ .getVideos()
263
+ .slice(
264
+ Math.max(0, parseInt(params.offset) || 0),
265
+ Math.max(0, parseInt(params.offset) || 0) +
266
+ Math.max(
267
+ 1,
268
+ Math.min(200, parseInt(params.limit) || 50),
269
+ ),
270
+ )
271
+ : [],
272
+ };
273
+ sendJSON(res, 200, result);
274
+ return;
275
+ }
276
+
277
+ const videoMatch = routePath.match(/^\/api\/videos\/([^/]+)$/);
278
+ if (req.method === "GET" && videoMatch) {
279
+ const videoId = videoMatch[1];
280
+ const video = store.getVideo ? store.getVideo(videoId) : null;
281
+ if (!video) {
282
+ sendJSON(res, 404, { error: "video not found" });
283
+ return;
284
+ }
285
+ sendJSON(res, 200, { video });
286
+ return;
287
+ }
288
+
445
289
  // 视频登记
446
290
  if (req.method === "POST" && routePath === "/api/videos") {
447
291
  const body = await readBody(req);
@@ -478,7 +322,6 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
478
322
  const userId = params.userId || "";
479
323
  const job = store.getNextRedoJob(userId);
480
324
  if (job) {
481
- store.save();
482
325
  logJob("REDO-CLAIM", { user: job.uniqueId, clientId: userId });
483
326
  sendJSON(res, 200, { hasJob: true, user: job });
484
327
  } else {
@@ -601,13 +444,8 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
601
444
  }
602
445
 
603
446
  if (req.method === "GET" && routePath === "/api/target-users") {
604
- const all = store.getAllUsers();
605
- const targets = all.filter(
606
- (u) =>
607
- u.ttSeller &&
608
- u.verified === false &&
609
- TARGET_LOCATIONS.includes(u.locationCreated),
610
- );
447
+ const targetResult = store.getTargetUsers(TARGET_LOCATIONS);
448
+ const targets = targetResult.users;
611
449
  if (req.headers["accept"]?.includes("text/csv")) {
612
450
  const columns = [
613
451
  "uniqueId",
@@ -679,168 +517,19 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
679
517
  }
680
518
 
681
519
  if (req.method === "GET" && routePath === "/api/users") {
682
- const all = store.getAllUsers();
683
- const limit = parseInt(params.limit) || 50;
684
- const offset = parseInt(params.offset) || 0;
685
-
686
- // 简单筛选:直接用预分组索引(已排序,免全量遍历)
687
- if (
688
- !params.search &&
689
- !params.target &&
690
- !params.location &&
691
- !params.targetLocation
692
- ) {
693
- const groups = store.getStatusGroups();
694
- if (params.status && params.status !== "all") {
695
- // 单状态快路径:直接取已排序的组
696
- const group = groups[params.status] || [];
697
- const paged = group.slice(offset, offset + limit);
698
- sendJSON(res, 200, { total: group.length, users: paged });
699
- return;
700
- }
701
- // status=all 快路径:按分组顺序 early-exit(各组已排序)
702
- const sOrder = {
703
- processing: 0,
704
- pending: 1,
705
- done: 2,
706
- error: 3,
707
- restricted: 4,
708
- };
709
- const sortedKeys = Object.keys(groups).sort(
710
- (a, b) => (sOrder[a] ?? 9) - (sOrder[b] ?? 9),
711
- );
712
- let totalCount = 0;
713
- for (const key of sortedKeys) totalCount += groups[key].length;
714
- const result = [];
715
- outer: for (const key of sortedKeys) {
716
- for (const u of groups[key]) {
717
- result.push(u);
718
- if (result.length >= offset + limit) break outer;
719
- }
720
- }
721
- const paged = result.slice(offset, offset + limit);
722
- sendJSON(res, 200, { total: totalCount, users: paged });
723
- return;
724
- }
725
-
726
- let filtered = all;
727
- if (params.status && params.status !== "all") {
728
- filtered = filtered.filter((u) => u.status === params.status);
729
- }
730
- if (params.target === "1") {
731
- filtered = filtered.filter(
732
- (u) =>
733
- u.ttSeller &&
734
- u.verified === false &&
735
- TARGET_LOCATIONS.includes(u.locationCreated),
736
- );
737
- }
738
- if (params.search) {
739
- const s = params.search.toLowerCase();
740
- filtered = filtered.filter(
741
- (u) =>
742
- u.uniqueId.toLowerCase().includes(s) ||
743
- (u.nickname || "").toLowerCase().includes(s),
744
- );
745
- }
746
- if (params.location) {
747
- filtered = filtered.filter(
748
- (u) => u.locationCreated === params.location,
749
- );
750
- }
751
- if (params.targetLocation) {
752
- filtered = filtered.filter(
753
- (u) =>
754
- u.ttSeller &&
755
- u.verified === false &&
756
- u.locationCreated === params.targetLocation,
757
- );
758
- }
759
-
760
- const needCount = offset + limit;
761
- const statusOrder = {
762
- processing: 0,
763
- pending: 1,
764
- done: 2,
765
- error: 3,
766
- restricted: 4,
767
- };
768
- const tier1Loc = new Set(["PL", "NL", "BE", "AT"]);
769
- const tier2Loc = new Set(["DE", "FR", "IT", "IE", "ES"]);
770
- function locationTier(u) {
771
- const loc = (u.guessedLocation || "").toUpperCase();
772
- if (tier1Loc.has(loc)) return 0;
773
- if (tier2Loc.has(loc)) return 1;
774
- return 2;
775
- }
776
-
777
- let sorted;
778
- if (filtered.length > needCount * 3) {
779
- const groups = {};
780
- for (const u of filtered) {
781
- const key = u.status || "pending";
782
- if (!groups[key]) groups[key] = [];
783
- groups[key].push(u);
784
- }
785
- const sortedKeys = Object.keys(groups).sort(
786
- (a, b) => (statusOrder[a] ?? 9) - (statusOrder[b] ?? 9),
787
- );
788
- for (const key of sortedKeys) {
789
- const g = groups[key];
790
- if (key === "done")
791
- g.sort((a, b) => (b.processedAt || 0) - (a.processedAt || 0));
792
- else if (key === "pending")
793
- g.sort((a, b) => {
794
- const aSeller =
795
- a.ttSeller === true && a.verified === false ? 0 : 1;
796
- const bSeller =
797
- b.ttSeller === true && b.verified === false ? 0 : 1;
798
- if (aSeller !== bSeller) return aSeller - bSeller;
799
- const la = locationTier(a),
800
- lb = locationTier(b);
801
- if (la !== lb) return la - lb;
802
- return (b.followerCount || 0) - (a.followerCount || 0);
803
- });
804
- else
805
- g.sort((a, b) => (b.followerCount || 0) - (a.followerCount || 0));
806
- const pinned = g.filter((u) => u.pinned);
807
- const unpinned = g.filter((u) => !u.pinned);
808
- groups[key] = pinned.concat(unpinned);
809
- }
810
- sorted = [];
811
- outer: for (const key of sortedKeys) {
812
- for (const u of groups[key]) {
813
- sorted.push(u);
814
- if (sorted.length >= needCount) break outer;
815
- }
816
- }
817
- } else {
818
- sorted = filtered.slice().sort((a, b) => {
819
- if (a.pinned && !b.pinned) return -1;
820
- if (!a.pinned && b.pinned) return 1;
821
- const sa = statusOrder[a.status] ?? 9;
822
- const sb = statusOrder[b.status] ?? 9;
823
- if (sa !== sb) return sa - sb;
824
- if (a.status === "done" && b.status === "done")
825
- return (b.processedAt || 0) - (a.processedAt || 0);
826
- if (a.status === "pending" && b.status === "pending") {
827
- const aSeller =
828
- a.ttSeller === true && a.verified === false ? 0 : 1;
829
- const bSeller =
830
- b.ttSeller === true && b.verified === false ? 0 : 1;
831
- if (aSeller !== bSeller) return aSeller - bSeller;
832
- const la = locationTier(a),
833
- lb = locationTier(b);
834
- if (la !== lb) return la - lb;
835
- }
836
- return (b.followerCount || 0) - (a.followerCount || 0);
837
- });
838
- }
839
-
840
- let paged = sorted.slice(offset, offset + limit);
520
+ const result = store.getUsersPage({
521
+ status: params.status,
522
+ search: params.search,
523
+ location: params.location,
524
+ target: params.target,
525
+ targetLocation: params.targetLocation,
526
+ limit: params.limit,
527
+ offset: params.offset,
528
+ targetLocations: TARGET_LOCATIONS,
529
+ });
841
530
 
842
531
  if (params.view === "light") {
843
- paged = paged.map((u) => ({
532
+ result.users = result.users.map((u) => ({
844
533
  uniqueId: u.uniqueId,
845
534
  nickname: u.nickname,
846
535
  status: u.status,
@@ -855,7 +544,7 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
855
544
  }));
856
545
  }
857
546
 
858
- sendJSON(res, 200, { total: filtered.length, users: paged });
547
+ sendJSON(res, 200, result);
859
548
  return;
860
549
  }
861
550