Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1428220210 | ||
|
|
7b97f010c9 |
No files matched your search
@@ -133,6 +133,8 @@ jobs:
|
||||
# Persistent data root: repo var wins, else derive from the org
|
||||
# PERSISTENT_MOUNT (absolute HOST path), else the workspace fallback.
|
||||
CRYPTO_DATA_DIR: ${{ vars.CRYPTO_DATA_DIR || (vars.PERSISTENT_MOUNT && format('{0}/crypto_trading', vars.PERSISTENT_MOUNT)) || './data' }}
|
||||
# Stamped into both images so the client can detect a stale cached bundle.
|
||||
CRYPTO_BUILD_VERSION: ${{ github.sha }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
steps:
|
||||
|
||||
@@ -5,6 +5,10 @@ ENV PYTHONUNBUFFERED=1 \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Build stamp exposed via /api/version so the frontend can detect a stale cache.
|
||||
ARG BUILD_VERSION=unknown
|
||||
ENV BUILD_VERSION=${BUILD_VERSION}
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
@@ -63,6 +63,13 @@ def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
def version(request: Request) -> dict[str, str]:
|
||||
"""Build version of the running backend, used by the client to self-heal
|
||||
from a stale cached bundle after a deploy."""
|
||||
return {"version": getattr(request.app.state, "build_version", "unknown")}
|
||||
|
||||
|
||||
@router.get("/strategies", response_model=list[StrategyOut])
|
||||
def strategies() -> list[StrategyOut]:
|
||||
return [strategy_spec(cls) for cls in list_strategies()]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@@ -27,6 +28,9 @@ def create_app(
|
||||
await runtime.shutdown()
|
||||
|
||||
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
||||
# Baked at build time (see Dockerfile); the frontend compares its own
|
||||
# version against this to self-heal from a stale cache.
|
||||
app.state.build_version = os.environ.get("BUILD_VERSION", "unknown")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origin_list,
|
||||
|
||||
@@ -9,6 +9,13 @@ from app.market.fake import FakeMarketData
|
||||
from app.runtime import Runtime
|
||||
|
||||
|
||||
def test_version_endpoint(runtime: Runtime) -> None:
|
||||
app = create_app(runtime.settings, runtime)
|
||||
with TestClient(app) as client:
|
||||
assert client.get("/api/version").status_code == 200
|
||||
assert "version" in client.get("/api/version").json()
|
||||
|
||||
|
||||
def test_health_and_strategies(runtime: Runtime) -> None:
|
||||
app = create_app(runtime.settings, runtime)
|
||||
with TestClient(app) as client:
|
||||
|
||||
@@ -5,7 +5,10 @@ name: crypto_trading_local
|
||||
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
build:
|
||||
context: ./backend
|
||||
args:
|
||||
BUILD_VERSION: ${BUILD_VERSION:-unknown}
|
||||
container_name: crypto-trading-backend-local
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -37,7 +40,10 @@ services:
|
||||
- "8000:8000"
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
build:
|
||||
context: ./frontend
|
||||
args:
|
||||
BUILD_VERSION: ${BUILD_VERSION:-unknown}
|
||||
container_name: crypto-trader-frontend-local
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
|
||||
+8
-2
@@ -2,7 +2,10 @@ name: crypto_trading
|
||||
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
build:
|
||||
context: ./backend
|
||||
args:
|
||||
BUILD_VERSION: ${CRYPTO_BUILD_VERSION:-unknown}
|
||||
image: crypto_trading-backend
|
||||
container_name: crypto-trading-backend
|
||||
restart: unless-stopped
|
||||
@@ -56,7 +59,10 @@ services:
|
||||
- internal
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
build:
|
||||
context: ./frontend
|
||||
args:
|
||||
BUILD_VERSION: ${CRYPTO_BUILD_VERSION:-unknown}
|
||||
image: crypto_trading-frontend
|
||||
container_name: crypto-trading-frontend
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
# Same value as the backend's BUILD_VERSION so the client can compare them.
|
||||
ARG BUILD_VERSION=unknown
|
||||
ENV VITE_BUILD_VERSION=${BUILD_VERSION}
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
@@ -133,7 +133,27 @@ function handleMessage(message) {
|
||||
}
|
||||
}
|
||||
|
||||
// If the server is running a newer build than this (possibly cached) bundle,
|
||||
// reload once with a cache-busting query so phones pick up deploys without a
|
||||
// manual hard-refresh. Guarded so it can never loop.
|
||||
async function ensureFreshBuild() {
|
||||
try {
|
||||
const { version } = await api.getVersion()
|
||||
if (!version || version === 'unknown') return
|
||||
if (version === __BUILD_VERSION__) return
|
||||
const key = `reloaded:${version}`
|
||||
if (sessionStorage.getItem(key)) return
|
||||
sessionStorage.setItem(key, '1')
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('v', version)
|
||||
window.location.replace(url.toString())
|
||||
} catch {
|
||||
// ignore; not worth blocking the app
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
ensureFreshBuild()
|
||||
await Promise.all([
|
||||
loadStrategies(),
|
||||
loadPresets(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from 'axios'
|
||||
|
||||
const http = axios.create({ baseURL: '/api', timeout: 10000 })
|
||||
|
||||
export const getVersion = () => http.get('/version').then((r) => r.data)
|
||||
export const getStatus = () => http.get('/status').then((r) => r.data)
|
||||
export const getStrategies = () => http.get('/strategies').then((r) => r.data)
|
||||
export const getPositions = () => http.get('/positions').then((r) => r.data)
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
// Short git SHA (or a timestamp fallback) baked into the bundle so the client
|
||||
// can detect when the server is running a newer build and reload itself.
|
||||
let buildVersion = process.env.VITE_BUILD_VERSION || String(Date.now())
|
||||
if (!process.env.VITE_BUILD_VERSION) {
|
||||
try {
|
||||
buildVersion = execSync('git rev-parse --short HEAD').toString().trim()
|
||||
} catch {
|
||||
// not a git checkout; keep the timestamp
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__BUILD_VERSION__: JSON.stringify(buildVersion),
|
||||
},
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
|
||||
Reference in new issue
Block a user