codex-endpoint-switcher 1.0.0 → 1.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/README.md +103 -0
- package/bin/codex-switcher.js +27 -0
- package/package.json +6 -1
- package/src/main/cloud-sync-client.js +328 -0
- package/src/main/main.js +37 -0
- package/src/main/preload.js +27 -0
- package/src/main/profile-manager.js +478 -42
- package/src/renderer/index.html +83 -0
- package/src/renderer/renderer.js +230 -6
- package/src/renderer/styles.css +45 -2
- package/src/web/cloud-sync-server.js +419 -0
- package/src/web/launcher.js +6 -1
- package/src/web/proxy-server.js +69 -0
- package/src/web/server.js +100 -2
package/src/renderer/index.html
CHANGED
|
@@ -64,6 +64,18 @@
|
|
|
64
64
|
<span class="metric-label">模型</span>
|
|
65
65
|
<strong id="currentModel">-</strong>
|
|
66
66
|
</article>
|
|
67
|
+
<article class="metric-card metric-card-wide">
|
|
68
|
+
<span class="metric-label">热更新模式</span>
|
|
69
|
+
<strong id="proxyModeStatus">未开启</strong>
|
|
70
|
+
<p class="field-hint" id="proxyModeHint">
|
|
71
|
+
当前还是直连模式。开启后,后续同一 Codex 会话可在下一次请求时热更新到新 URL/Key。
|
|
72
|
+
</p>
|
|
73
|
+
<div class="action-row metric-actions">
|
|
74
|
+
<button id="enableProxyModeButton" type="button" class="secondary-button">
|
|
75
|
+
开启热更新模式
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</article>
|
|
67
79
|
</div>
|
|
68
80
|
</section>
|
|
69
81
|
|
|
@@ -110,6 +122,77 @@
|
|
|
110
122
|
<button id="clearFormButton" type="button" class="ghost-button">清空表单</button>
|
|
111
123
|
</div>
|
|
112
124
|
</form>
|
|
125
|
+
|
|
126
|
+
<div class="stack-form stack-form-alt">
|
|
127
|
+
<div>
|
|
128
|
+
<p class="panel-kicker">Cloud Sync</p>
|
|
129
|
+
<h3>账号配置同步</h3>
|
|
130
|
+
</div>
|
|
131
|
+
<label class="field">
|
|
132
|
+
<span>同步服务器</span>
|
|
133
|
+
<input
|
|
134
|
+
id="cloudServerUrl"
|
|
135
|
+
type="text"
|
|
136
|
+
placeholder="例如:http://127.0.0.1:3190"
|
|
137
|
+
autocomplete="off"
|
|
138
|
+
/>
|
|
139
|
+
</label>
|
|
140
|
+
<label class="field">
|
|
141
|
+
<span>账号</span>
|
|
142
|
+
<input
|
|
143
|
+
id="cloudUsername"
|
|
144
|
+
type="text"
|
|
145
|
+
placeholder="例如:yourname"
|
|
146
|
+
autocomplete="off"
|
|
147
|
+
/>
|
|
148
|
+
</label>
|
|
149
|
+
<label class="field">
|
|
150
|
+
<span>密码</span>
|
|
151
|
+
<input
|
|
152
|
+
id="cloudPassword"
|
|
153
|
+
type="password"
|
|
154
|
+
placeholder="输入同步账号密码"
|
|
155
|
+
autocomplete="off"
|
|
156
|
+
/>
|
|
157
|
+
</label>
|
|
158
|
+
<p class="field-hint">
|
|
159
|
+
账号空间里保存的是你的连接配置。推送是把本机连接上传到账号,拉取是把账号里的连接同步回本机。
|
|
160
|
+
</p>
|
|
161
|
+
<div class="sync-status-grid">
|
|
162
|
+
<article class="sync-status-card">
|
|
163
|
+
<span class="metric-label">登录状态</span>
|
|
164
|
+
<strong id="cloudAuthStatus">未登录</strong>
|
|
165
|
+
</article>
|
|
166
|
+
<article class="sync-status-card">
|
|
167
|
+
<span class="metric-label">服务器</span>
|
|
168
|
+
<strong id="cloudRemoteStatus">未设置</strong>
|
|
169
|
+
</article>
|
|
170
|
+
<article class="sync-status-card sync-status-card-wide">
|
|
171
|
+
<span class="metric-label">同步记录</span>
|
|
172
|
+
<strong id="cloudLastSyncStatus">还没有推送或拉取记录</strong>
|
|
173
|
+
</article>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="action-row">
|
|
176
|
+
<button id="cloudRegisterButton" type="button" class="secondary-button">
|
|
177
|
+
注册账号
|
|
178
|
+
</button>
|
|
179
|
+
<button id="cloudLoginButton" type="button" class="primary-button">
|
|
180
|
+
登录账号
|
|
181
|
+
</button>
|
|
182
|
+
<button id="cloudLogoutButton" type="button" class="ghost-button">退出登录</button>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="action-row">
|
|
185
|
+
<button id="cloudPushButton" type="button" class="secondary-button">
|
|
186
|
+
推送当前连接
|
|
187
|
+
</button>
|
|
188
|
+
<button id="cloudPullMergeButton" type="button" class="primary-button">
|
|
189
|
+
合并拉取
|
|
190
|
+
</button>
|
|
191
|
+
<button id="cloudPullReplaceButton" type="button" class="danger-button">
|
|
192
|
+
覆盖拉取
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
113
196
|
</section>
|
|
114
197
|
|
|
115
198
|
<section class="panel profiles-panel">
|
package/src/renderer/renderer.js
CHANGED
|
@@ -2,6 +2,7 @@ const state = {
|
|
|
2
2
|
current: null,
|
|
3
3
|
endpoints: [],
|
|
4
4
|
paths: null,
|
|
5
|
+
cloud: null,
|
|
5
6
|
};
|
|
6
7
|
|
|
7
8
|
function createWebBridge() {
|
|
@@ -51,6 +52,42 @@ function createWebBridge() {
|
|
|
51
52
|
body: JSON.stringify(payload),
|
|
52
53
|
});
|
|
53
54
|
},
|
|
55
|
+
enableProxyMode() {
|
|
56
|
+
return request("/api/proxy/enable", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
getCloudSyncStatus() {
|
|
61
|
+
return request("/api/cloud/status");
|
|
62
|
+
},
|
|
63
|
+
registerCloudAccount(payload) {
|
|
64
|
+
return request("/api/cloud/register", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
loginCloudAccount(payload) {
|
|
70
|
+
return request("/api/cloud/login", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify(payload),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
logoutCloudAccount() {
|
|
76
|
+
return request("/api/cloud/logout", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
pushCloudConfig() {
|
|
81
|
+
return request("/api/cloud/push", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
pullCloudConfig(payload) {
|
|
86
|
+
return request("/api/cloud/pull", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify(payload),
|
|
89
|
+
});
|
|
90
|
+
},
|
|
54
91
|
getPaths() {
|
|
55
92
|
return request("/api/paths");
|
|
56
93
|
},
|
|
@@ -91,6 +128,19 @@ function setStatus(message, type = "info") {
|
|
|
91
128
|
statusBox.className = `status-box ${type}`;
|
|
92
129
|
}
|
|
93
130
|
|
|
131
|
+
function syncFieldValue(selector, value) {
|
|
132
|
+
const input = $(selector);
|
|
133
|
+
if (!input) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (document.activeElement === input && String(input.value || "").trim()) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
input.value = value || "";
|
|
142
|
+
}
|
|
143
|
+
|
|
94
144
|
function resetForm() {
|
|
95
145
|
$("#endpointId").value = "";
|
|
96
146
|
$("#endpointNote").value = "";
|
|
@@ -120,11 +170,64 @@ function renderCurrent() {
|
|
|
120
170
|
$("#currentBaseUrl").textContent = current.currentUrl || "-";
|
|
121
171
|
$("#currentKeyMasked").textContent = current.currentKeyMasked || "-";
|
|
122
172
|
$("#currentModel").textContent = current.model || "-";
|
|
173
|
+
$("#proxyModeStatus").textContent = current.proxyModeEnabled
|
|
174
|
+
? `已开启,固定代理:${current.proxyBaseUrl}`
|
|
175
|
+
: "未开启";
|
|
176
|
+
$("#proxyModeHint").textContent = current.proxyModeEnabled
|
|
177
|
+
? "当前 Codex 只要已经接入本地代理,后续同一会话内切换连接会在下一次请求时热更新。"
|
|
178
|
+
: "当前还是直连模式。开启后需要把已打开的旧 Codex 会话重开一次;之后同一会话即可热更新。";
|
|
179
|
+
$("#enableProxyModeButton").disabled = current.proxyModeEnabled;
|
|
180
|
+
$("#enableProxyModeButton").textContent = current.proxyModeEnabled
|
|
181
|
+
? "热更新模式已开启"
|
|
182
|
+
: "开启热更新模式";
|
|
123
183
|
$("#heroBadge").textContent = current.currentNote
|
|
124
|
-
?
|
|
184
|
+
? `${current.proxyModeEnabled ? "热更新中" : "当前连接"}:${current.currentNote}`
|
|
125
185
|
: "当前未匹配到已保存连接";
|
|
126
186
|
}
|
|
127
187
|
|
|
188
|
+
function renderCloudStatus() {
|
|
189
|
+
const cloud = state.cloud;
|
|
190
|
+
if (!cloud) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
syncFieldValue("#cloudServerUrl", cloud.serverUrl);
|
|
195
|
+
syncFieldValue("#cloudUsername", cloud.username);
|
|
196
|
+
|
|
197
|
+
const authText = !cloud.serverUrl
|
|
198
|
+
? "未配置同步服务器"
|
|
199
|
+
: cloud.loggedIn
|
|
200
|
+
? `已登录:${cloud.remoteUser || cloud.username}`
|
|
201
|
+
: cloud.hasToken
|
|
202
|
+
? "本地已保存登录态,但远端校验未通过"
|
|
203
|
+
: "未登录";
|
|
204
|
+
|
|
205
|
+
const remoteText = cloud.serverUrl
|
|
206
|
+
? cloud.lastError
|
|
207
|
+
? `${cloud.serverUrl} · ${cloud.lastError}`
|
|
208
|
+
: cloud.serverUrl
|
|
209
|
+
: "未设置";
|
|
210
|
+
|
|
211
|
+
const syncStatusParts = [];
|
|
212
|
+
if (cloud.lastPushAt) {
|
|
213
|
+
syncStatusParts.push(`最近推送:${formatDateTime(cloud.lastPushAt)}`);
|
|
214
|
+
}
|
|
215
|
+
if (cloud.lastPullAt) {
|
|
216
|
+
syncStatusParts.push(`最近拉取:${formatDateTime(cloud.lastPullAt)}`);
|
|
217
|
+
}
|
|
218
|
+
if (!syncStatusParts.length) {
|
|
219
|
+
syncStatusParts.push("还没有推送或拉取记录");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
$("#cloudAuthStatus").textContent = authText;
|
|
223
|
+
$("#cloudRemoteStatus").textContent = remoteText;
|
|
224
|
+
$("#cloudLastSyncStatus").textContent = syncStatusParts.join(" / ");
|
|
225
|
+
$("#cloudLogoutButton").disabled = !cloud.hasToken;
|
|
226
|
+
$("#cloudPushButton").disabled = !cloud.loggedIn;
|
|
227
|
+
$("#cloudPullMergeButton").disabled = !cloud.loggedIn;
|
|
228
|
+
$("#cloudPullReplaceButton").disabled = !cloud.loggedIn;
|
|
229
|
+
}
|
|
230
|
+
|
|
128
231
|
function createEndpointCard(endpoint) {
|
|
129
232
|
const card = document.createElement("article");
|
|
130
233
|
card.className = `profile-card ${endpoint.isActive ? "active" : ""}`;
|
|
@@ -215,18 +318,21 @@ function renderEndpoints() {
|
|
|
215
318
|
|
|
216
319
|
async function refreshDashboard(showMessage = true) {
|
|
217
320
|
try {
|
|
218
|
-
const [current, endpoints, paths] = await Promise.all([
|
|
321
|
+
const [current, endpoints, paths, cloud] = await Promise.all([
|
|
219
322
|
bridge.getCurrentConfig(),
|
|
220
323
|
bridge.listEndpoints(),
|
|
221
324
|
bridge.getPaths(),
|
|
325
|
+
bridge.getCloudSyncStatus(),
|
|
222
326
|
]);
|
|
223
327
|
|
|
224
328
|
state.current = current;
|
|
225
329
|
state.endpoints = endpoints;
|
|
226
330
|
state.paths = paths;
|
|
331
|
+
state.cloud = cloud;
|
|
227
332
|
|
|
228
333
|
renderCurrent();
|
|
229
334
|
renderEndpoints();
|
|
335
|
+
renderCloudStatus();
|
|
230
336
|
|
|
231
337
|
if (showMessage) {
|
|
232
338
|
setStatus("已刷新当前连接状态。", "success");
|
|
@@ -241,10 +347,14 @@ async function handleSwitchEndpoint(id, note) {
|
|
|
241
347
|
setStatus(`正在切换到:${note} ...`, "info");
|
|
242
348
|
const result = await bridge.switchEndpoint({ id });
|
|
243
349
|
await refreshDashboard(false);
|
|
244
|
-
|
|
245
|
-
`已切换到 ${note}
|
|
246
|
-
|
|
247
|
-
|
|
350
|
+
if (result.switchedViaProxy) {
|
|
351
|
+
setStatus(`已切换到 ${note}。当前会话下一次请求会直接走新的 URL/Key。`, "success");
|
|
352
|
+
} else {
|
|
353
|
+
setStatus(
|
|
354
|
+
`已切换到 ${note}。当前还是直连模式,只有新开的 Codex 会话会立即使用新配置。`,
|
|
355
|
+
"success",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
248
358
|
return result;
|
|
249
359
|
} catch (error) {
|
|
250
360
|
setStatus(`切换失败:${error.message}`, "error");
|
|
@@ -252,6 +362,109 @@ async function handleSwitchEndpoint(id, note) {
|
|
|
252
362
|
}
|
|
253
363
|
}
|
|
254
364
|
|
|
365
|
+
async function handleEnableProxyMode() {
|
|
366
|
+
try {
|
|
367
|
+
setStatus("正在切到热更新代理模式 ...", "info");
|
|
368
|
+
const result = await bridge.enableProxyMode();
|
|
369
|
+
await refreshDashboard(false);
|
|
370
|
+
if (result.oneTimeRestartRequired) {
|
|
371
|
+
setStatus(
|
|
372
|
+
"热更新模式已开启。请把当前已经打开着的旧 Codex 会话重开一次;之后同一会话内即可热更新。",
|
|
373
|
+
"success",
|
|
374
|
+
);
|
|
375
|
+
} else {
|
|
376
|
+
setStatus("热更新模式已开启。当前会话后续请求可直接热更新到新连接。", "success");
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
setStatus(`开启热更新模式失败:${error.message}`, "error");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function getCloudFormPayload() {
|
|
384
|
+
return {
|
|
385
|
+
serverUrl: $("#cloudServerUrl").value.trim(),
|
|
386
|
+
username: $("#cloudUsername").value.trim(),
|
|
387
|
+
password: $("#cloudPassword").value,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function handleCloudRegister() {
|
|
392
|
+
try {
|
|
393
|
+
setStatus("正在注册同步账号 ...", "info");
|
|
394
|
+
const payload = getCloudFormPayload();
|
|
395
|
+
const result = await bridge.registerCloudAccount(payload);
|
|
396
|
+
$("#cloudPassword").value = "";
|
|
397
|
+
await refreshDashboard(false);
|
|
398
|
+
setStatus(result.message || "账号注册成功。", "success");
|
|
399
|
+
} catch (error) {
|
|
400
|
+
setStatus(`注册失败:${error.message}`, "error");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function handleCloudLogin() {
|
|
405
|
+
try {
|
|
406
|
+
setStatus("正在登录同步账号 ...", "info");
|
|
407
|
+
const payload = getCloudFormPayload();
|
|
408
|
+
const result = await bridge.loginCloudAccount(payload);
|
|
409
|
+
$("#cloudPassword").value = "";
|
|
410
|
+
await refreshDashboard(false);
|
|
411
|
+
setStatus(result.message || "账号登录成功。", "success");
|
|
412
|
+
} catch (error) {
|
|
413
|
+
setStatus(`登录失败:${error.message}`, "error");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function handleCloudLogout() {
|
|
418
|
+
try {
|
|
419
|
+
setStatus("正在退出同步账号 ...", "info");
|
|
420
|
+
const result = await bridge.logoutCloudAccount();
|
|
421
|
+
await refreshDashboard(false);
|
|
422
|
+
setStatus(result.message || "已退出同步账号。", "success");
|
|
423
|
+
} catch (error) {
|
|
424
|
+
setStatus(`退出失败:${error.message}`, "error");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function handleCloudPush() {
|
|
429
|
+
try {
|
|
430
|
+
setStatus("正在把当前连接推送到账号空间 ...", "info");
|
|
431
|
+
const result = await bridge.pushCloudConfig();
|
|
432
|
+
await refreshDashboard(false);
|
|
433
|
+
setStatus(
|
|
434
|
+
result.message || `已推送 ${result.endpointCount || 0} 条连接到账号空间。`,
|
|
435
|
+
"success",
|
|
436
|
+
);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
setStatus(`推送失败:${error.message}`, "error");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function handleCloudPull(mode) {
|
|
443
|
+
if (mode === "replace") {
|
|
444
|
+
const confirmed = window.confirm(
|
|
445
|
+
"覆盖拉取会直接替换本机保存的连接列表。拉取前会自动备份,确定继续吗?",
|
|
446
|
+
);
|
|
447
|
+
if (!confirmed) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
setStatus(mode === "merge" ? "正在从账号空间合并拉取 ..." : "正在从账号空间覆盖拉取 ...", "info");
|
|
454
|
+
const result = await bridge.pullCloudConfig({ mode });
|
|
455
|
+
await refreshDashboard(false);
|
|
456
|
+
const activeHint = result.activeEndpointNote
|
|
457
|
+
? `当前生效连接记录:${result.activeEndpointNote}。`
|
|
458
|
+
: "当前没有匹配到生效连接记录。";
|
|
459
|
+
setStatus(
|
|
460
|
+
`${result.message || "拉取完成。"} 新增 ${result.addedCount} 条,更新 ${result.updatedCount} 条。${activeHint}`,
|
|
461
|
+
"success",
|
|
462
|
+
);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
setStatus(`拉取失败:${error.message}`, "error");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
255
468
|
async function handleSubmitEndpoint(event) {
|
|
256
469
|
event.preventDefault();
|
|
257
470
|
const id = $("#endpointId").value.trim();
|
|
@@ -320,6 +533,17 @@ function bindEvents() {
|
|
|
320
533
|
resetForm();
|
|
321
534
|
setStatus("已清空表单。", "info");
|
|
322
535
|
});
|
|
536
|
+
$("#enableProxyModeButton").addEventListener("click", handleEnableProxyMode);
|
|
537
|
+
$("#cloudRegisterButton").addEventListener("click", handleCloudRegister);
|
|
538
|
+
$("#cloudLoginButton").addEventListener("click", handleCloudLogin);
|
|
539
|
+
$("#cloudLogoutButton").addEventListener("click", handleCloudLogout);
|
|
540
|
+
$("#cloudPushButton").addEventListener("click", handleCloudPush);
|
|
541
|
+
$("#cloudPullMergeButton").addEventListener("click", () => {
|
|
542
|
+
handleCloudPull("merge");
|
|
543
|
+
});
|
|
544
|
+
$("#cloudPullReplaceButton").addEventListener("click", () => {
|
|
545
|
+
handleCloudPull("replace");
|
|
546
|
+
});
|
|
323
547
|
$("#openCodexRootButton").addEventListener("click", () => {
|
|
324
548
|
if (state.paths?.codexRoot) {
|
|
325
549
|
handleOpenPath(state.paths.codexRoot, " Codex 目录");
|
package/src/renderer/styles.css
CHANGED
|
@@ -33,7 +33,8 @@ body {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
button,
|
|
36
|
-
input
|
|
36
|
+
input,
|
|
37
|
+
textarea {
|
|
37
38
|
font: inherit;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -259,7 +260,8 @@ code {
|
|
|
259
260
|
|
|
260
261
|
input[type="text"],
|
|
261
262
|
input[type="password"],
|
|
262
|
-
input[type="file"]
|
|
263
|
+
input[type="file"],
|
|
264
|
+
textarea {
|
|
263
265
|
width: 100%;
|
|
264
266
|
min-height: 46px;
|
|
265
267
|
padding: 10px 14px;
|
|
@@ -269,6 +271,12 @@ input[type="file"] {
|
|
|
269
271
|
color: var(--text);
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
textarea {
|
|
275
|
+
min-height: 168px;
|
|
276
|
+
resize: vertical;
|
|
277
|
+
line-height: 1.6;
|
|
278
|
+
}
|
|
279
|
+
|
|
272
280
|
input[readonly] {
|
|
273
281
|
color: var(--muted);
|
|
274
282
|
background: rgba(249, 244, 238, 0.95);
|
|
@@ -300,6 +308,33 @@ input[readonly] {
|
|
|
300
308
|
gap: 10px;
|
|
301
309
|
}
|
|
302
310
|
|
|
311
|
+
.sync-status-grid {
|
|
312
|
+
display: grid;
|
|
313
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
314
|
+
gap: 10px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.sync-status-card {
|
|
318
|
+
padding: 14px;
|
|
319
|
+
border-radius: 14px;
|
|
320
|
+
background: rgba(255, 255, 255, 0.72);
|
|
321
|
+
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.sync-status-card strong {
|
|
325
|
+
display: block;
|
|
326
|
+
margin-top: 8px;
|
|
327
|
+
word-break: break-all;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.sync-status-card-wide {
|
|
331
|
+
grid-column: span 2;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.metric-actions {
|
|
335
|
+
margin-top: 12px;
|
|
336
|
+
}
|
|
337
|
+
|
|
303
338
|
.primary-button,
|
|
304
339
|
.secondary-button,
|
|
305
340
|
.ghost-button {
|
|
@@ -504,4 +539,12 @@ input[readonly] {
|
|
|
504
539
|
.hero-side {
|
|
505
540
|
min-height: 180px;
|
|
506
541
|
}
|
|
542
|
+
|
|
543
|
+
.sync-status-grid {
|
|
544
|
+
grid-template-columns: 1fr;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.sync-status-card-wide {
|
|
548
|
+
grid-column: auto;
|
|
549
|
+
}
|
|
507
550
|
}
|