Entry View: 3h-Segmente, Tagesansicht mit 3h-Separatoren
This commit is contained in:
1 parent
4beb7cc663
commit
6c40a9063a
2 files changed
+20
-22
No files matched your search
@@ -2,10 +2,6 @@ Cost-Analytics Dashboard für OpenClaw API-Nutzung. Importiert Session-JSONL-Fil
|
|||||||
|
|
||||||
**Stack:** .NET 10 Minimal API + Vanilla JS SPA + SQLite | **Port:** 18684
|
**Stack:** .NET 10 Minimal API + Vanilla JS SPA + SQLite | **Port:** 18684
|
||||||
|
|
||||||
## Pfad
|
|
||||||
|
|
||||||
`/home/node/.openclaw/workspace/projects/token-analytics`
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -14,8 +10,6 @@ bash deploy.sh
|
|||||||
|
|
||||||
## Notizen
|
## Notizen
|
||||||
|
|
||||||
- Siehe ARCHITECTURE.md für technische Details
|
|
||||||
- Siehe IDEAS.md für offene Ideen
|
|
||||||
- Config wird server-seitig in `dashboard-config.json` gespeichert (nicht in Git)
|
- Config wird server-seitig in `dashboard-config.json` gespeichert (nicht in Git)
|
||||||
- JSONL-Import ist Full-Scan (robust gegen OpenClaw Session-Kompaktierung)
|
- JSONL-Import ist Full-Scan (robust gegen OpenClaw Session-Kompaktierung)
|
||||||
- Balkenanzahl dynamisch: Vielfache von 28, basierend auf Viewport und `minBarWidth`
|
- Balkenanzahl dynamisch: Vielfache von 28, basierend auf Viewport und `minBarWidth`
|
||||||
+20
-16
@@ -216,6 +216,7 @@ function renderBucketChart(buckets, bucketMinutes) {
|
|||||||
// Determine week boundaries for grouping
|
// Determine week boundaries for grouping
|
||||||
let lastWeekId = null;
|
let lastWeekId = null;
|
||||||
let lastDayId = null;
|
let lastDayId = null;
|
||||||
|
let lastSixHourId = null;
|
||||||
|
|
||||||
for (let i = 0; i < getBarCount(); i++) {
|
for (let i = 0; i < getBarCount(); i++) {
|
||||||
const b = bucketMap[i];
|
const b = bucketMap[i];
|
||||||
@@ -236,12 +237,18 @@ function renderBucketChart(buckets, bucketMinutes) {
|
|||||||
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;
|
||||||
const isNewDay = showDaySep && lastDayId !== null && dayId !== lastDayId && !isNewWeek;
|
const isNewDay = showDaySep && lastDayId !== null && dayId !== lastDayId && !isNewWeek;
|
||||||
if (isNewWeek || isNewDay) {
|
// 3h quarter separators for 1-day view (absolute, anchored to anchorHour)
|
||||||
|
const isOneDayView = ZOOM_LEVELS[zoomIndex].days === 1;
|
||||||
|
const absHour = new Date(bucketStart);
|
||||||
|
const sixHourId = isOneDayView ? Math.floor((absHour.getHours() - CFG.anchorHour + 24) % 24 / 3) : null;
|
||||||
|
const isNewQuarter = isOneDayView && lastSixHourId !== null && sixHourId !== lastSixHourId && !isNewWeek && !isNewDay;
|
||||||
|
if (isNewWeek || isNewDay || isNewQuarter) {
|
||||||
const spacer = document.createElement('div');
|
const spacer = document.createElement('div');
|
||||||
spacer.className = isNewWeek ? 'bar-spacer' : 'bar-spacer day-spacer';
|
spacer.className = isNewWeek ? 'bar-spacer' : 'bar-spacer day-spacer';
|
||||||
chart.appendChild(spacer);
|
chart.appendChild(spacer);
|
||||||
labels.appendChild(Object.assign(document.createElement('div'), { className: 'chart-label bar-spacer' }));
|
labels.appendChild(Object.assign(document.createElement('div'), { className: 'chart-label bar-spacer' }));
|
||||||
}
|
}
|
||||||
|
if (isOneDayView) lastSixHourId = sixHourId;
|
||||||
lastWeekId = weekId;
|
lastWeekId = weekId;
|
||||||
lastDayId = dayId;
|
lastDayId = dayId;
|
||||||
|
|
||||||
@@ -365,20 +372,9 @@ function renderChainBlocks(chains) {
|
|||||||
if (entryMode) {
|
if (entryMode) {
|
||||||
openModal(null, null, c.chainId);
|
openModal(null, null, c.chainId);
|
||||||
} else if (zoomIndex >= ZOOM_LEVELS.length - 1) {
|
} else if (zoomIndex >= ZOOM_LEVELS.length - 1) {
|
||||||
// Already at max zoom (1 Tag) - enter entry mode for the bucket containing this chain
|
// Already at max zoom (1 Tag) - enter entry mode for 3h window containing this chain
|
||||||
const chainMid = new Date((new Date(c.first).getTime() + new Date(c.last).getTime()) / 2);
|
const chainMid = new Date((new Date(c.first).getTime() + new Date(c.last).getTime()) / 2);
|
||||||
const totalMinutes = (viewTo - viewFrom) / 60000;
|
enterEntryMode(chainMid, chainMid);
|
||||||
const bucketMinutes = Math.max(1, Math.round(totalMinutes / getBarCount()));
|
|
||||||
const bucketMs = bucketMinutes * 60000;
|
|
||||||
const elapsed = chainMid - viewFrom;
|
|
||||||
const bucketIndex = Math.floor(elapsed / bucketMs);
|
|
||||||
const bucketStart = new Date(viewFrom.getTime() + bucketIndex * bucketMs);
|
|
||||||
const bucketEnd = new Date(bucketStart.getTime() + bucketMs);
|
|
||||||
zoomStack.push({ from: new Date(viewFrom), to: new Date(viewTo), zoom: zoomIndex, entry: false });
|
|
||||||
viewFrom = bucketStart;
|
|
||||||
viewTo = bucketEnd;
|
|
||||||
entryMode = true;
|
|
||||||
loadView();
|
|
||||||
} else {
|
} else {
|
||||||
const chainMid = new Date((new Date(c.first).getTime() + new Date(c.last).getTime()) / 2);
|
const chainMid = new Date((new Date(c.first).getTime() + new Date(c.last).getTime()) / 2);
|
||||||
zoomStack.push({ from: new Date(viewFrom), to: new Date(viewTo), zoom: zoomIndex, entry: false });
|
zoomStack.push({ from: new Date(viewFrom), to: new Date(viewTo), zoom: zoomIndex, entry: false });
|
||||||
@@ -497,8 +493,16 @@ function zoomInto(clickStart, clickEnd) {
|
|||||||
|
|
||||||
function enterEntryMode(bucketStart, bucketEnd) {
|
function enterEntryMode(bucketStart, bucketEnd) {
|
||||||
zoomStack.push({ from: new Date(viewFrom), to: new Date(viewTo), zoom: zoomIndex, entry: false });
|
zoomStack.push({ from: new Date(viewFrom), to: new Date(viewTo), zoom: zoomIndex, entry: false });
|
||||||
viewFrom = bucketStart;
|
// Snap to 3-hour window containing the clicked bucket (use bucketStart to avoid boundary drift)
|
||||||
viewTo = bucketEnd;
|
const THREE_H = 3 * 3600000;
|
||||||
|
// Align to 3h slots starting at anchorHour
|
||||||
|
const dayStart = new Date(bucketStart);
|
||||||
|
dayStart.setHours(CFG.anchorHour, 0, 0, 0);
|
||||||
|
if (dayStart > bucketStart) dayStart.setDate(dayStart.getDate() - 1);
|
||||||
|
const elapsed = bucketStart.getTime() - dayStart.getTime();
|
||||||
|
const slotIdx = Math.floor(elapsed / THREE_H);
|
||||||
|
viewFrom = new Date(dayStart.getTime() + slotIdx * THREE_H);
|
||||||
|
viewTo = new Date(viewFrom.getTime() + THREE_H);
|
||||||
entryMode = true;
|
entryMode = true;
|
||||||
loadView();
|
loadView();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user