diff --git a/app.py b/app.py index 55ff4dd..444ebd7 100644 --- a/app.py +++ b/app.py @@ -362,6 +362,8 @@ def api_hub_backup_create(): env_path=str(ENV_PATH), db_path=_resolved_hub_db_path(), label="manual", + username=hub_username(), + password=hub_password(), ) return jsonify(result) except Exception as exc: @@ -395,6 +397,12 @@ def api_hub_backup_restore(): env_path=str(ENV_PATH), db_path=_resolved_hub_db_path(), ) + if result.get("ok"): + u = str(result.get("hub_username") or "").strip() + p = result.get("hub_password") + if u or p is not None: + set_hub_credentials(u or hub_username(), "" if p is None else str(p)) + result.pop("hub_password", None) return jsonify(result), (200 if result.get("ok") else 400) @@ -517,6 +525,7 @@ try: env_path=str(ENV_PATH), db_path_fn=_resolved_hub_db_path, hour=3, + get_credentials=lambda: (hub_username(), hub_password()), ) except Exception as _hub_bak_err: print(f"[backup] scheduler: {_hub_bak_err}") @@ -781,6 +790,21 @@ def api_cond_orders_exit(hub_order_id: str): return jsonify(post_cond_exit(inst, hub_order_id, payload)) +@app.route("/manifest.webmanifest") +def pwa_manifest(): + path = ROOT / "static" / "icons" / "manifest.webmanifest" + return send_file(path, mimetype="application/manifest+json; charset=utf-8") + + +@app.route("/sw.js") +def pwa_service_worker(): + path = ROOT / "static" / "sw.js" + resp = send_file(path, mimetype="application/javascript; charset=utf-8") + resp.headers["Cache-Control"] = "no-cache" + resp.headers["Service-Worker-Allowed"] = "/" + return resp + + if __name__ == "__main__": # PM2 下禁止 debug reloader,否则会 Bad file descriptor 崩循环 _debug = (os.getenv("APP_DEBUG") or "").strip().lower() in ("1", "true", "yes", "on") diff --git a/static/hub.css b/static/hub.css index 98b424c..43902eb 100644 --- a/static/hub.css +++ b/static/hub.css @@ -169,11 +169,16 @@ a:hover { color: #7dd3fc; } .nav-spot--flat .nav-spot-last, .nav-spot--flat .nav-spot-chg, .nav-spot--flat .nav-spot-pct { color: #9aa3bf; } -.topbar-logout { +.topbar-actions { position: fixed; top: 16px; right: 22px; z-index: 20; + display: flex; + align-items: center; + gap: 10px; +} +.topbar-logout { color: var(--muted); font-size: 0.82rem; letter-spacing: 0.04em; @@ -181,6 +186,39 @@ a:hover { color: #7dd3fc; } .topbar-logout:hover { color: #e8f1ff; } +.topbar-install { + appearance: none; + border: 1px solid #304164; + background: #151a2a; + color: #dbe4ff; + font-size: 0.78rem; + border-radius: 8px; + padding: 4px 10px; + cursor: pointer; + font-family: inherit; +} +.topbar-install:hover { + color: #ffffff; + border-color: #3d5278; +} +.topbar-install-hint { + position: fixed; + top: 46px; + right: 22px; + z-index: 20; + margin: 0; + max-width: min(360px, calc(100vw - 40px)); + color: var(--muted); + font-size: 0.75rem; + line-height: 1.4; + text-align: right; +} +@media (display-mode: standalone) { + .topbar-install, + .topbar-install-hint { + display: none !important; + } +} @media (max-width: 1100px) { .topbar-row { grid-template-columns: 1fr; diff --git a/static/hub_pwa.js b/static/hub_pwa.js new file mode 100644 index 0000000..de3a6e7 --- /dev/null +++ b/static/hub_pwa.js @@ -0,0 +1,89 @@ +(function (global) { + var deferredPrompt = null; + + function isStandalone() { + return ( + (global.matchMedia && global.matchMedia("(display-mode: standalone)").matches) || + global.navigator.standalone === true + ); + } + + function btn() { + return document.getElementById("hub-install-btn"); + } + + function setHint(msg) { + var el = document.getElementById("hub-install-hint"); + if (el) el.textContent = msg || ""; + } + + function showInstall() { + var el = btn(); + if (!el || isStandalone()) return; + el.hidden = false; + } + + function hideInstall() { + var el = btn(); + if (el) el.hidden = true; + setHint(""); + } + + function downloadShortcut() { + var origin = global.location.origin; + var body = "[InternetShortcut]\r\nURL=" + origin + "/\r\n"; + var blob = new Blob([body], { type: "application/internet-shortcut" }); + var a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "期权中控.url"; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(function () { + URL.revokeObjectURL(a.href); + }, 1000); + } + + async function onInstallClick() { + setHint(""); + if (deferredPrompt) { + deferredPrompt.prompt(); + try { + var choice = await deferredPrompt.userChoice; + deferredPrompt = null; + if (choice && choice.outcome === "accepted") { + hideInstall(); + return; + } + } catch (e) { + deferredPrompt = null; + } + } + downloadShortcut(); + setHint("已下载快捷方式,拖到桌面即可。Chrome / Edge 也可在地址栏右侧点「安装」。"); + } + + function boot() { + if (isStandalone()) { + hideInstall(); + return; + } + showInstall(); + global.addEventListener("beforeinstallprompt", function (ev) { + ev.preventDefault(); + deferredPrompt = ev; + showInstall(); + }); + global.addEventListener("appinstalled", function () { + deferredPrompt = null; + hideInstall(); + }); + var el = btn(); + if (el) el.addEventListener("click", onInstallClick); + if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(function () {}); + } + } + + global.HubPwa = { boot: boot }; +})(window); diff --git a/static/icons/apple-touch-icon.png b/static/icons/apple-touch-icon.png new file mode 100644 index 0000000..4728401 Binary files /dev/null and b/static/icons/apple-touch-icon.png differ diff --git a/static/icons/favicon.ico b/static/icons/favicon.ico new file mode 100644 index 0000000..8423839 Binary files /dev/null and b/static/icons/favicon.ico differ diff --git a/static/icons/icon-16.png b/static/icons/icon-16.png new file mode 100644 index 0000000..53a7b92 Binary files /dev/null and b/static/icons/icon-16.png differ diff --git a/static/icons/icon-192-maskable.png b/static/icons/icon-192-maskable.png new file mode 100644 index 0000000..573979f Binary files /dev/null and b/static/icons/icon-192-maskable.png differ diff --git a/static/icons/icon-192.png b/static/icons/icon-192.png new file mode 100644 index 0000000..423310e Binary files /dev/null and b/static/icons/icon-192.png differ diff --git a/static/icons/icon-32.png b/static/icons/icon-32.png new file mode 100644 index 0000000..613ec49 Binary files /dev/null and b/static/icons/icon-32.png differ diff --git a/static/icons/icon-512-maskable.png b/static/icons/icon-512-maskable.png new file mode 100644 index 0000000..f8f812e Binary files /dev/null and b/static/icons/icon-512-maskable.png differ diff --git a/static/icons/icon-512.png b/static/icons/icon-512.png new file mode 100644 index 0000000..6a4b8e0 Binary files /dev/null and b/static/icons/icon-512.png differ diff --git a/static/icons/icon.svg b/static/icons/icon.svg new file mode 100644 index 0000000..09c0f62 --- /dev/null +++ b/static/icons/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/icons/manifest.webmanifest b/static/icons/manifest.webmanifest new file mode 100644 index 0000000..719cbf1 --- /dev/null +++ b/static/icons/manifest.webmanifest @@ -0,0 +1,37 @@ +{ + "name": "加密货币|OKX 期权中控", + "short_name": "期权中控", + "description": "多机期权持仓总览与工作台", + "id": "/", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#070b14", + "theme_color": "#070b14", + "icons": [ + { + "src": "/static/icons/icon-192.png?v=1", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/static/icons/icon-512.png?v=1", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/static/icons/icon-192-maskable.png?v=1", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/static/icons/icon-512-maskable.png?v=1", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 0000000..7800c8c --- /dev/null +++ b/static/sw.js @@ -0,0 +1,7 @@ +/* 期权中控 PWA:仅占位使可安装,请求一律走网络 */ +self.addEventListener("install", function (event) { + event.waitUntil(self.skipWaiting()); +}); +self.addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); diff --git a/templates/base.html b/templates/base.html index 390648a..5c66ec8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,12 +4,23 @@ {% block title %}加密货币|OKX 期权中控{% endblock %} - + + + + + + + + - {% if session.get('logged_in') %} - 退出 - {% endif %} +
+ + {% if session.get('logged_in') %} + 退出 + {% endif %} +
+

{% if session.get('logged_in') %} 加密货币|OKX 期权中控 @@ -54,5 +65,7 @@ {% if session.get('logged_in') and not request.path.startswith('/desk') %} {% endif %} + +