From 6666129dfacb2c578cffdaab30fcfaaeb880a2e7 Mon Sep 17 00:00:00 2001 From: Meleki Date: Mon, 23 Feb 2026 06:55:40 +0100 Subject: [PATCH] config modal: server-side settings (budget, anchor, bar width), removed localStorage --- .gitignore | 1 + Program.cs | 21 +++++++++++ wwwroot/app.js | 88 +++++++++++++++++++++++++++++----------------- wwwroot/index.html | 36 ++++++++++++++----- wwwroot/style.css | 68 ++++++++++++++++++++++++++--------- 5 files changed, 157 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 22edf10..faa8dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /jsonl-sync/state.json core +jsonl-sync/dashboard-config.json diff --git a/Program.cs b/Program.cs index 05bf908..15b9ea9 100644 --- a/Program.cs +++ b/Program.cs @@ -15,6 +15,27 @@ var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); +var configPath = Path.Combine(Path.GetDirectoryName(dbPath)!, "dashboard-config.json"); + +// GET /api/config +app.MapGet("/api/config", () => +{ + if (File.Exists(configPath)) + return Results.Text(File.ReadAllText(configPath), "application/json"); + return Results.Text("{}", "application/json"); +}); + +// PUT /api/config +app.MapPut("/api/config", async (HttpRequest req) => +{ + using var reader = new StreamReader(req.Body); + var json = await reader.ReadToEndAsync(); + // Validate it's valid JSON + System.Text.Json.JsonDocument.Parse(json); + await File.WriteAllTextAsync(configPath, json); + return Results.Ok(new { ok = true }); +}); + // Ensure indexes for chain lookups using (var db = app.Services.GetRequiredService>()()) { diff --git a/wwwroot/app.js b/wwwroot/app.js index f36565c..a864a81 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -6,15 +6,13 @@ const ZOOM_LEVELS = [ { name: '1 Woche', weeks: 1 }, { name: '1 Tag', days: 1 } ]; -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; +// Config - loaded from server +let CFG = { weeklyBudget: 500, anchorDay: 0, anchorHour: 0, minBarWidth: 24 }; + 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 raw = Math.floor(w / CFG.minBarWidth); const count = Math.floor(raw / 28) * 28; return Math.max(28, count); } @@ -33,9 +31,9 @@ let entryMode = false; function getLastAnchor(date) { const d = new Date(date); // Walk back to the anchor weekday - const diff = (d.getDay() - ANCHOR_DAY + 7) % 7; + const diff = (d.getDay() - CFG.anchorDay + 7) % 7; d.setDate(d.getDate() - diff); - d.setHours(ANCHOR_HOUR, 0, 0, 0); + d.setHours(CFG.anchorHour, 0, 0, 0); // If we landed in the future, go back one week if (d > date) d.setDate(d.getDate() - 7); return d; @@ -72,11 +70,15 @@ async function init() { $('btn-sync').addEventListener('click', syncData); $('modal-close').addEventListener('click', closeModal); $('modal').addEventListener('click', e => { if (e.target === $('modal')) closeModal(); }); - document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); - // Anchor dropdowns - const daySelect = $('anchor-day'); - const hourSelect = $('anchor-hour'); + // Config modal + $('btn-config').addEventListener('click', openConfigModal); + $('config-close').addEventListener('click', closeConfigModal); + $('config-modal').addEventListener('click', e => { if (e.target === $('config-modal')) closeConfigModal(); }); + $('cfg-save').addEventListener('click', saveConfig); + + // Populate hour dropdown + const hourSelect = $('cfg-anchor-hour'); for (let h = 0; h < 24; h++) { const opt = document.createElement('option'); opt.value = h; @@ -84,24 +86,18 @@ async function init() { hourSelect.appendChild(opt); } - // Load from localStorage - const savedDay = localStorage.getItem('anchorDay'); - const savedHour = localStorage.getItem('anchorHour'); - if (savedDay != null) ANCHOR_DAY = parseInt(savedDay); - if (savedHour != null) ANCHOR_HOUR = parseInt(savedHour); - daySelect.value = ANCHOR_DAY; - hourSelect.value = ANCHOR_HOUR; + document.addEventListener('keydown', e => { + if (e.key === 'Escape') { closeModal(); closeConfigModal(); } + }); - daySelect.addEventListener('change', () => { - ANCHOR_DAY = parseInt(daySelect.value); - localStorage.setItem('anchorDay', ANCHOR_DAY); - resetToStart(); - }); - hourSelect.addEventListener('change', () => { - ANCHOR_HOUR = parseInt(hourSelect.value); - localStorage.setItem('anchorHour', ANCHOR_HOUR); - resetToStart(); - }); + // Load config from server + try { + const serverCfg = await api('/api/config'); + if (serverCfg.weeklyBudget != null) CFG.weeklyBudget = serverCfg.weeklyBudget; + if (serverCfg.anchorDay != null) CFG.anchorDay = serverCfg.anchorDay; + if (serverCfg.anchorHour != null) CFG.anchorHour = serverCfg.anchorHour; + if (serverCfg.minBarWidth != null) CFG.minBarWidth = serverCfg.minBarWidth; + } catch {} zoomIndex = 0; const range = getAnchoredRange(zoomIndex); @@ -118,6 +114,34 @@ async function init() { }); } +function openConfigModal() { + $('cfg-budget').value = CFG.weeklyBudget; + $('cfg-anchor-day').value = CFG.anchorDay; + $('cfg-anchor-hour').value = CFG.anchorHour; + $('cfg-bar-width').value = CFG.minBarWidth; + $('config-modal').classList.remove('hidden'); +} + +function closeConfigModal() { + $('config-modal').classList.add('hidden'); +} + +async function saveConfig() { + CFG.weeklyBudget = Number($('cfg-budget').value) || 500; + CFG.anchorDay = parseInt($('cfg-anchor-day').value); + CFG.anchorHour = parseInt($('cfg-anchor-hour').value); + CFG.minBarWidth = Number($('cfg-bar-width').value) || 12; + + await fetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(CFG) + }); + + closeConfigModal(); + resetToStart(); +} + function saveToHash() { const state = { f: viewFrom.toISOString(), @@ -207,7 +231,7 @@ function renderBucketChart(buckets, bucketMinutes) { const weekId = weekAnchor.getTime(); // Use local time for day boundary at anchor hour const localDayStart = new Date(bucketStart); - localDayStart.setHours(localDayStart.getHours() - ANCHOR_HOUR); + localDayStart.setHours(localDayStart.getHours() - CFG.anchorHour); const dayId = localDayStart.toLocaleDateString(); const isNewWeek = lastWeekId !== null && weekId !== lastWeekId; const showDaySep = ZOOM_LEVELS[zoomIndex].weeks === 1; @@ -651,11 +675,11 @@ function escAttr(s) { } function pct(cost) { - return (cost / WEEKLY_BUDGET * 100).toFixed(3) + '%'; + return (cost / CFG.weeklyBudget * 100).toFixed(3) + '%'; } function pctScaled(cost) { const days = (viewTo - viewFrom) / 86400000; - const budget = WEEKLY_BUDGET / 7 * days; + const budget = CFG.weeklyBudget / 7 * days; return (cost / budget * 100).toFixed(3) + '%'; } diff --git a/wwwroot/index.html b/wwwroot/index.html index 7ed6006..a1f8ca0 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -10,15 +10,7 @@

