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

+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) + '%';
}