diff --git a/lib/bridge_client_lib.py b/lib/bridge_client_lib.py
index 4285c15..6bdb645 100644
--- a/lib/bridge_client_lib.py
+++ b/lib/bridge_client_lib.py
@@ -302,6 +302,8 @@ def fetch_client_version(inst: dict[str, Any]) -> dict[str, Any]:
"short_sha": "",
"branch": "",
"dirty": False,
+ "mode": "",
+ "mode_label": "",
"msg": "",
"instance_id": inst.get("id"),
"instance_name": inst.get("name"),
@@ -320,7 +322,7 @@ def fetch_client_version(inst: dict[str, Any]) -> dict[str, Any]:
if data is None:
out["msg"] = err or f"HTTP {r.status_code}"
return out
- for key in ("sha", "short_sha", "branch", "dirty", "msg", "repo"):
+ for key in ("sha", "short_sha", "branch", "dirty", "msg", "repo", "mode", "mode_label"):
if key in data:
out[key] = data.get(key)
out["ok"] = bool(data.get("ok")) and r.status_code == 200
diff --git a/static/hub.js b/static/hub.js
index 0e1642e..5c81684 100644
--- a/static/hub.js
+++ b/static/hub.js
@@ -255,7 +255,19 @@
status +
"" +
"
" +
- escapeHtml(inst.mode || "—") +
+ (function () {
+ const m = String(inst.mode || "").toLowerCase();
+ if (m === "sim" || m === "live") {
+ return (
+ '' +
+ escapeHtml(m) +
+ ""
+ );
+ }
+ return escapeHtml(inst.mode || "—");
+ })() +
" | " +
"" +
fmtPnlHtml(pnl) +
@@ -472,6 +484,24 @@
"";
}
+ function modeSortKey(mode) {
+ const m = String(mode || "").toLowerCase();
+ if (m === "sim") return 0;
+ if (m === "live") return 1;
+ return 2;
+ }
+
+ function sortInstancesByMode(list) {
+ return (list || []).slice().sort(function (a, b) {
+ const ma = modeSortKey((((a || {}).snapshot || {}).instance || {}).mode);
+ const mb = modeSortKey((((b || {}).snapshot || {}).instance || {}).mode);
+ if (ma !== mb) return ma - mb;
+ const ia = Number((a && a.id) || 0);
+ const ib = Number((b && b.id) || 0);
+ return ia - ib;
+ });
+ }
+
async function refresh() {
const status = document.getElementById("overview-status");
const grid = document.getElementById("overview-grid");
@@ -481,7 +511,7 @@
const r = await fetch("/api/overview");
const d = await r.json();
if (!d.ok) throw new Error(d.msg || "失败");
- const list = d.instances || [];
+ const list = sortInstancesByMode(d.instances || []);
renderOverviewSummary(list);
if (!list.length) {
grid.innerHTML =
diff --git a/static/instances_page.js b/static/instances_page.js
index 108b2b7..0537e9c 100644
--- a/static/instances_page.js
+++ b/static/instances_page.js
@@ -1,91 +1,143 @@
-(function () {
- function fmtVer(v) {
- if (!v || !v.online) return v && v.msg ? String(v.msg) : "离线";
- const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
- const branch = v.branch ? v.branch + " · " : "";
- if (!short) return "未知版本";
- return branch + short + (v.dirty ? " (dirty)" : "");
- }
-
- async function refreshVersion(row) {
- const iid = row.getAttribute("data-id");
- const verEl = row.querySelector("[data-inst-version]");
- if (!iid || !verEl) return;
- verEl.textContent = "…";
- verEl.className = "update-version muted";
- try {
- const r = await fetch("/api/instances/" + iid + "/version", {
- credentials: "same-origin",
- headers: { Accept: "application/json" },
- });
- const v = await r.json();
- if (v && v.online) {
- verEl.textContent = fmtVer(v);
- verEl.className = "update-version online";
- } else {
- verEl.textContent = (v && v.msg) || "离线";
- verEl.className = "update-version offline";
- }
- } catch (e) {
- verEl.textContent = "探测失败";
- verEl.className = "update-version offline";
- }
- }
-
- function setTokenVisible(input, toggleBtn, visible) {
- if (!input) return;
- input.type = visible ? "text" : "password";
- if (toggleBtn) toggleBtn.textContent = visible ? "隐藏" : "显示";
- }
-
- function bindTokenToggles(root) {
- (root || document).querySelectorAll(".inst-token-toggle").forEach(function (btn) {
- if (btn.dataset.bound) return;
- btn.dataset.bound = "1";
- btn.addEventListener("click", function () {
- const wrap = btn.closest(".inst-token-wrap");
- const input = wrap && wrap.querySelector(".inst-token-input");
- if (!input) return;
- setTokenVisible(input, btn, input.type === "password");
- });
- });
- }
-
- function bindEditReveal() {
- document.querySelectorAll(".inst-edit-check[data-reveal-token]").forEach(function (cb) {
- if (cb.dataset.bound) return;
- cb.dataset.bound = "1";
- const sync = function () {
- const panel = cb.nextElementSibling;
- if (!panel) return;
- const input = panel.querySelector("[data-edit-token]");
- const toggle = panel.querySelector(".inst-token-toggle");
- // 展开编辑时明文显示 Token;收起后恢复打码
- setTokenVisible(input, toggle, !!cb.checked);
- };
- cb.addEventListener("change", sync);
- sync();
- });
- }
-
- function boot() {
- bindTokenToggles(document);
- bindEditReveal();
- document.querySelectorAll("tr.inst-row[data-id]").forEach(function (row) {
- refreshVersion(row);
- const btn = row.querySelector("[data-inst-refresh-ver]");
- if (btn) {
- btn.addEventListener("click", function (e) {
- e.preventDefault();
- refreshVersion(row);
- });
- }
- });
- }
-
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", boot);
- } else {
- boot();
- }
-})();
+(function () {
+ function fmtVer(v) {
+ if (!v || !v.online) return v && v.msg ? String(v.msg) : "离线";
+ const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
+ const branch = v.branch ? v.branch + " · " : "";
+ if (!short) return "未知版本";
+ return branch + short + (v.dirty ? " (dirty)" : "");
+ }
+
+ function modeSortKey(mode) {
+ const m = String(mode || "").toLowerCase();
+ if (m === "sim") return 0;
+ if (m === "live") return 1;
+ return 2;
+ }
+
+ function applyModeCell(row, mode) {
+ const modeEl = row.querySelector("[data-inst-mode]");
+ const m = String(mode || "").toLowerCase();
+ row.setAttribute("data-mode", m);
+ if (!modeEl) return;
+ if (m === "sim" || m === "live") {
+ modeEl.textContent = m;
+ modeEl.className = "overview-mode-tag overview-mode-tag--" + m;
+ } else {
+ modeEl.textContent = "—";
+ modeEl.className = "overview-mode-tag muted";
+ }
+ }
+
+ function sortInstanceRows() {
+ const tbody = document.querySelector(".inst-table tbody");
+ if (!tbody) return;
+ const rows = Array.prototype.slice.call(tbody.querySelectorAll("tr.inst-row[data-id]"));
+ if (rows.length < 2) return;
+ const pairs = rows.map(function (row) {
+ return { row: row, edit: row.nextElementSibling };
+ });
+ pairs.sort(function (a, b) {
+ const ka = modeSortKey(a.row.getAttribute("data-mode"));
+ const kb = modeSortKey(b.row.getAttribute("data-mode"));
+ if (ka !== kb) return ka - kb;
+ const ia = Number(a.row.getAttribute("data-id") || 0);
+ const ib = Number(b.row.getAttribute("data-id") || 0);
+ return ia - ib;
+ });
+ pairs.forEach(function (p) {
+ tbody.appendChild(p.row);
+ if (p.edit && p.edit.classList.contains("inst-edit-row")) {
+ tbody.appendChild(p.edit);
+ }
+ });
+ }
+
+ async function refreshVersion(row) {
+ const iid = row.getAttribute("data-id");
+ const verEl = row.querySelector("[data-inst-version]");
+ if (!iid || !verEl) return;
+ verEl.textContent = "…";
+ verEl.className = "update-version muted";
+ try {
+ const r = await fetch("/api/instances/" + iid + "/version", {
+ credentials: "same-origin",
+ headers: { Accept: "application/json" },
+ });
+ const v = await r.json();
+ if (v && v.online) {
+ verEl.textContent = fmtVer(v);
+ verEl.className = "update-version online";
+ applyModeCell(row, v.mode || "");
+ } else {
+ verEl.textContent = (v && v.msg) || "离线";
+ verEl.className = "update-version offline";
+ applyModeCell(row, (v && v.mode) || "");
+ }
+ } catch (e) {
+ verEl.textContent = "探测失败";
+ verEl.className = "update-version offline";
+ applyModeCell(row, "");
+ }
+ }
+
+ function setTokenVisible(input, toggleBtn, visible) {
+ if (!input) return;
+ input.type = visible ? "text" : "password";
+ if (toggleBtn) toggleBtn.textContent = visible ? "隐藏" : "显示";
+ }
+
+ function bindTokenToggles(root) {
+ (root || document).querySelectorAll(".inst-token-toggle").forEach(function (btn) {
+ if (btn.dataset.bound) return;
+ btn.dataset.bound = "1";
+ btn.addEventListener("click", function () {
+ const wrap = btn.closest(".inst-token-wrap");
+ const input = wrap && wrap.querySelector(".inst-token-input");
+ if (!input) return;
+ setTokenVisible(input, btn, input.type === "password");
+ });
+ });
+ }
+
+ function bindEditReveal() {
+ document.querySelectorAll(".inst-edit-check[data-reveal-token]").forEach(function (cb) {
+ if (cb.dataset.bound) return;
+ cb.dataset.bound = "1";
+ const sync = function () {
+ const panel = cb.nextElementSibling;
+ if (!panel) return;
+ const input = panel.querySelector("[data-edit-token]");
+ const toggle = panel.querySelector(".inst-token-toggle");
+ // 展开编辑时明文显示 Token;收起后恢复打码
+ setTokenVisible(input, toggle, !!cb.checked);
+ };
+ cb.addEventListener("change", sync);
+ sync();
+ });
+ }
+
+ async function boot() {
+ bindTokenToggles(document);
+ bindEditReveal();
+ const rows = Array.prototype.slice.call(document.querySelectorAll("tr.inst-row[data-id]"));
+ await Promise.all(
+ rows.map(function (row) {
+ const btn = row.querySelector("[data-inst-refresh-ver]");
+ if (btn) {
+ btn.addEventListener("click", function (e) {
+ e.preventDefault();
+ refreshVersion(row).then(sortInstanceRows);
+ });
+ }
+ return refreshVersion(row);
+ })
+ );
+ sortInstanceRows();
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", boot);
+ } else {
+ boot();
+ }
+})();
diff --git a/templates/dashboard.html b/templates/dashboard.html
index d9bb543..b45d6c5 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -15,6 +15,6 @@
{% endblock %}
{% block scripts %}
-
+
{% endblock %}
diff --git a/templates/instances.html b/templates/instances.html
index bb0a406..ef24311 100644
--- a/templates/instances.html
+++ b/templates/instances.html
@@ -80,6 +80,7 @@
| Token |
路径 |
状态 |
+ 模式 |
版本 |
标签 |
操作 |
@@ -87,7 +88,7 @@
{% for inst in instances %}
-
+
| {{ inst.name }} |
{{ inst.base_url }} |
@@ -101,6 +102,7 @@
停用
{% endif %}
|
+ — |
— |
{{ inst.tags or '—' }} |
@@ -115,7 +117,7 @@
|
- |
+ |
|