config modal: server-side settings (budget, anchor, bar width), removed localStorage

This commit is contained in:
Meleki committed 2026-02-23 06:55:40 +01:00
1 parent 59c492c04f
commit 6666129dfa
5 files changed
+157 -57

No files matched your search

+1
View File
@@ -10,3 +10,4 @@
/jsonl-sync/state.json
core
jsonl-sync/dashboard-config.json
+21
View File
@@ -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<Func<SqliteConnection>>()())
{
+56 -32
View File
@@ -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) + '%';
}
+27 -9
View File
@@ -10,15 +10,7 @@
<header>
<h1>Cost Analytics</h1>
<div id="summary"></div>
<div id="anchor-config">
<label>Anker:</label>
<select id="anchor-day">
<option value="0">So</option><option value="1">Mo</option><option value="2">Di</option>
<option value="3">Mi</option><option value="4">Do</option><option value="5">Fr</option>
<option value="6">Sa</option>
</select>
<select id="anchor-hour"></select>
</div>
<button id="btn-config" title="Einstellungen">⚙</button>
<button id="btn-sync" title="Neue Einträge importieren">↻ Sync</button>
</header>
@@ -47,6 +39,32 @@
</div>
</div>
<div id="config-modal" class="modal hidden">
<div class="modal-box config-box">
<button id="config-close">✕</button>
<h2>Einstellungen</h2>
<div class="config-grid">
<label>Wochen-Budget ($)</label>
<input type="number" id="cfg-budget" step="1" min="1" />
<label>Budget-Anker Wochentag</label>
<select id="cfg-anchor-day">
<option value="0">Sonntag</option><option value="1">Montag</option>
<option value="2">Dienstag</option><option value="3">Mittwoch</option>
<option value="4">Donnerstag</option><option value="5">Freitag</option>
<option value="6">Samstag</option>
</select>
<label>Anker Stunde</label>
<select id="cfg-anchor-hour"></select>
<label>Min. Balkenbreite (px)</label>
<input type="number" id="cfg-bar-width" step="1" min="4" max="60" />
</div>
<button id="cfg-save" class="cfg-save-btn">Speichern</button>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
+52 -16
View File
@@ -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; }