Show midnight price change in page title #1
@@ -4,8 +4,9 @@ export
|
|||||||
VENV := .venv
|
VENV := .venv
|
||||||
UVICORN := $(VENV)/bin/uvicorn
|
UVICORN := $(VENV)/bin/uvicorn
|
||||||
TEST_DEPS := $(VENV)/.test-deps
|
TEST_DEPS := $(VENV)/.test-deps
|
||||||
|
K3S_BTCDATA_PORT := 4100
|
||||||
|
|
||||||
.PHONY: clean run run-local run-citadel tailwind test venv
|
.PHONY: clean run run-local run-citadel run-k3s tailwind test venv
|
||||||
|
|
||||||
venv: $(UVICORN)
|
venv: $(UVICORN)
|
||||||
|
|
||||||
@@ -26,6 +27,12 @@ run-local: venv
|
|||||||
run-citadel: venv
|
run-citadel: venv
|
||||||
BTCDATA_URL=$(BTCDATA_URL) $(UVICORN) app.main:app --host 0.0.0.0 --port 4000 --reload
|
BTCDATA_URL=$(BTCDATA_URL) $(UVICORN) app.main:app --host 0.0.0.0 --port 4000 --reload
|
||||||
|
|
||||||
|
run-k3s: venv
|
||||||
|
kubectl -n btcdata port-forward service/btcdata $(K3S_BTCDATA_PORT):4101 & \
|
||||||
|
port_forward_pid=$$!; \
|
||||||
|
trap 'kill $$port_forward_pid' EXIT INT TERM; \
|
||||||
|
BTCDATA_URL=http://127.0.0.1:$(K3S_BTCDATA_PORT) $(UVICORN) app.main:app --host 0.0.0.0 --port 4000 --reload
|
||||||
|
|
||||||
tailwind:
|
tailwind:
|
||||||
npm run css:watch
|
npm run css:watch
|
||||||
|
|
||||||
|
|||||||
+42
-4
@@ -47,6 +47,7 @@ HOME_HTML = """\
|
|||||||
<div class="mx-auto max-w-lg px-4 py-6">
|
<div class="mx-auto max-w-lg px-4 py-6">
|
||||||
<header class="text-center mb-8">
|
<header class="text-center mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-900">BTC Price</h1>
|
<h1 class="text-3xl font-bold text-gray-900">BTC Price</h1>
|
||||||
|
<p id="midnight-change" class="mt-1 text-sm text-gray-400"> </p>
|
||||||
</header>
|
</header>
|
||||||
<div id="price-panel" hx-ext="ws" ws-connect="/ws/price">
|
<div id="price-panel" hx-ext="ws" ws-connect="/ws/price">
|
||||||
<div id="price-display">
|
<div id="price-display">
|
||||||
@@ -78,14 +79,50 @@ HOME_HTML = """\
|
|||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
let currentPrice = null;
|
||||||
|
let midnightPrice = null;
|
||||||
|
|
||||||
|
function formatPriceChange(change) {
|
||||||
|
const sign = change > 0 ? '+' : change < 0 ? '-' : '±';
|
||||||
|
const amount = Math.abs(change).toLocaleString('da-DK', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
return sign + '$' + amount;
|
||||||
|
}
|
||||||
|
|
||||||
function updateTitleFromPrice() {
|
function updateTitleFromPrice() {
|
||||||
const priceDisplay = document.getElementById('price-display');
|
const priceDisplay = document.getElementById('price-display');
|
||||||
const price = priceDisplay && priceDisplay.dataset.titlePrice;
|
const price = priceDisplay && priceDisplay.dataset.titlePrice;
|
||||||
if (price) {
|
const priceValue = priceDisplay && Number(priceDisplay.dataset.priceValue);
|
||||||
|
if (price && Number.isFinite(priceValue)) {
|
||||||
|
currentPrice = priceValue;
|
||||||
|
}
|
||||||
|
if (price && currentPrice !== null && midnightPrice !== null) {
|
||||||
|
const change = currentPrice - midnightPrice;
|
||||||
|
const formattedChange = formatPriceChange(change);
|
||||||
|
document.title = price + ' | ' + formattedChange;
|
||||||
|
const midnightChange = document.getElementById('midnight-change');
|
||||||
|
midnightChange.textContent = formattedChange;
|
||||||
|
midnightChange.className = 'mt-1 text-sm font-bold ' + (
|
||||||
|
change > 0 ? 'text-green-600' : change < 0 ? 'text-red-600' : 'text-gray-500'
|
||||||
|
);
|
||||||
|
} else if (price) {
|
||||||
document.title = price + ' | BTC Price';
|
document.title = price + ' | BTC Price';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMidnightPrice(candles) {
|
||||||
|
const midnight = new Date();
|
||||||
|
midnight.setHours(0, 0, 0, 0);
|
||||||
|
const midnightSeconds = Math.floor(midnight.getTime() / 1000);
|
||||||
|
const firstToday = candles.find(candle => candle.time >= midnightSeconds);
|
||||||
|
if (firstToday) {
|
||||||
|
midnightPrice = firstToday.open;
|
||||||
|
updateTitleFromPrice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pricePanel = document.getElementById('price-panel');
|
const pricePanel = document.getElementById('price-panel');
|
||||||
new MutationObserver(updateTitleFromPrice).observe(pricePanel, {
|
new MutationObserver(updateTitleFromPrice).observe(pricePanel, {
|
||||||
childList: true,
|
childList: true,
|
||||||
@@ -93,7 +130,7 @@ HOME_HTML = """\
|
|||||||
});
|
});
|
||||||
updateTitleFromPrice();
|
updateTitleFromPrice();
|
||||||
|
|
||||||
function initChart(containerId, endpoint, intervalMs) {
|
function initChart(containerId, endpoint, intervalMs, onData) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
const chart = LightweightCharts.createChart(container, {
|
const chart = LightweightCharts.createChart(container, {
|
||||||
layout: { background: { type: 'solid', color: '#ffffff' }, textColor: '#6b7280' },
|
layout: { background: { type: 'solid', color: '#ffffff' }, textColor: '#6b7280' },
|
||||||
@@ -118,6 +155,7 @@ HOME_HTML = """\
|
|||||||
close: parseFloat(c.close),
|
close: parseFloat(c.close),
|
||||||
})).sort((a, b) => a.time - b.time);
|
})).sort((a, b) => a.time - b.time);
|
||||||
series.setData(data);
|
series.setData(data);
|
||||||
|
if (onData) onData(data);
|
||||||
chart.timeScale().fitContent();
|
chart.timeScale().fitContent();
|
||||||
})
|
})
|
||||||
.catch(e => console.error('Chart fetch error:', e));
|
.catch(e => console.error('Chart fetch error:', e));
|
||||||
@@ -130,7 +168,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);
|
initChart('chart-hour', '/api/kraken-hour', 5 * 60 * 1000, updateMidnightPrice);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -250,7 +288,7 @@ def price_json_to_html(text: str) -> str:
|
|||||||
|
|
||||||
formatted_price = f'${da_fmt(",.2f", p)}'
|
formatted_price = f'${da_fmt(",.2f", p)}'
|
||||||
html = (
|
html = (
|
||||||
f'<div id="price-display" data-title-price="{formatted_price}">'
|
f'<div id="price-display" data-title-price="{formatted_price}" data-price-value="{p}">'
|
||||||
'<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">'
|
||||||
'<div class="text-center">'
|
'<div class="text-center">'
|
||||||
f'<p class="text-4xl font-bold {color}">{formatted_price}</p>'
|
f'<p class="text-4xl font-bold {color}">{formatted_price}</p>'
|
||||||
|
|||||||
+11
-1
@@ -37,7 +37,16 @@ def test_home_updates_title_from_latest_price():
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "new MutationObserver(updateTitleFromPrice)" in response.text
|
assert "new MutationObserver(updateTitleFromPrice)" in response.text
|
||||||
assert "document.title = price + ' | BTC Price'" in response.text
|
assert "currentPrice - midnightPrice" in response.text
|
||||||
|
assert "document.title = price + ' | ' + formattedChange" in response.text
|
||||||
|
assert '<h1 class="text-3xl font-bold text-gray-900">BTC Price</h1>' in response.text
|
||||||
|
assert "page-title" not in response.text
|
||||||
|
assert 'id="midnight-change"' in response.text
|
||||||
|
assert "midnightChange.textContent = formattedChange" in response.text
|
||||||
|
assert "since 00:00" not in response.text
|
||||||
|
assert "change > 0 ? 'text-green-600' : change < 0 ? 'text-red-600'" in response.text
|
||||||
|
assert "formatPriceChange(change)" in response.text
|
||||||
|
assert "midnight.setHours(0, 0, 0, 0)" in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_price_fragment_exposes_formatted_price_for_title():
|
def test_price_fragment_exposes_formatted_price_for_title():
|
||||||
@@ -46,6 +55,7 @@ def test_price_fragment_exposes_formatted_price_for_title():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert 'data-title-price="$67.432,10"' in html
|
assert 'data-title-price="$67.432,10"' in html
|
||||||
|
assert 'data-price-value="67432.1"' in html
|
||||||
assert ">$67.432,10</p>" in html
|
assert ">$67.432,10</p>" in html
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user