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 data root: repo var wins, else derive from the org
|
||||||
# PERSISTENT_MOUNT (absolute HOST path), else the workspace fallback.
|
# 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' }}
|
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_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ ENV PYTHONUNBUFFERED=1 \
|
|||||||
|
|
||||||
WORKDIR /app
|
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 .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ def health() -> dict[str, str]:
|
|||||||
return {"status": "ok"}
|
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])
|
@router.get("/strategies", response_model=list[StrategyOut])
|
||||||
def strategies() -> list[StrategyOut]:
|
def strategies() -> list[StrategyOut]:
|
||||||
return [strategy_spec(cls) for cls in list_strategies()]
|
return [strategy_spec(cls) for cls in list_strategies()]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ def create_app(
|
|||||||
await runtime.shutdown()
|
await runtime.shutdown()
|
||||||
|
|
||||||
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
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(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=settings.cors_origin_list,
|
allow_origins=settings.cors_origin_list,
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ from app.market.fake import FakeMarketData
|
|||||||
from app.runtime import Runtime
|
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:
|
def test_health_and_strategies(runtime: Runtime) -> None:
|
||||||
app = create_app(runtime.settings, runtime)
|
app = create_app(runtime.settings, runtime)
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ name: crypto_trading_local
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build:
|
||||||
|
context: ./backend
|
||||||
|
args:
|
||||||
|
BUILD_VERSION: ${BUILD_VERSION:-unknown}
|
||||||
container_name: crypto-trading-backend-local
|
container_name: crypto-trading-backend-local
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -37,7 +40,10 @@ services:
|
|||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
args:
|
||||||
|
BUILD_VERSION: ${BUILD_VERSION:-unknown}
|
||||||
container_name: crypto-trader-frontend-local
|
container_name: crypto-trader-frontend-local
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
+8
-2
@@ -2,7 +2,10 @@ name: crypto_trading
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build:
|
||||||
|
context: ./backend
|
||||||
|
args:
|
||||||
|
BUILD_VERSION: ${CRYPTO_BUILD_VERSION:-unknown}
|
||||||
image: crypto_trading-backend
|
image: crypto_trading-backend
|
||||||
container_name: crypto-trading-backend
|
container_name: crypto-trading-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -56,7 +59,10 @@ services:
|
|||||||
- internal
|
- internal
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
args:
|
||||||
|
BUILD_VERSION: ${CRYPTO_BUILD_VERSION:-unknown}
|
||||||
image: crypto_trading-frontend
|
image: crypto_trading-frontend
|
||||||
container_name: crypto-trading-frontend
|
container_name: crypto-trading-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
FROM node:22-alpine AS build
|
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
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci --no-audit --no-fund
|
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 () => {
|
onMounted(async () => {
|
||||||
|
ensureFreshBuild()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadStrategies(),
|
loadStrategies(),
|
||||||
loadPresets(),
|
loadPresets(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import axios from 'axios'
|
|||||||
|
|
||||||
const http = axios.create({ baseURL: '/api', timeout: 10000 })
|
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 getStatus = () => http.get('/status').then((r) => r.data)
|
||||||
export const getStrategies = () => http.get('/strategies').then((r) => r.data)
|
export const getStrategies = () => http.get('/strategies').then((r) => r.data)
|
||||||
export const getPositions = () => http.get('/positions').then((r) => r.data)
|
export const getPositions = () => http.get('/positions').then((r) => r.data)
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
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({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__BUILD_VERSION__: JSON.stringify(buildVersion),
|
||||||
|
},
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|||||||
Reference in new issue
Block a user