diff --git a/backend/app/main.py b/backend/app/main.py
index b7cdb0d..ed83f19 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -128,6 +128,41 @@ _DIST = resolve_frontend_dist()
if (_DIST / "assets").is_dir():
app.mount("/assets", StaticFiles(directory=str(_DIST / "assets")), name="assets")
+_ICONS = _DIST / "icons"
+if _ICONS.is_dir():
+ app.mount("/icons", StaticFiles(directory=str(_ICONS)), name="icons")
+
+
+def _dist_file(name: str) -> Path:
+ return _DIST / name
+
+
+@app.get("/manifest.webmanifest")
+async def web_manifest():
+ path = _dist_file("manifest.webmanifest")
+ if not path.exists():
+ raise HTTPException(status_code=404, detail="manifest missing")
+ return FileResponse(
+ path,
+ media_type="application/manifest+json",
+ headers={"Cache-Control": "no-cache"},
+ )
+
+
+@app.get("/sw.js")
+async def service_worker():
+ path = _dist_file("sw.js")
+ if not path.exists():
+ raise HTTPException(status_code=404, detail="service worker missing")
+ return FileResponse(
+ path,
+ media_type="application/javascript",
+ headers={
+ "Cache-Control": "no-cache",
+ "Service-Worker-Allowed": "/",
+ },
+ )
+
@app.get("/")
async def index_page():
diff --git a/frontend/index.html b/frontend/index.html
index dc4bf04..8d79815 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,7 +2,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
OKX 自动化对冲系统
diff --git a/frontend/public/icons/apple-touch-icon.png b/frontend/public/icons/apple-touch-icon.png
new file mode 100644
index 0000000..7e38155
Binary files /dev/null and b/frontend/public/icons/apple-touch-icon.png differ
diff --git a/frontend/public/icons/favicon-32.png b/frontend/public/icons/favicon-32.png
new file mode 100644
index 0000000..d729280
Binary files /dev/null and b/frontend/public/icons/favicon-32.png differ
diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png
new file mode 100644
index 0000000..a8bca0c
Binary files /dev/null and b/frontend/public/icons/icon-192.png differ
diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png
new file mode 100644
index 0000000..c77ad6e
Binary files /dev/null and b/frontend/public/icons/icon-512.png differ
diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest
new file mode 100644
index 0000000..b64978e
--- /dev/null
+++ b/frontend/public/manifest.webmanifest
@@ -0,0 +1,34 @@
+{
+ "name": "OKX 自动化对冲系统",
+ "short_name": "对冲系统",
+ "description": "ETH 永续+期权对冲监控与策略控制台",
+ "start_url": "/plan",
+ "scope": "/",
+ "display": "standalone",
+ "display_override": ["standalone", "minimal-ui", "browser"],
+ "orientation": "any",
+ "background_color": "#0b0e11",
+ "theme_color": "#0b0e11",
+ "lang": "zh-CN",
+ "categories": ["finance", "productivity"],
+ "icons": [
+ {
+ "src": "/icons/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/icons/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/icons/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/frontend/public/sw.js b/frontend/public/sw.js
new file mode 100644
index 0000000..368c2d8
--- /dev/null
+++ b/frontend/public/sw.js
@@ -0,0 +1,38 @@
+/* Minimal service worker: makes the app installable on desktop & tablet. */
+const CACHE = "eth-hedge-shell-v1";
+const PRECACHE = ["/", "/plan", "/manifest.webmanifest", "/icons/icon-192.png"];
+
+self.addEventListener("install", (event) => {
+ event.waitUntil(
+ caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()),
+ );
+});
+
+self.addEventListener("activate", (event) => {
+ event.waitUntil(
+ caches.keys().then((keys) =>
+ Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
+ ).then(() => self.clients.claim()),
+ );
+});
+
+self.addEventListener("fetch", (event) => {
+ const req = event.request;
+ if (req.method !== "GET") return;
+ const url = new URL(req.url);
+ if (url.origin !== self.location.origin) return;
+ // Never cache API / health / websocket-ish paths
+ if (url.pathname.startsWith("/api") || url.pathname === "/health") return;
+
+ event.respondWith(
+ fetch(req)
+ .then((res) => {
+ if (res.ok && (url.pathname.startsWith("/assets") || url.pathname === "/")) {
+ const copy = res.clone();
+ caches.open(CACHE).then((cache) => cache.put(req, copy));
+ }
+ return res;
+ })
+ .catch(() => caches.match(req).then((hit) => hit || caches.match("/"))),
+ );
+});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index a8c4591..2e1c23e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,7 @@
import type { ReactNode } from "react";
import { NavLink, Navigate, Route, Routes } from "react-router-dom";
import { clearSession, getToken, getUsername } from "./api/client";
+import InstallBadge from "./components/InstallBadge";
import LoginPage from "./pages/Login";
import PlanPage from "./pages/Plan";
import TradesPage from "./pages/Trades";
@@ -14,6 +15,7 @@ function Shell({ children }: { children: ReactNode }) {