From 452c7c59b6c8e0e3bfc71d0cd50cffaf0f2613e7 Mon Sep 17 00:00:00 2001 From: messypy Date: Mon, 27 Jul 2026 23:53:00 +0900 Subject: [PATCH] Include full day details in history export --- cmd/vrc_osc_gui/history_view_windows.go | 154 +++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/cmd/vrc_osc_gui/history_view_windows.go b/cmd/vrc_osc_gui/history_view_windows.go index 4c17323..2869f60 100644 --- a/cmd/vrc_osc_gui/history_view_windows.go +++ b/cmd/vrc_osc_gui/history_view_windows.go @@ -591,9 +591,11 @@ func exportHistoryDay(app *guiApp, day time.Time, selected map[string]bool) (str } func exportHistoryDayFromRequest(req historyExportRequest) (string, error) { + events := historyEventsFromSnapshot() + visits := historyVisitSource() rows := append([]historyDetailRow(nil), req.Rows...) if len(rows) == 0 { - rows = historyVisitsForDayFromData(nil, req.Day, historyVisitSource(), historyEventsFromSnapshot(), req.Query) + rows = historyVisitsForDayFromData(nil, req.Day, visits, events, req.Query) } if len(req.Selected) > 0 { filtered := rows[:0] @@ -679,6 +681,9 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) { } } } + appendExportDayVisits(&b, req.Day, visits, req.Query) + appendExportDayEvents(&b, req.Day, events, req.Query) + appendExportGuestSnapshot(&b, req.Day, req.Query) exportDir := runtimeDir() if strings.TrimSpace(req.ExportDir) != "" { exportDir = normalizeHistoryExportDir(req.ExportDir) @@ -696,6 +701,153 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) { return path, nil } +func appendExportDayVisits(b *strings.Builder, day time.Time, visits []historyVisitRecord, q historyQuery) { + if b == nil { + return + } + dayStart, dayEnd := historyDayBounds(day) + matched := make([]historyVisitRecord, 0, len(visits)) + for _, visit := range visits { + start := visit.Start + if start.IsZero() { + continue + } + end := visit.End + if end.IsZero() { + end = time.Now() + } + if !start.Before(dayEnd) || !end.After(dayStart) { + continue + } + if q.Pattern != nil && !q.Pattern.MatchString(visit.WorldLabel) && !q.Pattern.MatchString(visit.WorldID) && !q.Pattern.MatchString(visit.InstanceID) { + continue + } + matched = append(matched, visit) + } + sort.SliceStable(matched, func(i, j int) bool { + return matched[i].Start.Before(matched[j].Start) + }) + b.WriteString("\nAll visits:\n") + if len(matched) == 0 { + b.WriteString("(no visits)\n") + return + } + for _, visit := range matched { + b.WriteString("- ") + b.WriteString(historyTimeRangeLabel(visit.Start, visit.End, visit.Current)) + b.WriteString(" ") + b.WriteString(strings.TrimSpace(visit.WorldLabel)) + if strings.TrimSpace(visit.WorldID) != "" { + b.WriteString(" world_id=") + b.WriteString(strings.TrimSpace(visit.WorldID)) + } + if strings.TrimSpace(visit.InstanceID) != "" { + b.WriteString(" instance_id=") + b.WriteString(strings.TrimSpace(visit.InstanceID)) + } + b.WriteString("\n") + } +} + +func appendExportDayEvents(b *strings.Builder, day time.Time, events []historyEventRecord, q historyQuery) { + if b == nil { + return + } + dayStart, dayEnd := historyDayBounds(day) + matched := make([]historyEventRecord, 0, len(events)) + for _, ev := range events { + if ev.At.IsZero() || ev.At.Before(dayStart) || !ev.At.Before(dayEnd) { + continue + } + raw := fmt.Sprintf("[%s] %s %s", ev.At.Format("15:04"), ev.Kind, ev.Name) + if q.Pattern != nil && !q.Pattern.MatchString(ev.Name) && !q.Pattern.MatchString(raw) { + continue + } + matched = append(matched, ev) + } + sort.SliceStable(matched, func(i, j int) bool { + if matched[i].At.Equal(matched[j].At) { + return strings.ToLower(matched[i].Name) < strings.ToLower(matched[j].Name) + } + return matched[i].At.Before(matched[j].At) + }) + b.WriteString("\nAll join/leave events:\n") + if len(matched) == 0 { + b.WriteString("(no events)\n") + return + } + for _, ev := range matched { + b.WriteString("- ") + b.WriteString(ev.At.Format("15:04")) + b.WriteString(" ") + b.WriteString(ev.Kind) + b.WriteString(" ") + b.WriteString(strings.TrimSpace(ev.Name)) + if ev.Raw != "" { + b.WriteString(" | ") + b.WriteString(strings.TrimSpace(ev.Raw)) + } + b.WriteString("\n") + } +} + +func appendExportGuestSnapshot(b *strings.Builder, day time.Time, q historyQuery) { + if b == nil { + return + } + guests := readGuestSnapshot() + dayStart, dayEnd := historyDayBounds(day) + type guestLine struct { + name string + present bool + join time.Time + leave time.Time + } + lines := make([]guestLine, 0, len(guests)) + for _, guest := range guests { + name := strings.TrimSpace(guest.Name) + if name == "" { + continue + } + inDay := guest.Present || + (!guest.LastJoin.IsZero() && !guest.LastJoin.Before(dayStart) && guest.LastJoin.Before(dayEnd)) || + (!guest.LastLeave.IsZero() && !guest.LastLeave.Before(dayStart) && guest.LastLeave.Before(dayEnd)) + if !inDay { + continue + } + if q.Pattern != nil && !q.Pattern.MatchString(name) { + continue + } + lines = append(lines, guestLine{name: name, present: guest.Present, join: guest.LastJoin, leave: guest.LastLeave}) + } + sort.SliceStable(lines, func(i, j int) bool { + return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) + }) + b.WriteString("\nGuest snapshot:\n") + if len(lines) == 0 { + b.WriteString("(no guests)\n") + return + } + for _, line := range lines { + b.WriteString("- ") + if line.present { + b.WriteString("present ") + } else { + b.WriteString("left ") + } + b.WriteString(line.name) + if !line.join.IsZero() { + b.WriteString(" join=") + b.WriteString(line.join.Format("15:04")) + } + if !line.leave.IsZero() { + b.WriteString(" leave=") + b.WriteString(line.leave.Format("15:04")) + } + b.WriteString("\n") + } +} + func cloneHistorySelection(selected map[string]bool) map[string]bool { if len(selected) == 0 { return nil