@swimmingliu/autovpn 2.0.2 → 2.1.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/dist/cli/accounts-commands.js +123 -7
- package/dist/cli/output.js +1 -1
- package/dist/config/profile.js +40 -9
- package/dist/pipeline/availability.js +201 -150
- package/dist/pipeline/concurrency.js +68 -0
- package/dist/pipeline/credential-source.js +490 -99
- package/dist/pipeline/egress.js +372 -0
- package/dist/pipeline/extract.js +108 -69
- package/dist/pipeline/http-transport.js +232 -0
- package/dist/pipeline/orchestrator.js +34 -8
- package/dist/pipeline/proxy-runtime.js +35 -2
- package/dist/pipeline/run-store.js +39 -0
- package/dist/pipeline/speedtest.js +187 -144
- package/dist/pool/account-store.js +46 -4
- package/dist/server/http.js +11 -0
- package/dist/server/runtime.js +42 -2
- package/dist/server/web-adapter.js +12 -0
- package/dist/web/renderer/app.js +143 -13
- package/dist/web/renderer/i18n.js +1 -1
- package/dist/web/renderer/styles.css +37 -2
- package/dist/web/renderer/tables.js +2 -2
- package/dist/web/renderer/views.js +156 -20
- package/dist/web/renderer/workspace.css +5 -0
- package/package.json +1 -1
|
@@ -172,6 +172,18 @@ export function renderWebAdapterScript(options = {}) {
|
|
|
172
172
|
method: 'POST',
|
|
173
173
|
body: JSON.stringify(payload || {})
|
|
174
174
|
}),
|
|
175
|
+
// The H5 build reaches the same expiry editor the Electron preload
|
|
176
|
+
// exposes; without this the browser UI hides the affordance entirely.
|
|
177
|
+
// Accepts both shapes the renderer may use: (id, date) like the preload
|
|
178
|
+
// bridge, or ({id, expires_at}).
|
|
179
|
+
setAccountExpiry: async (idOrPayload, date) => request('/api/accounts/expiry', {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
body: JSON.stringify(
|
|
182
|
+
idOrPayload && typeof idOrPayload === 'object'
|
|
183
|
+
? idOrPayload
|
|
184
|
+
: { id: idOrPayload, expires_at: date }
|
|
185
|
+
)
|
|
186
|
+
}),
|
|
175
187
|
accountsTemplate: async () => ({ ok: true, content: ${JSON.stringify(accountsTemplate())} }),
|
|
176
188
|
runPipeline: async (options = {}) => request('/api/runs', {
|
|
177
189
|
method: 'POST',
|
package/dist/web/renderer/app.js
CHANGED
|
@@ -9,8 +9,10 @@ import { renderAnalytics, clearAnalytics } from './analytics.bundle.js';
|
|
|
9
9
|
import {
|
|
10
10
|
addAvailabilityTargetDraft,
|
|
11
11
|
applyAvailabilityTargetDraft,
|
|
12
|
+
applyNetworkDraft,
|
|
12
13
|
applySourceIterationDraft,
|
|
13
14
|
buildAvailabilityTargetDraft,
|
|
15
|
+
buildNetworkDraft,
|
|
14
16
|
buildPageMarkup,
|
|
15
17
|
buildDashboardMetricsMarkup,
|
|
16
18
|
buildDashboardStateMarkup,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
buildAccountPoolRecords,
|
|
26
28
|
removeAvailabilityTargetDraft,
|
|
27
29
|
resolveDashboardRunSummary,
|
|
30
|
+
SOURCE_ROUND_DEFAULTS,
|
|
28
31
|
validateSourceDraft,
|
|
29
32
|
classifyLogEntry,
|
|
30
33
|
escapeHtml,
|
|
@@ -36,10 +39,10 @@ initializeAppearance();
|
|
|
36
39
|
|
|
37
40
|
const demoProfile = {
|
|
38
41
|
sources: {
|
|
39
|
-
leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true,
|
|
40
|
-
heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true,
|
|
41
|
-
mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true,
|
|
42
|
-
xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true,
|
|
42
|
+
leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
43
|
+
heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
44
|
+
mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
45
|
+
xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true, ...SOURCE_ROUND_DEFAULTS }
|
|
43
46
|
},
|
|
44
47
|
speed_test: {
|
|
45
48
|
min_download_mb_s: 1.0,
|
|
@@ -114,7 +117,9 @@ const state = {
|
|
|
114
117
|
statusFilter: 'all',
|
|
115
118
|
brandFilter: 'all',
|
|
116
119
|
sortBy: 'brand',
|
|
117
|
-
busy: false
|
|
120
|
+
busy: false,
|
|
121
|
+
canSetExpiry: typeof window.vpnAutomation?.setAccountExpiry === 'function',
|
|
122
|
+
expiryEditorId: null
|
|
118
123
|
},
|
|
119
124
|
isDemo: false,
|
|
120
125
|
runtime: document.body.dataset.runtime === 'web' ? 'web' : 'electron',
|
|
@@ -138,7 +143,8 @@ const state = {
|
|
|
138
143
|
qr: createQrState(),
|
|
139
144
|
runStartedAt: null,
|
|
140
145
|
lastUpdateAt: null,
|
|
141
|
-
modalTransform: ''
|
|
146
|
+
modalTransform: '',
|
|
147
|
+
progressSnapshot: null
|
|
142
148
|
};
|
|
143
149
|
|
|
144
150
|
let settingsDrawerOpener = null;
|
|
@@ -197,7 +203,10 @@ function hydrateInitialRuntimeState(loadedState = {}) {
|
|
|
197
203
|
const loadedProfile = loadedState.profile ?? {};
|
|
198
204
|
state.activity = Array.isArray(loadedState.activity) ? loadedState.activity : [];
|
|
199
205
|
state.activityUnavailable = Number(loadedState.activityUnavailable ?? 0);
|
|
200
|
-
|
|
206
|
+
// An older profile has no [network] section. Seeding the displayed defaults
|
|
207
|
+
// here means saving any other drawer writes the section the pipeline reads
|
|
208
|
+
// instead of leaving it absent, and the network drawer opens on real values.
|
|
209
|
+
state.profile = { ...loadedProfile, network: buildNetworkDraft(loadedProfile.network) };
|
|
201
210
|
state.savedProfile = structuredClone(loadedProfile);
|
|
202
211
|
const nextRunState = String(loadedState.runState ?? '');
|
|
203
212
|
if (['idle', 'running', 'stopping', 'failed', 'success'].includes(nextRunState)) {
|
|
@@ -222,11 +231,13 @@ function hydrateHistoricalEvents(events = []) {
|
|
|
222
231
|
}
|
|
223
232
|
const previousRunState = state.runState;
|
|
224
233
|
const previousRunResult = state.runResult;
|
|
234
|
+
const snapshot = state.progressSnapshot;
|
|
225
235
|
for (const event of events) {
|
|
226
236
|
handlePipelineEvent(event, { historical: true });
|
|
227
237
|
}
|
|
228
238
|
state.runState = previousRunState;
|
|
229
239
|
state.runResult = previousRunState === 'idle' ? previousRunResult : previousRunState;
|
|
240
|
+
if (snapshot) applyProgressSnapshot(snapshot);
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
function bindActions() {
|
|
@@ -639,6 +650,10 @@ function handleDocumentInput(event) {
|
|
|
639
650
|
const floor = SOURCE_MINIMUM_ONE_KEYS.has(key) ? 1 : 0;
|
|
640
651
|
sourceDraft[sourceName][key] = Math.max(floor, Math.trunc(parsed));
|
|
641
652
|
}
|
|
653
|
+
// The area controls have a hard ceiling the backend also clamps to.
|
|
654
|
+
if (SOURCE_MAXIMUM_KEYS.has(key)) {
|
|
655
|
+
sourceDraft[sourceName][key] = Math.min(SOURCE_MAXIMUM_KEYS.get(key), sourceDraft[sourceName][key]);
|
|
656
|
+
}
|
|
642
657
|
}
|
|
643
658
|
return;
|
|
644
659
|
}
|
|
@@ -790,6 +805,8 @@ const SOURCE_NUMERIC_KEYS = new Set([
|
|
|
790
805
|
'max_iterations',
|
|
791
806
|
'min_iterations',
|
|
792
807
|
'plateau_limit',
|
|
808
|
+
'areas_per_round',
|
|
809
|
+
'area_concurrency',
|
|
793
810
|
'max_runtime_seconds',
|
|
794
811
|
'max_areas',
|
|
795
812
|
'area_min',
|
|
@@ -799,7 +816,15 @@ const SOURCE_NUMERIC_KEYS = new Set([
|
|
|
799
816
|
/** Of those, the ones the runner clamps to at least one. */
|
|
800
817
|
const SOURCE_MINIMUM_ONE_KEYS = new Set([
|
|
801
818
|
'max_iterations',
|
|
802
|
-
'plateau_limit'
|
|
819
|
+
'plateau_limit',
|
|
820
|
+
'areas_per_round',
|
|
821
|
+
'area_concurrency'
|
|
822
|
+
]);
|
|
823
|
+
|
|
824
|
+
/** Numeric source fields with a hard upper bound, matching the backend clamp. */
|
|
825
|
+
const SOURCE_MAXIMUM_KEYS = new Map([
|
|
826
|
+
['areas_per_round', 64],
|
|
827
|
+
['area_concurrency', 16]
|
|
803
828
|
]);
|
|
804
829
|
|
|
805
830
|
function resolveErrorMessage(error) {
|
|
@@ -1034,6 +1059,7 @@ function buildSettingsDraft(section) {
|
|
|
1034
1059
|
if (section === 'speed_test') return buildSpeedTestDraft(state.profile.speed_test);
|
|
1035
1060
|
if (section === 'availability_targets') return buildAvailabilityTargetDraft(state.profile.availability_targets);
|
|
1036
1061
|
if (section === 'deploy') return buildDeployDraft(state.profile.deploy);
|
|
1062
|
+
if (section === 'network') return buildNetworkDraft(state.profile.network);
|
|
1037
1063
|
if (section === 'paths') return structuredClone(state.profile.paths);
|
|
1038
1064
|
if (section === 'about') return { version: getMessages(state.language).sidebarVersion };
|
|
1039
1065
|
return null;
|
|
@@ -1088,6 +1114,9 @@ async function saveSettingsDrawer() {
|
|
|
1088
1114
|
|
|
1089
1115
|
async function hydrateAccountPool() {
|
|
1090
1116
|
state.accountPool.status = 'loading';
|
|
1117
|
+
// Re-detect each hydration: the preload bridge is fixed for the page, but a
|
|
1118
|
+
// test harness or a future H5 adapter may attach it after bootstrap.
|
|
1119
|
+
state.accountPool.canSetExpiry = typeof window.vpnAutomation?.setAccountExpiry === 'function';
|
|
1091
1120
|
touchUpdate();
|
|
1092
1121
|
renderAccountPoolPanelOnly();
|
|
1093
1122
|
if (!window.vpnAutomation?.loadAccounts) {
|
|
@@ -1151,9 +1180,56 @@ async function handleAccountPoolAction(button) {
|
|
|
1151
1180
|
}
|
|
1152
1181
|
if (action === 'toggle') {
|
|
1153
1182
|
await toggleAccountStatus(Number(button.dataset.accountId));
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (action === 'expiry-edit') {
|
|
1186
|
+
state.accountPool.expiryEditorId = Number(button.dataset.accountId);
|
|
1187
|
+
renderAll();
|
|
1188
|
+
queueMicrotask(() => document.querySelector(`[data-account-expiry-input="${CSS.escape(String(button.dataset.accountId))}"]`)?.focus());
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (action === 'expiry-cancel') {
|
|
1192
|
+
state.accountPool.expiryEditorId = null;
|
|
1193
|
+
renderAll();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (action === 'expiry-save') {
|
|
1197
|
+
const id = Number(button.dataset.accountId);
|
|
1198
|
+
const input = document.querySelector(`[data-account-expiry-input="${CSS.escape(String(id))}"]`);
|
|
1199
|
+
await setAccountExpiry(id, input?.value ?? '');
|
|
1154
1200
|
}
|
|
1155
1201
|
}
|
|
1156
1202
|
|
|
1203
|
+
async function setAccountExpiry(id, expiresAt) {
|
|
1204
|
+
const account = (state.accountPool.accounts ?? []).find((item) => item.id === id);
|
|
1205
|
+
// Feature-detected: the browser build has no preload bridge, so this path is
|
|
1206
|
+
// never reachable there and the row hides the affordance.
|
|
1207
|
+
if (!account || typeof window.vpnAutomation?.setAccountExpiry !== 'function') {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const value = String(expiresAt ?? '').trim();
|
|
1211
|
+
if (value && !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
1212
|
+
showToast({ tone: 'danger', message: '请选择有效的到期日期' });
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
state.accountPool.busy = true;
|
|
1216
|
+
renderAccountPoolPanelOnly();
|
|
1217
|
+
try {
|
|
1218
|
+
// Preload bridge signature: setAccountExpiry(id, 'YYYY-MM-DD'); '' clears it.
|
|
1219
|
+
const result = await window.vpnAutomation.setAccountExpiry(id, value);
|
|
1220
|
+
if (!result?.ok) {
|
|
1221
|
+
throw new Error(result?.error || '操作失败');
|
|
1222
|
+
}
|
|
1223
|
+
state.accountPool.expiryEditorId = null;
|
|
1224
|
+
showToast({ tone: 'success', message: value ? `${account.account_masked} 到期日已设为 ${value}` : `${account.account_masked} 到期日已清除` });
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
showToast({ tone: 'danger', message: resolveErrorMessage(error) });
|
|
1227
|
+
} finally {
|
|
1228
|
+
state.accountPool.busy = false;
|
|
1229
|
+
}
|
|
1230
|
+
await hydrateAccountPool();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1157
1233
|
async function downloadAccountsTemplate() {
|
|
1158
1234
|
try {
|
|
1159
1235
|
let content = '';
|
|
@@ -1288,6 +1364,9 @@ function resolveSettingsDraftPayload(section, draft) {
|
|
|
1288
1364
|
if (section === 'deploy') {
|
|
1289
1365
|
return sanitizeDeployDraft(draft);
|
|
1290
1366
|
}
|
|
1367
|
+
if (section === 'network') {
|
|
1368
|
+
return applyNetworkDraft(draft);
|
|
1369
|
+
}
|
|
1291
1370
|
return structuredClone(draft);
|
|
1292
1371
|
}
|
|
1293
1372
|
|
|
@@ -1554,7 +1633,24 @@ function finishRun(result = {}) {
|
|
|
1554
1633
|
void hydrateRetryArtifacts();
|
|
1555
1634
|
}
|
|
1556
1635
|
|
|
1636
|
+
function applyProgressSnapshot(snapshot) {
|
|
1637
|
+
if (!snapshot?.counts || !snapshot.source_counts) return;
|
|
1638
|
+
state.progressSnapshot = snapshot;
|
|
1639
|
+
state.counts = normalizeCounts({ ...state.counts, ...snapshot.counts });
|
|
1640
|
+
state.sourceCounts = normalizeSourceCounts(snapshot.source_counts);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1557
1643
|
function handlePipelineEvent(event, options = {}) {
|
|
1644
|
+
handlePipelineEventDetails(event, options);
|
|
1645
|
+
if (event.progress_snapshot && !options.historical
|
|
1646
|
+
&& (!event.progress_snapshot.artifact_dir || event.progress_snapshot.artifact_dir === state.artifactDir)) {
|
|
1647
|
+
applyProgressSnapshot(event.progress_snapshot);
|
|
1648
|
+
touchUpdate();
|
|
1649
|
+
renderRuntimeOnly({ chrome: false });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function handlePipelineEventDetails(event, options = {}) {
|
|
1558
1654
|
const historical = Boolean(options.historical);
|
|
1559
1655
|
if (event.type === 'server_state') {
|
|
1560
1656
|
const nextRunState = String(event.run_state ?? '');
|
|
@@ -1673,6 +1769,24 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1673
1769
|
return;
|
|
1674
1770
|
}
|
|
1675
1771
|
|
|
1772
|
+
if (event.type === 'extract_platform_started') {
|
|
1773
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}开始提取,最多 ${event.requested_iterations ?? 0} 次,最少 ${event.min_iterations ?? 0} 次`, {
|
|
1774
|
+
kind: 'stage',
|
|
1775
|
+
stage: 'extract',
|
|
1776
|
+
level: 'info'
|
|
1777
|
+
});
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (event.type === 'extract_platform_completed') {
|
|
1782
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}完成,成功 ${event.successful_iterations ?? 0} 次,失败 ${event.failed_iterations ?? 0} 次,原始节点 ${event.raw_links ?? 0} 个`, {
|
|
1783
|
+
kind: 'stage',
|
|
1784
|
+
stage: 'extract',
|
|
1785
|
+
level: 'info'
|
|
1786
|
+
});
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1676
1790
|
if (event.type === 'extract_source_started') {
|
|
1677
1791
|
appendLog(`[extract] ${event.source_name} 开始提取,最多 ${event.requested_iterations ?? 0} 次,最少 ${event.min_iterations ?? 0} 次`, {
|
|
1678
1792
|
kind: 'stage',
|
|
@@ -1685,7 +1799,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1685
1799
|
if (event.type === 'extract_request_result') {
|
|
1686
1800
|
const status = event.success ? '成功' : '失败';
|
|
1687
1801
|
const retry = event.will_retry ? ',将重试' : '';
|
|
1688
|
-
appendLog(`[extract] ${event.source_name} #${event.iteration} ${event.via} ${status}${retry}`, {
|
|
1802
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}#${event.iteration} ${event.via} ${status}${retry}`, {
|
|
1689
1803
|
kind: 'stage',
|
|
1690
1804
|
stage: 'extract',
|
|
1691
1805
|
level: event.success ? 'info' : 'warning'
|
|
@@ -1694,7 +1808,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1694
1808
|
}
|
|
1695
1809
|
|
|
1696
1810
|
if (event.type === 'extract_decrypt_result' && !event.success) {
|
|
1697
|
-
appendLog(`[extract] ${event.source_name} #${event.iteration} 解密失败`, {
|
|
1811
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}#${event.iteration} 解密失败`, {
|
|
1698
1812
|
kind: 'stage',
|
|
1699
1813
|
stage: 'extract',
|
|
1700
1814
|
level: 'error'
|
|
@@ -1703,7 +1817,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1703
1817
|
}
|
|
1704
1818
|
|
|
1705
1819
|
if (event.type === 'extract_source_completed') {
|
|
1706
|
-
updateExtractMetrics({ source_name: event.source_name, total_links: event.raw_links });
|
|
1820
|
+
if (!event.progress_snapshot && !(historical && state.progressSnapshot)) updateExtractMetrics({ source_name: event.source_name, total_links: event.raw_links });
|
|
1707
1821
|
appendLog(`[extract] ${event.source_name} 完成,成功 ${event.successful_iterations ?? 0} 次,失败 ${event.failed_iterations ?? 0} 次,原始节点 ${event.raw_links ?? 0} 个`, {
|
|
1708
1822
|
kind: 'stage',
|
|
1709
1823
|
stage: 'extract',
|
|
@@ -1713,8 +1827,11 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1713
1827
|
}
|
|
1714
1828
|
|
|
1715
1829
|
if (event.type === 'extract_iteration') {
|
|
1716
|
-
updateExtractMetrics(event);
|
|
1717
|
-
|
|
1830
|
+
if (!event.progress_snapshot && !(historical && state.progressSnapshot)) updateExtractMetrics(event);
|
|
1831
|
+
// Android and iOS run the same round numbers independently, so a bare `#N`
|
|
1832
|
+
// shows two identical lines. The tag only appears when the event carries a
|
|
1833
|
+
// platform, which keeps older backends and the URL-mode stream unchanged.
|
|
1834
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}#${event.iteration ?? 0} 新增 ${event.new_items ?? 0} 个,本次解析 ${event.extracted_links ?? 0} 个,累计 ${event.total_links ?? 0} 个`, {
|
|
1718
1835
|
kind: 'stage',
|
|
1719
1836
|
stage: 'extract',
|
|
1720
1837
|
level: 'info'
|
|
@@ -1723,6 +1840,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1723
1840
|
}
|
|
1724
1841
|
|
|
1725
1842
|
if (event.type === 'speedtest_result') {
|
|
1843
|
+
if (event.progress_snapshot || (historical && state.progressSnapshot)) return;
|
|
1726
1844
|
if (event.passed_threshold) {
|
|
1727
1845
|
state.counts.speedtest_links = Number(state.counts.speedtest_links ?? 0) + 1;
|
|
1728
1846
|
}
|
|
@@ -1732,6 +1850,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1732
1850
|
}
|
|
1733
1851
|
|
|
1734
1852
|
if (event.type === 'availability_link_result') {
|
|
1853
|
+
if (event.progress_snapshot || (historical && state.progressSnapshot)) return;
|
|
1735
1854
|
if (event.all_passed) {
|
|
1736
1855
|
state.counts.availability_links = Number(state.counts.availability_links ?? 0) + 1;
|
|
1737
1856
|
}
|
|
@@ -1770,6 +1889,7 @@ function normalizeStageStatuses(stageStatuses = {}) {
|
|
|
1770
1889
|
}
|
|
1771
1890
|
|
|
1772
1891
|
function resetRunScopedProgress() {
|
|
1892
|
+
state.progressSnapshot = null;
|
|
1773
1893
|
state.stageStatus = {};
|
|
1774
1894
|
state.counts = {};
|
|
1775
1895
|
state.sourceCounts = {};
|
|
@@ -1875,6 +1995,8 @@ function hydrateArtifactState(result) {
|
|
|
1875
1995
|
state.artifactDir = result.artifact_dir;
|
|
1876
1996
|
state.counts = normalizeCounts(result.counts ?? {});
|
|
1877
1997
|
state.sourceCounts = normalizeSourceCounts(result.source_counts ?? {});
|
|
1998
|
+
// Bounded log history cannot reconstruct metrics that predate its tail.
|
|
1999
|
+
state.progressSnapshot = result.progress_snapshot ?? null;
|
|
1878
2000
|
state.extractDedupedFingerprints = new Set();
|
|
1879
2001
|
state.outputFiles = result.outputFiles ?? [];
|
|
1880
2002
|
state.nodeRows = result.nodeRows ?? [];
|
|
@@ -1951,6 +2073,14 @@ function resolveDefaultRetryStage(artifact) {
|
|
|
1951
2073
|
return failedStage || retryableStages.at(-1) || '';
|
|
1952
2074
|
}
|
|
1953
2075
|
|
|
2076
|
+
// Both channels emit the same round numbers, so the log needs the channel on
|
|
2077
|
+
// the line. `[android] ` keeps the rest of the format untouched; an event with
|
|
2078
|
+
// no platform (older backend, URL-mode extract) renders exactly as before.
|
|
2079
|
+
function platformTag(platform) {
|
|
2080
|
+
const value = String(platform ?? '').trim().toLowerCase();
|
|
2081
|
+
return value === 'android' || value === 'ios' ? `[${value}] ` : '';
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1954
2084
|
function appendLog(message, overrides = {}) {
|
|
1955
2085
|
const logCenter = document.querySelector('#logCenterTable');
|
|
1956
2086
|
if (logCenter) {
|
|
@@ -1390,6 +1390,8 @@ body[data-page="logs"] #logCenterTable {
|
|
|
1390
1390
|
|
|
1391
1391
|
.source-credential-rounds {
|
|
1392
1392
|
display: grid;
|
|
1393
|
+
/* Four controls in a 232px column: two per line, so the labels stay readable
|
|
1394
|
+
next to a 36px number input. */
|
|
1393
1395
|
grid-template-columns: repeat(2, minmax(72px, 1fr));
|
|
1394
1396
|
gap: 8px;
|
|
1395
1397
|
}
|
|
@@ -1478,7 +1480,7 @@ body[data-page="logs"] #logCenterTable {
|
|
|
1478
1480
|
}
|
|
1479
1481
|
|
|
1480
1482
|
.settings-col-rounds {
|
|
1481
|
-
width:
|
|
1483
|
+
width: 232px;
|
|
1482
1484
|
}
|
|
1483
1485
|
|
|
1484
1486
|
.availability-col-url {
|
|
@@ -1704,13 +1706,40 @@ body[data-page="logs"] #logCenterTable {
|
|
|
1704
1706
|
}
|
|
1705
1707
|
|
|
1706
1708
|
.account-col-expiry {
|
|
1707
|
-
width:
|
|
1709
|
+
width: 178px;
|
|
1708
1710
|
}
|
|
1709
1711
|
|
|
1710
1712
|
.account-col-usage {
|
|
1711
1713
|
width: 132px;
|
|
1712
1714
|
}
|
|
1713
1715
|
|
|
1716
|
+
.account-expiry-value {
|
|
1717
|
+
display: block;
|
|
1718
|
+
font-size: 13px;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
.account-expiry-value.unknown {
|
|
1722
|
+
color: var(--text-soft);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.account-expiry-edit {
|
|
1726
|
+
margin-top: 4px;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.account-expiry-editor {
|
|
1730
|
+
display: flex;
|
|
1731
|
+
flex-wrap: wrap;
|
|
1732
|
+
align-items: center;
|
|
1733
|
+
gap: 6px;
|
|
1734
|
+
margin-top: 6px;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
.account-expiry-editor input {
|
|
1738
|
+
min-width: 132px;
|
|
1739
|
+
padding-inline: 8px;
|
|
1740
|
+
font-size: 12px;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1714
1743
|
.account-col-actions {
|
|
1715
1744
|
width: 92px;
|
|
1716
1745
|
}
|
|
@@ -2841,6 +2870,12 @@ body[data-page="logs"] #logCenterTable {
|
|
|
2841
2870
|
min-height: 44px;
|
|
2842
2871
|
}
|
|
2843
2872
|
|
|
2873
|
+
/* The expiry editor sits in that same action cell at phone widths, so its
|
|
2874
|
+
date input has to meet the same tap-target floor. */
|
|
2875
|
+
.account-expiry-editor input {
|
|
2876
|
+
min-height: 44px;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2844
2879
|
.app-frame {
|
|
2845
2880
|
display: block;
|
|
2846
2881
|
height: auto;
|
|
@@ -8,8 +8,8 @@ const TABLES = {
|
|
|
8
8
|
],
|
|
9
9
|
accounts: [
|
|
10
10
|
['account', '账号', 240, 160], ['brand', '加速器', 110, 90],
|
|
11
|
-
['status', '状态',
|
|
12
|
-
['usage', '使用',
|
|
11
|
+
['status', '状态', 150, 120], ['expiry', '到期', 180, 130],
|
|
12
|
+
['usage', '使用', 150, 120], ['actions', '操作', 96, 80]
|
|
13
13
|
]
|
|
14
14
|
};
|
|
15
15
|
const initialized = new WeakSet();
|