package activity import ( "database/sql" "time" "verstak/internal/core/storage" "verstak/internal/core/util" ) // Event types. const ( TypeNoteCreated = "note_created" TypeNoteUpdated = "note_updated" TypeFileAdded = "file_added" TypeFileDeleted = "file_deleted" TypeFileRenamed = "file_renamed" TypeFileCopied = "file_copied" TypeFileMoved = "file_moved" TypeFolderAdded = "folder_added" TypeFolderDeleted = "folder_deleted" TypeFolderRenamed = "folder_renamed" TypeNodeCreated = "node_created" TypeNodeUpdated = "node_updated" TypeActionCreated = "action_created" TypeActionDone = "action_done" TypeWorklogAdded = "worklog_added" ) // Target types. const ( TargetNote = "note" TargetFile = "file" TargetFolder = "folder" TargetAction = "action" TargetNode = "node" TargetWorklog = "worklog" ) // Event represents an activity event. type Event struct { ID string `json:"id"` NodeID string `json:"node_id"` EventType string `json:"event_type"` TargetType string `json:"target_type,omitempty"` TargetID string `json:"target_id,omitempty"` TargetPath string `json:"target_path,omitempty"` Title string `json:"title"` DetailsJSON string `json:"details_json,omitempty"` CreatedAt string `json:"created_at"` } // Service records and queries activity events. type Service struct { db *storage.DB } func NewService(db *storage.DB) *Service { return &Service{db: db} } // Record inserts a new activity event. func (s *Service) Record(nodeID, targetType, targetID, targetPath, eventType, title, detailsJSON string) error { id := util.UUID7() now := time.Now().UTC().Format(time.RFC3339) _, err := s.db.Exec( `INSERT INTO activity_events(id,node_id,event_type,target_type,target_id,target_path,title,metadata,created_at) VALUES(?,?,?,?,?,?,?,?,?)`, id, nodeID, eventType, targetType, targetID, targetPath, title, detailsJSON, now) return err } // TodayBoundaries returns the current local day in UTC as RFC3339 strings. func TodayBoundaries() (string, string) { now := time.Now() y, m, d := now.Date() start := time.Date(y, m, d, 0, 0, 0, 0, now.Location()) end := start.Add(24 * time.Hour) return start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339) } // scanEvent scans a single event row. func scanEvent(rows *sql.Rows) (Event, error) { var e Event err := rows.Scan(&e.ID, &e.NodeID, &e.EventType, &e.TargetType, &e.TargetID, &e.TargetPath, &e.Title, &e.DetailsJSON, &e.CreatedAt) return e, err } // ListTodayEvents returns all activity events from today. func (s *Service) ListTodayEvents() ([]Event, error) { start, end := TodayBoundaries() rows, err := s.db.Query( `SELECT id,node_id,event_type,COALESCE(target_type,''),COALESCE(target_id,''),COALESCE(target_path,''), title,COALESCE(metadata,'{}'),created_at FROM activity_events WHERE created_at >= ? AND created_at < ? ORDER BY created_at DESC`, start, end) if err != nil { return nil, err } defer rows.Close() var events []Event for rows.Next() { e, err := scanEvent(rows) if err != nil { return nil, err } events = append(events, e) } return events, rows.Err() } // ListTodayEventsByParent returns today's events grouped by their NodeID. func (s *Service) ListTodayEventsByParent() (map[string][]Event, error) { events, err := s.ListTodayEvents() if err != nil { return nil, err } grouped := make(map[string][]Event, 8) for _, e := range events { pid := e.NodeID grouped[pid] = append(grouped[pid], e) } return grouped, nil } // ListRecent returns a paginated global activity feed. func (s *Service) ListRecent(limit, offset int) ([]Event, error) { rows, err := s.db.Query( `SELECT id,node_id,event_type,COALESCE(target_type,''),COALESCE(target_id,''),COALESCE(target_path,''), title,COALESCE(metadata,'{}'),created_at FROM activity_events ORDER BY created_at DESC LIMIT ? OFFSET ?`, limit, offset) if err != nil { return nil, err } defer rows.Close() var events []Event for rows.Next() { e, err := scanEvent(rows) if err != nil { return nil, err } events = append(events, e) } return events, rows.Err() } // ListByNode returns paginated events for a specific node. func (s *Service) ListByNode(nodeID string, limit, offset int) ([]Event, error) { rows, err := s.db.Query( `SELECT id,node_id,event_type,COALESCE(target_type,''),COALESCE(target_id,''),COALESCE(target_path,''), title,COALESCE(metadata,'{}'),created_at FROM activity_events WHERE node_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`, nodeID, limit, offset) if err != nil { return nil, err } defer rows.Close() var events []Event for rows.Next() { e, err := scanEvent(rows) if err != nil { return nil, err } events = append(events, e) } return events, rows.Err() } // CountByNode returns the total number of events for a node. func (s *Service) CountByNode(nodeID string) (int, error) { var count int err := s.db.QueryRow( `SELECT COUNT(*) FROM activity_events WHERE node_id = ?`, nodeID).Scan(&count) return count, err }