From 59c492c04f32457a666f964d75a57993e23b3443 Mon Sep 17 00:00:00 2001 From: Meleki Date: Mon, 23 Feb 2026 06:32:08 +0100 Subject: [PATCH] dynamic bar count (multiples of 28), header overflow fix, parentId index for faster chain lookups --- Program.cs | 8 ++++++++ wwwroot/app.js | 22 +++++++++++++++++----- wwwroot/style.css | 6 ++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Program.cs b/Program.cs index 90b2f46..05bf908 100644 --- a/Program.cs +++ b/Program.cs @@ -15,6 +15,14 @@ var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); +// Ensure indexes for chain lookups +using (var db = app.Services.GetRequiredService>()()) +{ + db.Open(); + db.Execute("CREATE INDEX IF NOT EXISTS idx_entries_parentId ON entries(json_extract(data, '$.parentId'))"); +} + + // GET /api/cost-buckets?from=&to=&bucketMinutes= // Returns N buckets of aggregated cost app.MapGet("/api/cost-buckets", (Func factory, string from, string to, int bucketMinutes) => diff --git a/wwwroot/app.js b/wwwroot/app.js index 7fe0f3b..f36565c 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -11,7 +11,13 @@ const WEEKLY_BUDGET = 500; // $ pro Woche // Anchor config - loaded from /api/settings, defaults: Thursday 23:00 let ANCHOR_DAY = 0; // 0=Sun, 1=Mon, ..., 4=Thu let ANCHOR_HOUR = 0; -const BAR_COUNT = 50; +function getBarCount() { + const w = window.innerWidth || document.documentElement.clientWidth; + // ~12px per bar minimum, count must be divisible by 14 + const raw = Math.floor(w / 12); + const count = Math.floor(raw / 28) * 28; + return Math.max(28, count); +} const CHAIN_COLORS = [ '#4ade80','#60a5fa','#f97316','#c084fc','#f472b6', '#facc15','#34d399','#818cf8','#fb7185','#a78bfa', @@ -104,6 +110,12 @@ async function init() { restoreFromHash(); await loadView(); + + let resizeTimer; + window.addEventListener('resize', () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => loadView(), 250); + }); } function saveToHash() { @@ -147,7 +159,7 @@ async function loadView() { renderEntryChart(entries); } else { const totalMinutes = (viewTo - viewFrom) / 60000; - const bucketMinutes = Math.max(1, Math.round(totalMinutes / BAR_COUNT)); + const bucketMinutes = Math.max(1, Math.round(totalMinutes / getBarCount())); const buckets = await api(`/api/cost-buckets?from=${enc(from)}&to=${enc(to)}&bucketMinutes=${bucketMinutes}`); renderSummary(summary); renderBucketChart(buckets, bucketMinutes); @@ -181,7 +193,7 @@ function renderBucketChart(buckets, bucketMinutes) { let lastWeekId = null; let lastDayId = null; - for (let i = 0; i < BAR_COUNT; i++) { + for (let i = 0; i < getBarCount(); i++) { const b = bucketMap[i]; const cost = b ? b.cost : 0; const count = b ? b.count : 0; @@ -226,7 +238,7 @@ function renderBucketChart(buckets, bucketMinutes) { const lbl = document.createElement('div'); lbl.className = 'chart-label'; - if (i % Math.ceil(BAR_COUNT / 6) === 0) lbl.textContent = fmtShort(bucketStart); + if (i % Math.ceil(getBarCount() / 6) === 0) lbl.textContent = fmtShort(bucketStart); labels.appendChild(lbl); } } @@ -332,7 +344,7 @@ function renderChainBlocks(chains) { // Already at max zoom (1 Tag) - enter entry mode for the bucket containing this chain const chainMid = new Date((new Date(c.first).getTime() + new Date(c.last).getTime()) / 2); const totalMinutes = (viewTo - viewFrom) / 60000; - const bucketMinutes = Math.max(1, Math.round(totalMinutes / BAR_COUNT)); + const bucketMinutes = Math.max(1, Math.round(totalMinutes / getBarCount())); const bucketMs = bucketMinutes * 60000; const elapsed = chainMid - viewFrom; const bucketIndex = Math.floor(elapsed / bucketMs); diff --git a/wwwroot/style.css b/wwwroot/style.css index 7da3382..5ec32f3 100644 --- a/wwwroot/style.css +++ b/wwwroot/style.css @@ -30,8 +30,12 @@ header { align-items: center; gap: 24px; flex-shrink: 0; + overflow-x: auto; + flex-wrap: nowrap; } +header > * { flex-shrink: 0; } + header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); } #anchor-config { @@ -197,8 +201,6 @@ header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); } font-size: 0.55rem; color: var(--muted); text-align: center; - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; }