Compare commits
5
Commits
ws-reconnect
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86008fce06 | ||
|
|
0a27d01f12 | ||
|
|
803df14813 | ||
|
|
7ea25ee4fd | ||
|
|
ac01388117 |
@@ -66,14 +66,23 @@ jobs:
|
||||
cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max
|
||||
|
||||
# The primary tag is already pushed and deployable by the time this runs:
|
||||
# the image policy selects `main-<timestamp>`, so a failure here costs the
|
||||
# SHA audit tag, not the rollout.
|
||||
- name: Wait for Harbor policy, then add SHA tag
|
||||
shell: bash
|
||||
env:
|
||||
PRIMARY: ${{ steps.tags.outputs.primary }}
|
||||
QUALIFIED: ${{ steps.tags.outputs.qualified }}
|
||||
run: |
|
||||
# Harbor's vulnerability policy blocks manifest reads while Trivy's scan
|
||||
# is still Running, and `imagetools create` copies the manifest. Retry
|
||||
# that copy itself rather than an `inspect` probe: a probe that passes is
|
||||
# no guarantee the copy will, and under the default `bash -e` the first
|
||||
# blocked copy aborted the step before its retries could run.
|
||||
for attempt in $(seq 1 60); do
|
||||
if docker buildx imagetools inspect "$PRIMARY" >/dev/null 2>&1; then
|
||||
docker buildx imagetools create --tag "$QUALIFIED" "$PRIMARY"
|
||||
if docker buildx imagetools create --tag "$QUALIFIED" "$PRIMARY"; then
|
||||
echo "tagged $QUALIFIED on attempt $attempt"
|
||||
exit 0
|
||||
fi
|
||||
echo "waiting for Harbor scan/pull policy ($attempt/60)"
|
||||
|
||||
@@ -35,6 +35,12 @@ The image does not set `BTCDATA_URL`; deployments must provide it. `BUILD_SHA` i
|
||||
runner, then publishes immutable linux/amd64 images to
|
||||
`harbor.pmagnus.cloud/btcprice/btcprice`. It embeds the short Git SHA as `APP_VERSION`.
|
||||
|
||||
The build pushes two tags: `main-<timestamp>`, then a companion `main-<timestamp>-<sha>` for
|
||||
audit. Harbor's vulnerability policy blocks manifest reads while Trivy is still scanning, so
|
||||
the companion tag is retried until the scan clears. Only the first tag matters for deployment —
|
||||
the Flux image policy matches `^main-<14 digits>$` — so a failure in that last step costs the
|
||||
audit tag and nothing else.
|
||||
|
||||
Flux image automation updates the manifests in the separate `magnus/deploy` repository and Flux
|
||||
reconciles k3s. CI has no Kubernetes credentials. Production runs two replicas—one on hostinger and
|
||||
one on citadel—and is served at https://btcprice.k8s.pmagnus.cloud through ingress-nginx and
|
||||
|
||||
@@ -35,6 +35,12 @@ The image does not set `BTCDATA_URL`; deployments must provide it. `BUILD_SHA` i
|
||||
runner, then publishes immutable linux/amd64 images to
|
||||
`harbor.pmagnus.cloud/btcprice/btcprice`. It embeds the short Git SHA as `APP_VERSION`.
|
||||
|
||||
The build pushes two tags: `main-<timestamp>`, then a companion `main-<timestamp>-<sha>` for
|
||||
audit. Harbor's vulnerability policy blocks manifest reads while Trivy is still scanning, so
|
||||
the companion tag is retried until the scan clears. Only the first tag matters for deployment —
|
||||
the Flux image policy matches `^main-<14 digits>$` — so a failure in that last step costs the
|
||||
audit tag and nothing else.
|
||||
|
||||
Flux image automation updates the manifests in the separate `magnus/deploy` repository and Flux
|
||||
reconciles k3s. CI has no Kubernetes credentials. Production runs two replicas—one on hostinger and
|
||||
one on citadel—and is served at https://btcprice.k8s.pmagnus.cloud through ingress-nginx and
|
||||
|
||||
@@ -127,3 +127,7 @@ color-coded (green up, red down). Reconnection is handled by HTMX.
|
||||
Python · FastAPI · Uvicorn · websockets · HTMX · Tailwind CSS v4
|
||||
|
||||
The Clojure implementation this replaced is on the `clojure` branch.
|
||||
|
||||
The 24-hour chart uses 48 half-hour candles from rustbtcdata’s
|
||||
`/api/kraken-half-hour` endpoint, aggregated from minute OHLC data. It refreshes
|
||||
every five minutes and includes the current partial half-hour candle.
|
||||
|
||||
+7
-2
@@ -73,7 +73,7 @@ HOME_HTML = """\
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||
<p class="text-sm text-gray-500 mb-3">24h Candles (1h)</p>
|
||||
<p class="text-sm text-gray-500 mb-3">24h Candles (30m)</p>
|
||||
<div id="chart-hour" style="height:300px"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,7 +170,7 @@ HOME_HTML = """\
|
||||
}
|
||||
|
||||
initChart('chart-minute', '/api/kraken-minute', 60 * 1000);
|
||||
initChart('chart-hour', '/api/kraken-hour', 5 * 60 * 1000, updateMidnightPrice);
|
||||
initChart('chart-hour', '/api/kraken-half-hour', 5 * 60 * 1000, updateMidnightPrice);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
@@ -233,6 +233,11 @@ async def _proxy_btcdata_json(path: str) -> JSONResponse:
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/kraken-half-hour")
|
||||
async def kraken_half_hour():
|
||||
return await _proxy_btcdata_json("/api/kraken-half-hour")
|
||||
|
||||
|
||||
@app.get("/api/kraken-hour")
|
||||
async def kraken_hour():
|
||||
return await _proxy_btcdata_json("/api/kraken-hour")
|
||||
|
||||
@@ -107,3 +107,22 @@ def test_version_endpoint(monkeypatch):
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"version": "abc1234"}
|
||||
|
||||
|
||||
def test_24_hour_chart_uses_half_hour_candles():
|
||||
response = TestClient(main.app).get("/")
|
||||
assert "24h Candles (30m)" in response.text
|
||||
assert "initChart('chart-hour', '/api/kraken-half-hour', 5 * 60 * 1000, updateMidnightPrice)" in response.text
|
||||
assert "initChart('chart-minute', '/api/kraken-minute', 60 * 1000)" in response.text
|
||||
|
||||
|
||||
def test_half_hour_proxy_preserves_candles_and_upstream_errors(monkeypatch):
|
||||
for status, payload in [(200, [{"ts": 1800, "open": "10", "high": "12", "low": "9", "close": "11"}]), (503, {"error": "database unavailable"})]:
|
||||
request = httpx.Request("GET", "http://btcdata.test/api/kraken-half-hour")
|
||||
client = StubAsyncClient(response=httpx.Response(status, json=payload, request=request))
|
||||
monkeypatch.setattr(main, "BTCDATA_URL", "http://btcdata.test")
|
||||
monkeypatch.setattr(main.httpx, "AsyncClient", lambda: client)
|
||||
response = TestClient(main.app).get("/api/kraken-half-hour")
|
||||
assert response.status_code == status
|
||||
assert response.json() == payload
|
||||
assert client.requested_url == "http://btcdata.test/api/kraken-half-hour"
|
||||
|
||||
Reference in New Issue
Block a user