Cost Analytics

-
- - - -
+
@@ -47,6 +39,32 @@ + + diff --git a/wwwroot/style.css b/wwwroot/style.css index 5ec32f3..7203fb0 100644 --- a/wwwroot/style.css +++ b/wwwroot/style.css @@ -38,22 +38,17 @@ header > * { flex-shrink: 0; } header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); } -#anchor-config { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.8rem; - color: var(--muted); -} - -#anchor-config select { - background: var(--surface); +#btn-config { + background: var(--border); color: var(--text); - border: 1px solid var(--border); - border-radius: 4px; - padding: 2px 4px; - font-size: 0.8rem; + border: none; + padding: 5px; + border-radius: 6px; + cursor: pointer; + transition: background 0.15s, color 0.15s; + line-height: 1; } +#btn-config:hover { background: var(--accent) } #btn-sync { background: var(--border); @@ -77,7 +72,7 @@ header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); } } #summary .val { color: var(--text); font-weight: 600; } -#summary .cost { color: var(--green); font-weight: 700; font-size: 1rem; } +#summary .cost { color: var(--green); font-weight: 700; } #toolbar { padding: 10px 24px; @@ -329,7 +324,7 @@ header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); } position: relative; } -#modal-close { +#modal-close, #config-close { position: absolute; top: 12px; right: 14px; background: none; border: none; color: var(--muted); font-size: 1.2rem; cursor: pointer; @@ -448,6 +443,47 @@ header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); } margin-top: 8px; } +/* Config modal */ +.config-box { max-width: 420px; } +.config-box h2 { font-size: 1rem; margin-bottom: 16px; color: var(--accent); } + +.config-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 16px; + align-items: center; +} + +.config-grid label { + font-size: 0.82rem; + color: var(--muted); +} + +.config-grid input, +.config-grid select { + background: var(--bg); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 10px; + font-size: 0.85rem; +} + +.cfg-save-btn { + margin-top: 20px; + width: 100%; + padding: 8px; + background: var(--accent); + color: #fff; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + transition: opacity 0.15s; +} +.cfg-save-btn:hover { opacity: 0.85; } + ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }