package main import ( "sort" "time" "verstak/internal/core/activity" "verstak/internal/core/nodes" syncsvc "verstak/internal/core/sync" "verstak/internal/i18n" ) type SystemViewDTO struct { ID string `json:"id"` Label string `json:"label"` Icon string `json:"icon,omitempty"` } func (a *App) ListSystemViews() []SystemViewDTO { return []SystemViewDTO{ {ID: "today", Label: i18n.TF("ru", "nav.today")}, {ID: "inbox", Label: i18n.TF("ru", "nav.inbox")}, {ID: "trash", Label: i18n.TF("ru", "nav.trash")}, {ID: "journal", Label: i18n.TF("ru", "nav.journal")}, {ID: "activity", Label: i18n.TF("ru", "nav.activity")}, } } func (a *App) ListTodayView() (*TodayDashboardDTO, error) { if err := a.requireVault(); err != nil { return nil, err } aeByParent, err := a.activity.ListTodayEventsByParent() if err != nil { aeByParent = nil } todayNodes, _ := a.nodes.ListTodayNodes() type rawEvent struct { NodeID string EventType string TargetType string TargetID string TargetPath string Title string CreatedAt string } type caseInfo struct { Node nodes.Node Events []rawEvent } caseMap := make(map[string]*caseInfo) ensureCase := func(caseID string) *caseInfo { if ci, ok := caseMap[caseID]; ok { return ci } ci := &caseInfo{Events: nil} if n, err := a.nodes.GetActive(caseID); err == nil { ci.Node = *n } caseMap[caseID] = ci return ci } for pid, events := range aeByParent { ci := ensureCase(pid) for _, e := range events { ci.Events = append(ci.Events, rawEvent{ NodeID: e.NodeID, EventType: e.EventType, TargetType: e.TargetType, TargetID: e.TargetID, TargetPath: e.TargetPath, Title: e.Title, CreatedAt: e.CreatedAt, }) } } for _, n := range todayNodes { _ = ensureCase(n.ID) if ci := caseMap[n.ID]; ci.Node.ID == "" { ci.Node = n } } var groups []TodayGroupDTO var flatEvents []EventDTO summary := SummaryDTO{} for _, ci := range caseMap { if ci.Node.ID == "" { continue } summary.ChangedCases++ dtoEvents := make([]EventDTO, 0, len(ci.Events)) for _, re := range ci.Events { dtoEvents = append(dtoEvents, EventDTO{ ID: ci.Node.ID + "/" + re.NodeID + "/" + re.CreatedAt, NodeID: re.NodeID, EventType: re.EventType, TargetType: re.TargetType, TargetID: re.TargetID, TargetPath: re.TargetPath, Title: re.Title, CreatedAt: re.CreatedAt, }) switch re.EventType { case activity.TypeNoteCreated, activity.TypeNoteUpdated: summary.Notes++ case activity.TypeFileAdded, activity.TypeFileDeleted, activity.TypeFileRenamed, activity.TypeFileCopied, activity.TypeFileMoved: summary.Files++ } } last := ci.Node.UpdatedAt.Format(time.RFC3339) for _, e := range dtoEvents { if e.CreatedAt > last { last = e.CreatedAt } } groups = append(groups, TodayGroupDTO{ NodeID: ci.Node.ID, NodeTitle: ci.Node.Title, NodeKind: ci.Node.Type, Section: ci.Node.Section, LastActivityAt: last, Events: dtoEvents, }) flatEvents = append(flatEvents, dtoEvents...) } sort.Slice(groups, func(i, j int) bool { return groups[i].LastActivityAt > groups[j].LastActivityAt }) sort.Slice(flatEvents, func(i, j int) bool { return flatEvents[i].CreatedAt > flatEvents[j].CreatedAt }) return &TodayDashboardDTO{ Date: time.Now().Format("2006-01-02"), Summary: summary, Groups: groups, Events: flatEvents, }, nil } func (a *App) ListActivityFeed(limit, offset int) ([]EventDTO, error) { if err := a.requireVault(); err != nil { return nil, err } events, err := a.activity.ListRecent(limit, offset) if err != nil { return nil, err } result := make([]EventDTO, len(events)) for i, e := range events { result[i] = a.eventDTOWithPath(e) } return result, nil } func (a *App) ListActivityByNode(nodeID string, limit, offset int) ([]EventDTO, error) { if err := a.requireVault(); err != nil { return nil, err } events, err := a.listActivityByNodeSubtree(nodeID, limit, offset) if err != nil { return nil, err } result := make([]EventDTO, len(events)) for i, e := range events { result[i] = a.eventDTOWithPath(e) } return result, nil } func (a *App) CountActivityByNode(nodeID string) (int, error) { if err := a.requireVault(); err != nil { return 0, err } return a.activity.CountByNode(nodeID) } var _ = syncsvc.EntityNode func (a *App) listActivityByNodeSubtree(nodeID string, limit, offset int) ([]activity.Event, error) { rows, err := a.db.Query( `WITH RECURSIVE subtree(id) AS ( SELECT id FROM nodes WHERE id = ? AND deleted_at IS NULL UNION ALL SELECT n.id FROM nodes n JOIN subtree s ON n.parent_id = s.id WHERE n.deleted_at IS NULL ) SELECT e.id, e.node_id, e.event_type, COALESCE(e.target_type,''), COALESCE(e.target_id,''), COALESCE(e.target_path,''), e.title, COALESCE(e.metadata,'{}'), e.created_at FROM activity_events e JOIN subtree s ON s.id = e.node_id ORDER BY e.created_at DESC LIMIT ? OFFSET ?`, nodeID, limit, offset) if err != nil { return nil, err } defer rows.Close() var events []activity.Event for rows.Next() { var e activity.Event if err := rows.Scan(&e.ID, &e.NodeID, &e.EventType, &e.TargetType, &e.TargetID, &e.TargetPath, &e.Title, &e.DetailsJSON, &e.CreatedAt); err != nil { return nil, err } events = append(events, e) } return events, rows.Err() } func (a *App) eventDTOWithPath(e activity.Event) EventDTO { dto := toEventDTO(e) dto.NodePath = a.nodes.Path(e.NodeID) return dto } // ListTodayInProgress returns today's modification events — items the user worked on. func (a *App) ListTodayInProgress() ([]EventDTO, error) { if err := a.requireVault(); err != nil { return nil, err } events, err := a.activity.ListTodayEvents() if err != nil { return nil, err } modTypes := map[string]bool{ activity.TypeNoteCreated: true, activity.TypeNoteUpdated: true, activity.TypeNoteDeleted: true, activity.TypeFileAdded: true, activity.TypeFileDeleted: true, activity.TypeFileRenamed: true, activity.TypeFileCopied: true, activity.TypeFileMoved: true, activity.TypeFolderAdded: true, activity.TypeFolderDeleted: true, activity.TypeFolderRenamed: true, activity.TypeFolderMoved: true, activity.TypeNodeCreated: true, activity.TypeNodeUpdated: true, activity.TypeNodeDeleted: true, activity.TypeActionCreated: true, activity.TypeActionDone: true, } result := make([]EventDTO, 0, len(events)) for _, e := range events { if modTypes[e.EventType] { result = append(result, a.eventDTOWithPath(e)) } } sort.Slice(result, func(i, j int) bool { return result[i].CreatedAt > result[j].CreatedAt }) return result, nil }