config modal: server-side settings (budget, anchor, bar width), removed localStorage
This commit is contained in:
1 parent
59c492c04f
commit
6666129dfa
5 files changed
+157
-57
No files matched your search
@@ -10,3 +10,4 @@
|
|||||||
/jsonl-sync/state.json
|
/jsonl-sync/state.json
|
||||||
|
|
||||||
core
|
core
|
||||||
|
jsonl-sync/dashboard-config.json
|
||||||
+21
@@ -15,6 +15,27 @@ var app = builder.Build();
|
|||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
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
|
// Ensure indexes for chain lookups
|
||||||
using (var db = app.Services.GetRequiredService<Func<SqliteConnection>>()())
|
using (var db = app.Services.GetRequiredService<Func<SqliteConnection>>()())
|
||||||
{
|
{
|
||||||
|
|||||||
+56
-32
@@ -6,15 +6,13 @@ const ZOOM_LEVELS = [
|
|||||||
{ name: '1 Woche', weeks: 1 },
|
{ name: '1 Woche', weeks: 1 },
|
||||||
{ name: '1 Tag', days: 1 }
|
{ name: '1 Tag', days: 1 }
|
||||||
];
|
];
|
||||||
const WEEKLY_BUDGET = 500; // $ pro Woche
|
|
||||||
|
|
||||||
// Anchor config - loaded from /api/settings, defaults: Thursday 23:00
|
// Config - loaded from server
|
||||||
let ANCHOR_DAY = 0; // 0=Sun, 1=Mon, ..., 4=Thu
|
let CFG = { weeklyBudget: 500, anchorDay: 0, anchorHour: 0, minBarWidth: 24 };
|
||||||
let ANCHOR_HOUR = 0;
|
|
||||||
function getBarCount() {
|
function getBarCount() {
|
||||||
const w = window.innerWidth || document.documentElement.clientWidth;
|
const w = window.innerWidth || document.documentElement.clientWidth;
|
||||||
// ~12px per bar minimum, count must be divisible by 14
|
const raw = Math.floor(w / CFG.minBarWidth);
|
||||||
const raw = Math.floor(w / 12);
|
|
||||||
const count = Math.floor(raw / 28) * 28;
|
const count = Math.floor(raw / 28) * 28;
|
||||||
return Math.max(28, count);
|
return Math.max(28, count);
|
||||||
}
|
}
|
||||||
@@ -33,9 +31,9 @@ let entryMode = false;
|
|||||||
function getLastAnchor(date) {
|
function getLastAnchor(date) {
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
// Walk back to the anchor weekday
|
// 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.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 we landed in the future, go back one week
|
||||||
if (d > date) d.setDate(d.getDate() - 7);
|
if (d > date) d.setDate(d.getDate() - 7);
|
||||||
return d;
|
return d;
|
||||||
@@ -72,11 +70,15 @@ async function init() {
|
|||||||
$('btn-sync').addEventListener('click', syncData);
|
$('btn-sync').addEventListener('click', syncData);
|
||||||
$('modal-close').addEventListener('click', closeModal);
|
$('modal-close').addEventListener('click', closeModal);
|
||||||
$('modal').addEventListener('click', e => { if (e.target === $('modal')) closeModal(); });
|
$('modal').addEventListener('click', e => { if (e.target === $('modal')) closeModal(); });
|
||||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
|
||||||
|
|
||||||
// Anchor dropdowns
|
// Config modal
|
||||||
const daySelect = $('anchor-day');
|
$('btn-config').addEventListener('click', openConfigModal);
|
||||||
const hourSelect = $('anchor-hour');
|
$('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++) {
|
for (let h = 0; h < 24; h++) {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = h;
|
opt.value = h;
|
||||||
@@ -84,24 +86,18 @@ async function init() {
|
|||||||
hourSelect.appendChild(opt);
|
hourSelect.appendChild(opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load from localStorage
|
document.addEventListener('keydown', e => {
|
||||||
const savedDay = localStorage.getItem('anchorDay');
|
if (e.key === 'Escape') { closeModal(); closeConfigModal(); }
|
||||||
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;
|
|
||||||
|
|
||||||
daySelect.addEventListener('change', () => {
|
// Load config from server
|
||||||
ANCHOR_DAY = parseInt(daySelect.value);
|
try {
|
||||||
localStorage.setItem('anchorDay', ANCHOR_DAY);
|
const serverCfg = await api('/api/config');
|
||||||
resetToStart();
|
if (serverCfg.weeklyBudget != null) CFG.weeklyBudget = serverCfg.weeklyBudget;
|
||||||
});
|
if (serverCfg.anchorDay != null) CFG.anchorDay = serverCfg.anchorDay;
|
||||||
hourSelect.addEventListener('change', () => {
|
if (serverCfg.anchorHour != null) CFG.anchorHour = serverCfg.anchorHour;
|
||||||
ANCHOR_HOUR = parseInt(hourSelect.value);
|
if (serverCfg.minBarWidth != null) CFG.minBarWidth = serverCfg.minBarWidth;
|
||||||
localStorage.setItem('anchorHour', ANCHOR_HOUR);
|
} catch {}
|
||||||
resetToStart();
|
|
||||||
});
|
|
||||||
|
|
||||||
zoomIndex = 0;
|
zoomIndex = 0;
|
||||||
const range = getAnchoredRange(zoomIndex);
|
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() {
|
function saveToHash() {
|
||||||
const state = {
|
const state = {
|
||||||
f: viewFrom.toISOString(),
|
f: viewFrom.toISOString(),
|
||||||
@@ -207,7 +231,7 @@ function renderBucketChart(buckets, bucketMinutes) {
|
|||||||
const weekId = weekAnchor.getTime();
|
const weekId = weekAnchor.getTime();
|
||||||
// Use local time for day boundary at anchor hour
|
// Use local time for day boundary at anchor hour
|
||||||
const localDayStart = new Date(bucketStart);
|
const localDayStart = new Date(bucketStart);
|
||||||
localDayStart.setHours(localDayStart.getHours() - ANCHOR_HOUR);
|
localDayStart.setHours(localDayStart.getHours() - CFG.anchorHour);
|
||||||
const dayId = localDayStart.toLocaleDateString();
|
const dayId = localDayStart.toLocaleDateString();
|
||||||
const isNewWeek = lastWeekId !== null && weekId !== lastWeekId;
|
const isNewWeek = lastWeekId !== null && weekId !== lastWeekId;
|
||||||
const showDaySep = ZOOM_LEVELS[zoomIndex].weeks === 1;
|
const showDaySep = ZOOM_LEVELS[zoomIndex].weeks === 1;
|
||||||
@@ -651,11 +675,11 @@ function escAttr(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pct(cost) {
|
function pct(cost) {
|
||||||
return (cost / WEEKLY_BUDGET * 100).toFixed(3) + '%';
|
return (cost / CFG.weeklyBudget * 100).toFixed(3) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
function pctScaled(cost) {
|
function pctScaled(cost) {
|
||||||
const days = (viewTo - viewFrom) / 86400000;
|
const days = (viewTo - viewFrom) / 86400000;
|
||||||
const budget = WEEKLY_BUDGET / 7 * days;
|
const budget = CFG.weeklyBudget / 7 * days;
|
||||||
return (cost / budget * 100).toFixed(3) + '%';
|
return (cost / budget * 100).toFixed(3) + '%';
|
||||||
}
|
}
|
||||||
+27
-9
@@ -10,15 +10,7 @@
|
|||||||
<header>
|
<header>
|
||||||
<h1>Cost Analytics</h1>
|
<h1>Cost Analytics</h1>
|
||||||
<div id="summary"></div>
|
<div id="summary"></div>
|
||||||
<div id="anchor-config">
|
<button id="btn-config" title="Einstellungen">⚙</button>
|
||||||
<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-sync" title="Neue Einträge importieren">↻ Sync</button>
|
<button id="btn-sync" title="Neue Einträge importieren">↻ Sync</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -47,6 +39,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+52
-16
@@ -38,22 +38,17 @@ header > * { flex-shrink: 0; }
|
|||||||
|
|
||||||
header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); }
|
header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); }
|
||||||
|
|
||||||
#anchor-config {
|
#btn-config {
|
||||||
display: flex;
|
background: var(--border);
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
#anchor-config select {
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border: 1px solid var(--border);
|
border: none;
|
||||||
border-radius: 4px;
|
padding: 5px;
|
||||||
padding: 2px 4px;
|
border-radius: 6px;
|
||||||
font-size: 0.8rem;
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
#btn-config:hover { background: var(--accent) }
|
||||||
|
|
||||||
#btn-sync {
|
#btn-sync {
|
||||||
background: var(--border);
|
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 .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 {
|
#toolbar {
|
||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
@@ -329,7 +324,7 @@ header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); }
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal-close {
|
#modal-close, #config-close {
|
||||||
position: absolute; top: 12px; right: 14px;
|
position: absolute; top: 12px; right: 14px;
|
||||||
background: none; border: none;
|
background: none; border: none;
|
||||||
color: var(--muted); font-size: 1.2rem; cursor: pointer;
|
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;
|
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 { width: 6px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||||
Reference in new issue
Block a user