Author SHA1 Message Date
magnus 0a27d01f12 Show 30-minute candles on the last-24-hours chart
pull-request / validate (pull_request) Successful in 2m34s
2026-11-03 10:32:23 +01:00
magnus 803df14813 Merge pull request 'Retry the Harbor manifest copy instead of a probe for it' (#3) from fix-harbor-tag-retry into main
build / build (push) Successful in 2m56s
Reviewed-on: #3
2026-10-11 07:58:50 +00:00
magnusandClaude Opus 5 7ea25ee4fd Retry the Harbor manifest copy instead of a probe for it
pull-request / validate (pull_request) Successful in 32s
The companion SHA tag step failed on the last build even though the image had
already been pushed and deployed. Harbor's vulnerability policy blocks manifest
reads while Trivy's scan is Running, and `imagetools create` copies the manifest,
so Harbor answered 412 Precondition Failed.

Two faults compounded. The loop gated on `imagetools inspect`, but a probe passing
is no guarantee the copy will be allowed. And `create` sat outside the condition,
so under the default `bash -e` its first failure aborted the step before `exit 0`
or any further attempt: the log shows one waiting line, then the error, with 58 of
60 retries unused.

Retry the real operation instead, inside the condition so a blocked copy is a
reason to wait rather than fatal. The trade-off is that a permanently failing copy
now takes the full ten minutes to report instead of failing fast; the previous
behaviour failed fast but wrongly, on a transient block.

Also records that only the primary tag matters for rollout, since the Flux image
policy matches the timestamp tag, so a failure in this step costs the audit tag
rather than the deployment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-11 09:56:26 +02:00
magnus ac01388117 Merge pull request 'Recover from a btcdata restart without reloading the page' (#2) from ws-reconnect into main
build / build (push) Failing after 3m11s
Reviewed-on: #2
2026-10-11 07:26:42 +00:00
6 changed files with 53 additions and 4 deletions
+11 -2
View File
@@ -66,14 +66,23 @@ jobs:
cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max 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 - name: Wait for Harbor policy, then add SHA tag
shell: bash
env: env:
PRIMARY: ${{ steps.tags.outputs.primary }} PRIMARY: ${{ steps.tags.outputs.primary }}
QUALIFIED: ${{ steps.tags.outputs.qualified }} QUALIFIED: ${{ steps.tags.outputs.qualified }}
run: | 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 for attempt in $(seq 1 60); do
if docker buildx imagetools inspect "$PRIMARY" >/dev/null 2>&1; then if docker buildx imagetools create --tag "$QUALIFIED" "$PRIMARY"; then
docker buildx imagetools create --tag "$QUALIFIED" "$PRIMARY" echo "tagged $QUALIFIED on attempt $attempt"
exit 0 exit 0
fi fi
echo "waiting for Harbor scan/pull policy ($attempt/60)" echo "waiting for Harbor scan/pull policy ($attempt/60)"
+6
View File
@@ -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 runner, then publishes immutable linux/amd64 images to
`harbor.pmagnus.cloud/btcprice/btcprice`. It embeds the short Git SHA as `APP_VERSION`. `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 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 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 one on citadel—and is served at https://btcprice.k8s.pmagnus.cloud through ingress-nginx and
+6
View File
@@ -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 runner, then publishes immutable linux/amd64 images to
`harbor.pmagnus.cloud/btcprice/btcprice`. It embeds the short Git SHA as `APP_VERSION`. `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 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 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 one on citadel—and is served at https://btcprice.k8s.pmagnus.cloud through ingress-nginx and
+4
View File
@@ -127,3 +127,7 @@ color-coded (green up, red down). Reconnection is handled by HTMX.
Python · FastAPI · Uvicorn · websockets · HTMX · Tailwind CSS v4 Python · FastAPI · Uvicorn · websockets · HTMX · Tailwind CSS v4
The Clojure implementation this replaced is on the `clojure` branch. 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
View File
@@ -73,7 +73,7 @@ HOME_HTML = """\
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6"> <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 id="chart-hour" style="height:300px"></div>
</div> </div>
</div> </div>
@@ -170,7 +170,7 @@ HOME_HTML = """\
} }
initChart('chart-minute', '/api/kraken-minute', 60 * 1000); 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> </script>
</body> </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") @app.get("/api/kraken-hour")
async def kraken_hour(): async def kraken_hour():
return await _proxy_btcdata_json("/api/kraken-hour") return await _proxy_btcdata_json("/api/kraken-hour")
+19
View File
@@ -107,3 +107,22 @@ def test_version_endpoint(monkeypatch):
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"version": "abc1234"} 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"