chain blocks: single-line layout, user message display, raw json fix, range label with time

This commit is contained in:
Meleki committed 2026-02-22 17:32:29 +01:00
1 parent 791dd5718c
commit 670c5b7f16
4 files changed
+160 -52

No files matched your search

+38 -36
View File
@@ -66,33 +66,28 @@ async function loadView() {
const to = viewTo.toISOString();
$('range-label').textContent =
`${fmtDate(viewFrom)} - ${fmtDate(viewTo)} (${entryMode ? 'Einträge' : ZOOM_LEVELS[zoomIndex].name})`;
`${fmtDateTime(viewFrom)} - ${fmtDateTime(viewTo)} (${entryMode ? 'Einträge' : ZOOM_LEVELS[zoomIndex].name})`;
$('btn-zoom-out').disabled = zoomStack.length === 0 && zoomIndex === 0 && !entryMode;
const [summary, chains] = await Promise.all([
api(`/api/cost-summary?from=${enc(from)}&to=${enc(to)}`),
api(`/api/cost-chains?from=${enc(from)}&to=${enc(to)}`),
]);
if (entryMode) {
const [entries, summary] = await Promise.all([
api(`/api/entry-chains?from=${enc(from)}&to=${enc(to)}`),
api(`/api/cost-summary?from=${enc(from)}&to=${enc(to)}`),
]);
const entries = await api(`/api/entry-chains?from=${enc(from)}&to=${enc(to)}`);
renderSummary(summary);
renderEntryChart(entries);
renderEntryList(entries);
} else {
const totalMinutes = (viewTo - viewFrom) / 60000;
const bucketMinutes = Math.max(1, Math.round(totalMinutes / BAR_COUNT));
const [buckets, summary] = await Promise.all([
api(`/api/cost-buckets?from=${enc(from)}&to=${enc(to)}&bucketMinutes=${bucketMinutes}`),
api(`/api/cost-summary?from=${enc(from)}&to=${enc(to)}`),
]);
const chains = await api(`/api/cost-chains?from=${enc(from)}&to=${enc(to)}`);
const buckets = await api(`/api/cost-buckets?from=${enc(from)}&to=${enc(to)}&bucketMinutes=${bucketMinutes}`);
renderSummary(summary);
renderBucketChart(buckets, bucketMinutes);
renderChainBlocks(chains);
}
renderChainBlocks(chains);
saveToHash();
}
@@ -127,7 +122,7 @@ function renderBucketChart(buckets, bucketMinutes) {
const wrapper = document.createElement('div');
wrapper.className = 'bar-wrapper';
wrapper.innerHTML = `
<div class="bar-tooltip">${pctScaled(cost)} / ${count}</div>
<div class="bar-tooltip">${pct(cost)} / ${count}</div>
<div class="bar" style="height:${Math.max(barPct, cost > 0 ? 2 : 0)}%"></div>
`;
@@ -228,26 +223,20 @@ function renderChainBlocks(chains) {
chains.forEach((c, ci) => {
const block = document.createElement('div');
block.className = 'block';
const first = new Date(c.first);
const last = new Date(c.last);
const durSec = Math.round((last - first) / 1000);
block.innerHTML = `
<div class="block-header">
<span class="block-label">${fmtShort(new Date(c.first))} - ${fmtShort(new Date(c.last))}</span>
<span class="block-stats">
<span class="block-count">${c.count} Einträge</span>
<span class="block-cost">${pct(c.totalCost)}</span>
</span>
<span class="block-date">${fmtDateTime(first)}</span>
<span class="block-dur">${durSec}s</span>
<span class="block-msg">${escHtml(c.userMessage || '-')}</span>
<span class="block-count">${c.count}</span>
<span class="block-cost">${pct(c.totalCost)}</span>
</div>
`;
block.querySelector('.block-header').addEventListener('click', () => {
// Jump directly to entry mode, centered on this chain
const first = new Date(c.first);
const last = new Date(c.last);
const pad = Math.max((last - first) * 0.5, 60000); // min 1 min padding
zoomStack.push({ from: new Date(viewFrom), to: new Date(viewTo), zoom: zoomIndex, entry: false });
viewFrom = new Date(first.getTime() - pad);
viewTo = new Date(last.getTime() + pad);
zoomIndex = ZOOM_LEVELS.length - 1;
entryMode = true;
loadView();
openModal(null, null, c.chainId);
});
container.appendChild(block);
});
@@ -372,19 +361,22 @@ async function syncData() {
}
// --- Modal ---
async function openModal(dataStr, clickedId) {
async function openModal(dataStr, clickedId, chainId) {
const chainEl = $('modal-chain');
const rawEl = $('modal-content');
try { rawEl.textContent = JSON.stringify(JSON.parse(dataStr), null, 2); }
catch { rawEl.textContent = dataStr; }
if (dataStr) {
try { rawEl.textContent = JSON.stringify(JSON.parse(dataStr), null, 2); }
catch { rawEl.textContent = dataStr; }
} else {
rawEl.textContent = '';
}
chainEl.innerHTML = '<p style="color:var(--muted)">Lade Kontext...</p>';
$('modal').classList.remove('hidden');
try {
const parsed = JSON.parse(dataStr);
const entryId = parsed.id;
const entryId = chainId || JSON.parse(dataStr).id;
const chain = await api(`/api/entry-chain/${encodeURIComponent(entryId)}`);
// Calculate chain total
@@ -397,6 +389,11 @@ async function openModal(dataStr, clickedId) {
const chainTotal = chainCosts.reduce((a, b) => a + b, 0);
const chainCount = chainCosts.filter(c => c > 0).length;
// Fill raw JSON if not set (e.g. opened from chain block)
if (!dataStr) {
rawEl.textContent = JSON.stringify(chain.map(e => JSON.parse(e.data)), null, 2);
}
let html = `<div class="chain-summary">
<span class="chain-summary-cost">Kette: ${pct(chainTotal)}</span>
<span class="chain-summary-count">${chainCount} Einträge mit Kosten / ${chain.length} gesamt</span>
@@ -461,6 +458,11 @@ function fmtShort(d) {
+ ' ' + d.toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit' });
}
function fmtDateTime(d) {
return d.toLocaleDateString('de-AT', { day: '2-digit', month: '2-digit', year: 'numeric' })
+ ' ' + d.toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function fmtRange(a, b) { return fmtShort(a) + ' - ' + fmtShort(b); }
function fmtTime(ts) {