package store import ( "errors" "fmt" "sort" "sync" "time" "github.com/google/uuid" ) type Workspace struct { ID string `json:"id"` Slug string `json:"slug"` Name string `json:"name"` Role string `json:"role"` CreatedAt time.Time `json:"createdAt"` } type Member struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Name string `json:"name"` Email string `json:"email"` Role string `json:"role"` Status string `json:"status"` } type Invite struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Email string `json:"email"` Role string `json:"role"` Token string `json:"token"` CreatedAt time.Time `json:"createdAt"` Status string `json:"status"` } type ActivityEntry struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Title string `json:"title"` Detail string `json:"detail"` CreatedAt time.Time `json:"createdAt"` } type BoardGroup struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Name string `json:"name"` Color string `json:"color"` Order int `json:"order"` } type Label struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Name string `json:"name"` Color string `json:"color"` } type Attachment struct { ID string `json:"id"` Name string `json:"name"` MimeType string `json:"mimeType"` Size int `json:"size"` DataURL string `json:"dataUrl,omitempty"` } type TaskComment struct { ID string `json:"id"` TaskID string `json:"taskId"` Author string `json:"author"` Content string `json:"content"` CreatedAt time.Time `json:"createdAt"` } type Task struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` BoardGroupID string `json:"boardGroupId"` Title string `json:"title"` Description string `json:"description,omitempty"` Status string `json:"status"` Color string `json:"color"` DueAt *time.Time `json:"dueAt,omitempty"` ScheduledStart *time.Time `json:"scheduledStart,omitempty"` ScheduledEnd *time.Time `json:"scheduledEnd,omitempty"` AssigneeID *string `json:"assigneeId,omitempty"` LabelIDs []string `json:"labelIds"` Attachments []Attachment `json:"attachments"` Comments []TaskComment `json:"comments"` RecurrenceRule string `json:"recurrenceRule,omitempty"` RecurrenceEnd *time.Time `json:"recurrenceEnd,omitempty"` Archived bool `json:"archived"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type CalendarEvent struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Title string `json:"title"` Description string `json:"description,omitempty"` StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` Color string `json:"color"` LinkedTaskID *string `json:"linkedTaskId,omitempty"` Attachments []Attachment `json:"attachments"` } type Note struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Title string `json:"title"` Content string `json:"content"` UpdatedAt time.Time `json:"updatedAt"` } type FocusSession struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` TaskID *string `json:"taskId,omitempty"` Mode string `json:"mode"` StartedAt time.Time `json:"startedAt"` CompletedAt *time.Time `json:"completedAt,omitempty"` PausedAt *time.Time `json:"pausedAt,omitempty"` PausedTotalSeconds int `json:"pausedTotalSeconds"` DurationSeconds int `json:"durationSeconds"` } type CreateBoardGroupInput struct { WorkspaceSlug string `json:"workspaceSlug" binding:"required"` Name string `json:"name" binding:"required"` Color string `json:"color"` } type UpdateBoardGroupInput struct { Name *string `json:"name"` Color *string `json:"color"` Order *int `json:"order"` } type CreateInviteInput struct { WorkspaceSlug string `json:"workspaceSlug" binding:"required"` Email string `json:"email" binding:"required"` Role string `json:"role" binding:"required"` } type AcceptInviteInput struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required"` } type UpdateMemberInput struct { Role *string `json:"role"` Status *string `json:"status"` } type CreateLabelInput struct { WorkspaceSlug string `json:"workspaceSlug" binding:"required"` Name string `json:"name" binding:"required"` Color string `json:"color" binding:"required"` } type CreateTaskInput struct { WorkspaceSlug string `json:"workspaceSlug" binding:"required"` BoardGroupID string `json:"boardGroupId" binding:"required"` Title string `json:"title" binding:"required"` Description string `json:"description"` DueAt *time.Time `json:"dueAt"` Color string `json:"color"` } type UpdateTaskInput struct { Title *string `json:"title"` Description *string `json:"description"` Status *string `json:"status"` BoardGroupID *string `json:"boardGroupId"` Color *string `json:"color"` DueAt *time.Time `json:"dueAt"` ScheduledStart *time.Time `json:"scheduledStart"` ScheduledEnd *time.Time `json:"scheduledEnd"` AssigneeID *string `json:"assigneeId"` LabelIDs []string `json:"labelIds"` Attachments []Attachment `json:"attachments"` Comments []TaskComment `json:"comments"` RecurrenceRule *string `json:"recurrenceRule"` RecurrenceEnd *time.Time `json:"recurrenceEnd"` Archived *bool `json:"archived"` } type CreateEventInput struct { WorkspaceSlug string `json:"workspaceSlug" binding:"required"` Title string `json:"title" binding:"required"` Description string `json:"description"` StartsAt string `json:"startsAt" binding:"required"` EndsAt string `json:"endsAt" binding:"required"` LinkedTaskID *string `json:"linkedTaskId"` Color string `json:"color"` Attachments []Attachment `json:"attachments"` } type UpdateEventInput struct { Title *string `json:"title"` Description *string `json:"description"` StartsAt *time.Time `json:"startsAt"` EndsAt *time.Time `json:"endsAt"` LinkedTaskID *string `json:"linkedTaskId"` Color *string `json:"color"` Attachments []Attachment `json:"attachments"` } type CreateNoteInput struct { WorkspaceSlug string `json:"workspaceSlug" binding:"required"` Title string `json:"title" binding:"required"` Content string `json:"content" binding:"required"` } type UpdateNoteInput struct { Title *string `json:"title"` Content *string `json:"content"` } type CreateFocusSessionInput struct { WorkspaceSlug string `json:"workspaceSlug" binding:"required"` TaskID *string `json:"taskId"` Mode string `json:"mode" binding:"required"` DurationSeconds int `json:"durationSeconds" binding:"required"` } type UpdateFocusSessionInput struct { CompletedAt *time.Time `json:"completedAt"` PausedAt *time.Time `json:"pausedAt"` PausedTotalSeconds *int `json:"pausedTotalSeconds"` } type State struct { mu sync.RWMutex Mode string Workspaces []Workspace Members []Member Invites []Invite Activities []ActivityEntry BoardGroups []BoardGroup Labels []Label Tasks []Task Events []CalendarEvent Notes []Note FocusSessions []FocusSession Mailboxes []Mailbox MailboxAuth map[string]MailboxConnection MailMessages []MailMessage OutgoingMails []OutgoingMail } func NewSeededState(mode string) *State { now := time.Now().UTC() workspace := Workspace{ ID: uuid.NewString(), Slug: "personal", Name: "Personal HQ", Role: "owner", CreatedAt: now.Add(-72 * time.Hour), } boardGroups := []BoardGroup{ {ID: "group-inbox", WorkspaceSlug: workspace.Slug, Name: "Inbox", Color: "slate", Order: 0}, {ID: "group-doing", WorkspaceSlug: workspace.Slug, Name: "Doing", Color: "rose", Order: 1}, {ID: "group-review", WorkspaceSlug: workspace.Slug, Name: "Review", Color: "amber", Order: 2}, {ID: "group-done", WorkspaceSlug: workspace.Slug, Name: "Done", Color: "emerald", Order: 3}, } labels := []Label{ {ID: "label-launch", WorkspaceSlug: workspace.Slug, Name: "Launch", Color: "rose"}, {ID: "label-ui", WorkspaceSlug: workspace.Slug, Name: "UI", Color: "amber"}, {ID: "label-deep", WorkspaceSlug: workspace.Slug, Name: "Deep Work", Color: "emerald"}, {ID: "label-admin", WorkspaceSlug: workspace.Slug, Name: "Admin", Color: "blue"}, } taskDue := now.Add(10 * time.Hour) taskStart := time.Date(now.Year(), now.Month(), now.Day(), 9, 30, 0, 0, time.UTC) taskEnd := taskStart.Add(90 * time.Minute) linkedTaskID := uuid.NewString() completed := now.Add(-2 * time.Hour) assigneeID := "member-1" members := []Member{ {ID: "member-1", WorkspaceSlug: workspace.Slug, Name: "Taylor", Email: "taylor@productier.app", Role: "owner", Status: "active"}, {ID: "member-2", WorkspaceSlug: workspace.Slug, Name: "Alex", Email: "alex@productier.app", Role: "member", Status: "active"}, } invites := []Invite{ { ID: "invite-1", WorkspaceSlug: workspace.Slug, Email: "jamie@productier.app", Role: "member", Token: "invite-jamie", CreatedAt: now.Add(-12 * time.Hour), Status: "pending", }, } activities := []ActivityEntry{ { ID: "activity-1", WorkspaceSlug: workspace.Slug, Title: "Board updated", Detail: "Task moved into Review.", CreatedAt: now.Add(-4 * time.Hour), }, { ID: "activity-2", WorkspaceSlug: workspace.Slug, Title: "Invite created", Detail: "Jamie was invited to Personal HQ.", CreatedAt: now.Add(-10 * time.Hour), }, } return &State{ Mode: mode, Workspaces: []Workspace{workspace}, Members: members, Invites: invites, Activities: activities, BoardGroups: boardGroups, Labels: labels, Tasks: []Task{ { ID: linkedTaskID, WorkspaceSlug: workspace.Slug, BoardGroupID: "group-doing", Title: "Finalize Productier foundation", Description: "Shape the workspace shell, polish the board, and keep the Koffan calmness without copying the layout.", Status: "in_progress", Color: "rose", DueAt: &taskDue, ScheduledStart: &taskStart, ScheduledEnd: &taskEnd, AssigneeID: &assigneeID, LabelIDs: []string{"label-launch", "label-ui"}, Attachments: []Attachment{}, Comments: []TaskComment{ { ID: uuid.NewString(), TaskID: linkedTaskID, Author: "Alex", Content: "Keep the calendar dense, but not cramped.", CreatedAt: now.Add(-5 * time.Hour), }, }, CreatedAt: now.Add(-24 * time.Hour), UpdatedAt: now.Add(-2 * time.Hour), }, }, Events: []CalendarEvent{ { ID: uuid.NewString(), WorkspaceSlug: workspace.Slug, Title: "Design review", Description: "Refine the calendar density and modal patterns.", StartsAt: time.Date(now.Year(), now.Month(), now.Day(), 13, 0, 0, 0, time.UTC), EndsAt: time.Date(now.Year(), now.Month(), now.Day(), 14, 0, 0, 0, time.UTC), Color: "amber", LinkedTaskID: &linkedTaskID, Attachments: []Attachment{}, }, }, Notes: []Note{ { ID: uuid.NewString(), WorkspaceSlug: workspace.Slug, Title: "Calm UI direction", Content: "Use soft stone surfaces, controlled color, and denser layouts than Koffan. Keep interactions obvious and low-friction.", UpdatedAt: now.Add(-90 * time.Minute), }, }, FocusSessions: []FocusSession{ { ID: uuid.NewString(), WorkspaceSlug: workspace.Slug, TaskID: &linkedTaskID, Mode: "focus", StartedAt: now.Add(-55 * time.Minute), CompletedAt: &completed, PausedTotalSeconds: 0, DurationSeconds: 1500, }, }, Mailboxes: []Mailbox{}, MailboxAuth: map[string]MailboxConnection{}, MailMessages: []MailMessage{}, OutgoingMails: []OutgoingMail{}, } } func (s *State) ListWorkspaces() []Workspace { s.mu.RLock() defer s.mu.RUnlock() return append([]Workspace(nil), s.Workspaces...) } func (s *State) ListMembers(workspaceSlug string) []Member { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Members, workspaceSlug) } func (s *State) GetMemberByID(memberID string) (Member, error) { s.mu.RLock() defer s.mu.RUnlock() for _, member := range s.Members { if member.ID == memberID { return member, nil } } return Member{}, errors.New("member not found") } func (s *State) UpdateMember(memberID string, input UpdateMemberInput) (Member, error) { s.mu.Lock() defer s.mu.Unlock() for index, member := range s.Members { if member.ID != memberID { continue } nextRole := member.Role if input.Role != nil { if !isValidWorkspaceRole(*input.Role) { return Member{}, errors.New("invalid member role") } nextRole = *input.Role } nextStatus := member.Status if input.Status != nil { if !isValidMemberStatus(*input.Status) { return Member{}, errors.New("invalid member status") } nextStatus = *input.Status } if member.Role == "owner" && member.Status == "active" && (nextRole != "owner" || nextStatus != "active") { activeOwnerCount := 0 for _, candidate := range s.Members { if candidate.WorkspaceSlug == member.WorkspaceSlug && candidate.Role == "owner" && candidate.Status == "active" { activeOwnerCount++ } } if activeOwnerCount <= 1 { return Member{}, errors.New("workspace must have at least one active owner") } } member.Role = nextRole member.Status = nextStatus s.Members[index] = member s.appendActivityLocked(member.WorkspaceSlug, "Member updated", fmt.Sprintf("%s membership settings changed.", member.Email)) return member, nil } return Member{}, errors.New("member not found") } func (s *State) ListInvites(workspaceSlug string) []Invite { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Invites, workspaceSlug) } func (s *State) GetInviteByID(inviteID string) (Invite, error) { s.mu.RLock() defer s.mu.RUnlock() for _, invite := range s.Invites { if invite.ID == inviteID { return invite, nil } } return Invite{}, errors.New("invite not found") } func (s *State) GetInviteByToken(token string) (Invite, error) { s.mu.RLock() defer s.mu.RUnlock() for _, invite := range s.Invites { if invite.Token == token { return invite, nil } } return Invite{}, errors.New("invite not found") } func (s *State) CreateInvite(input CreateInviteInput) Invite { s.mu.Lock() defer s.mu.Unlock() invite := Invite{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, Email: input.Email, Role: defaultString(input.Role, "member"), Token: "join-" + uuid.NewString(), CreatedAt: time.Now().UTC(), Status: "pending", } s.Invites = append([]Invite{invite}, s.Invites...) s.appendActivityLocked(input.WorkspaceSlug, "Invite created", fmt.Sprintf("Shared workspace access with %s.", input.Email)) return invite } func (s *State) RevokeInvite(inviteID string) error { s.mu.Lock() defer s.mu.Unlock() for index, invite := range s.Invites { if invite.ID != inviteID { continue } if invite.Status != "pending" { return errors.New("only pending invites can be revoked") } s.Invites = append(s.Invites[:index], s.Invites[index+1:]...) s.appendActivityLocked(invite.WorkspaceSlug, "Invite revoked", fmt.Sprintf("Invite for %s was revoked.", invite.Email)) return nil } return errors.New("invite not found") } func (s *State) AcceptInvite(token string, input AcceptInviteInput) (Invite, error) { s.mu.Lock() defer s.mu.Unlock() for index, invite := range s.Invites { if invite.Token != token { continue } invite.Status = "accepted" s.Invites[index] = invite memberExists := false for _, member := range s.Members { if member.WorkspaceSlug == invite.WorkspaceSlug && member.Email == input.Email { memberExists = true break } } if !memberExists { s.Members = append([]Member{{ ID: uuid.NewString(), WorkspaceSlug: invite.WorkspaceSlug, Name: input.Name, Email: input.Email, Role: invite.Role, Status: "active", }}, s.Members...) } s.appendActivityLocked(invite.WorkspaceSlug, "Invite accepted", fmt.Sprintf("%s joined the workspace.", input.Email)) return invite, nil } return Invite{}, errors.New("invite not found") } func (s *State) ListActivities(workspaceSlug string) []ActivityEntry { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Activities, workspaceSlug) } func (s *State) ListBoardGroups(workspaceSlug string) []BoardGroup { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.BoardGroups, workspaceSlug) } func (s *State) GetBoardGroupByID(groupID string) (BoardGroup, error) { s.mu.RLock() defer s.mu.RUnlock() for _, group := range s.BoardGroups { if group.ID == groupID { return group, nil } } return BoardGroup{}, errors.New("board group not found") } func (s *State) CreateBoardGroup(input CreateBoardGroupInput) BoardGroup { s.mu.Lock() defer s.mu.Unlock() order := 0 for _, group := range s.BoardGroups { if group.WorkspaceSlug == input.WorkspaceSlug { order++ } } group := BoardGroup{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, Name: input.Name, Color: defaultString(input.Color, "slate"), Order: order, } s.BoardGroups = append(s.BoardGroups, group) s.appendActivityLocked(input.WorkspaceSlug, "Board group added", fmt.Sprintf("%s is ready for new tasks.", input.Name)) return group } func (s *State) rebalanceBoardGroupOrderLocked(workspaceSlug string) { groups := make([]BoardGroup, 0) for _, group := range s.BoardGroups { if group.WorkspaceSlug == workspaceSlug { groups = append(groups, group) } } sort.SliceStable(groups, func(i, j int) bool { if groups[i].Order == groups[j].Order { return groups[i].Name < groups[j].Name } return groups[i].Order < groups[j].Order }) nextByID := make(map[string]BoardGroup, len(groups)) for index, group := range groups { group.Order = index nextByID[group.ID] = group } for index, group := range s.BoardGroups { updated, ok := nextByID[group.ID] if !ok { continue } s.BoardGroups[index] = updated } } func (s *State) UpdateBoardGroup(groupID string, input UpdateBoardGroupInput) (BoardGroup, error) { s.mu.Lock() defer s.mu.Unlock() for index, group := range s.BoardGroups { if group.ID != groupID { continue } if input.Name != nil { group.Name = *input.Name } if input.Color != nil { group.Color = *input.Color } if input.Order != nil { nextOrder := *input.Order if nextOrder < 0 { nextOrder = 0 } group.Order = nextOrder } s.BoardGroups[index] = group if input.Order != nil { s.rebalanceBoardGroupOrderLocked(group.WorkspaceSlug) for _, current := range s.BoardGroups { if current.ID == groupID { group = current break } } } s.appendActivityLocked(group.WorkspaceSlug, "Board group updated", fmt.Sprintf("%s settings changed.", group.Name)) return group, nil } return BoardGroup{}, errors.New("board group not found") } func (s *State) ListLabels(workspaceSlug string) []Label { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Labels, workspaceSlug) } func (s *State) CreateLabel(input CreateLabelInput) Label { s.mu.Lock() defer s.mu.Unlock() label := Label{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, Name: input.Name, Color: input.Color, } s.Labels = append(s.Labels, label) s.appendActivityLocked(input.WorkspaceSlug, "Label added", fmt.Sprintf("%s is now available across the workspace.", input.Name)) return label } func (s *State) ListTasks(workspaceSlug string) []Task { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Tasks, workspaceSlug) } func (s *State) GetTaskByID(taskID string) (Task, error) { s.mu.RLock() defer s.mu.RUnlock() for _, task := range s.Tasks { if task.ID == taskID { return task, nil } } return Task{}, errors.New("task not found") } func (s *State) CreateTask(input CreateTaskInput) Task { s.mu.Lock() defer s.mu.Unlock() now := time.Now().UTC() task := Task{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, BoardGroupID: input.BoardGroupID, Title: input.Title, Description: input.Description, Status: "todo", Color: defaultString(input.Color, "slate"), DueAt: input.DueAt, LabelIDs: []string{}, Attachments: []Attachment{}, Comments: []TaskComment{}, CreatedAt: now, UpdatedAt: now, } s.Tasks = append(s.Tasks, task) s.appendActivityLocked(input.WorkspaceSlug, "Task created", input.Title) return task } func (s *State) UpdateTask(taskID string, input UpdateTaskInput) (Task, error) { s.mu.Lock() defer s.mu.Unlock() for index, task := range s.Tasks { if task.ID != taskID { continue } if input.Title != nil { task.Title = *input.Title } if input.Description != nil { task.Description = *input.Description } if input.Status != nil { task.Status = *input.Status } if input.BoardGroupID != nil { task.BoardGroupID = *input.BoardGroupID } if input.Color != nil { task.Color = *input.Color } if input.DueAt != nil { task.DueAt = input.DueAt } if input.ScheduledStart != nil { task.ScheduledStart = input.ScheduledStart } if input.ScheduledEnd != nil { task.ScheduledEnd = input.ScheduledEnd } if input.AssigneeID != nil { task.AssigneeID = input.AssigneeID } if input.LabelIDs != nil { task.LabelIDs = input.LabelIDs } if input.Attachments != nil { task.Attachments = input.Attachments } if input.Comments != nil { task.Comments = input.Comments } task.UpdatedAt = time.Now().UTC() s.Tasks[index] = task s.appendActivityLocked(task.WorkspaceSlug, "Task updated", task.Title) return task, nil } return Task{}, errors.New("task not found") } func (s *State) ListEvents(workspaceSlug string) []CalendarEvent { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Events, workspaceSlug) } func (s *State) GetEventByID(eventID string) (CalendarEvent, error) { s.mu.RLock() defer s.mu.RUnlock() for _, event := range s.Events { if event.ID == eventID { return event, nil } } return CalendarEvent{}, errors.New("event not found") } func (s *State) CreateEvent(input CreateEventInput) CalendarEvent { s.mu.Lock() defer s.mu.Unlock() startsAt, _ := time.Parse(time.RFC3339, input.StartsAt) endsAt, _ := time.Parse(time.RFC3339, input.EndsAt) event := CalendarEvent{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, Title: input.Title, Description: input.Description, StartsAt: startsAt, EndsAt: endsAt, Color: defaultString(input.Color, "blue"), LinkedTaskID: input.LinkedTaskID, Attachments: input.Attachments, } s.Events = append(s.Events, event) s.appendActivityLocked(input.WorkspaceSlug, "Event created", input.Title) return event } func (s *State) UpdateEvent(eventID string, input UpdateEventInput) (CalendarEvent, error) { s.mu.Lock() defer s.mu.Unlock() for index, event := range s.Events { if event.ID != eventID { continue } if input.Title != nil { event.Title = *input.Title } if input.Description != nil { event.Description = *input.Description } if input.StartsAt != nil { event.StartsAt = *input.StartsAt } if input.EndsAt != nil { event.EndsAt = *input.EndsAt } if input.LinkedTaskID != nil { event.LinkedTaskID = input.LinkedTaskID } if input.Color != nil { event.Color = *input.Color } if input.Attachments != nil { event.Attachments = input.Attachments } s.Events[index] = event s.appendActivityLocked(event.WorkspaceSlug, "Event updated", event.Title) return event, nil } return CalendarEvent{}, errors.New("event not found") } func (s *State) ListNotes(workspaceSlug string) []Note { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Notes, workspaceSlug) } func (s *State) GetNoteByID(noteID string) (Note, error) { s.mu.RLock() defer s.mu.RUnlock() for _, note := range s.Notes { if note.ID == noteID { return note, nil } } return Note{}, errors.New("note not found") } func (s *State) CreateNote(input CreateNoteInput) Note { s.mu.Lock() defer s.mu.Unlock() note := Note{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, Title: input.Title, Content: input.Content, UpdatedAt: time.Now().UTC(), } s.Notes = append(s.Notes, note) s.appendActivityLocked(input.WorkspaceSlug, "Note created", input.Title) return note } func (s *State) UpdateNote(noteID string, input UpdateNoteInput) (Note, error) { s.mu.Lock() defer s.mu.Unlock() for index, note := range s.Notes { if note.ID != noteID { continue } if input.Title != nil { note.Title = *input.Title } if input.Content != nil { note.Content = *input.Content } note.UpdatedAt = time.Now().UTC() s.Notes[index] = note s.appendActivityLocked(note.WorkspaceSlug, "Note updated", note.Title) return note, nil } return Note{}, errors.New("note not found") } func (s *State) ListFocusSessions(workspaceSlug string) []FocusSession { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.FocusSessions, workspaceSlug) } func (s *State) GetFocusSessionByID(sessionID string) (FocusSession, error) { s.mu.RLock() defer s.mu.RUnlock() for _, session := range s.FocusSessions { if session.ID == sessionID { return session, nil } } return FocusSession{}, errors.New("focus session not found") } func (s *State) CreateFocusSession(input CreateFocusSessionInput) FocusSession { s.mu.Lock() defer s.mu.Unlock() session := FocusSession{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, TaskID: input.TaskID, Mode: input.Mode, StartedAt: time.Now().UTC(), PausedTotalSeconds: 0, DurationSeconds: input.DurationSeconds, } s.FocusSessions = append(s.FocusSessions, session) s.appendActivityLocked(input.WorkspaceSlug, "Focus session started", input.Mode) return session } func (s *State) UpdateFocusSession(sessionID string, input UpdateFocusSessionInput) (FocusSession, error) { s.mu.Lock() defer s.mu.Unlock() for index, session := range s.FocusSessions { if session.ID != sessionID { continue } if input.CompletedAt != nil { session.CompletedAt = input.CompletedAt } if input.PausedAt != nil { session.PausedAt = input.PausedAt } if input.PausedTotalSeconds != nil { session.PausedTotalSeconds = *input.PausedTotalSeconds } s.FocusSessions[index] = session s.appendActivityLocked(session.WorkspaceSlug, "Focus session updated", session.Mode) return session, nil } return FocusSession{}, errors.New("focus session not found") } func filterByWorkspace[T interface{ GetWorkspaceSlug() string }](items []T, workspaceSlug string) []T { if workspaceSlug == "" { return append([]T(nil), items...) } filtered := make([]T, 0, len(items)) for _, item := range items { if item.GetWorkspaceSlug() == workspaceSlug { filtered = append(filtered, item) } } return filtered } func defaultString(value string, fallback string) string { if value == "" { return fallback } return value } func isValidWorkspaceRole(role string) bool { switch role { case "owner", "admin", "member": return true default: return false } } func isValidMemberStatus(status string) bool { switch status { case "active", "removed": return true default: return false } } func (s *State) appendActivityLocked(workspaceSlug string, title string, detail string) { entry := ActivityEntry{ ID: uuid.NewString(), WorkspaceSlug: workspaceSlug, Title: title, Detail: detail, CreatedAt: time.Now().UTC(), } s.Activities = append([]ActivityEntry{entry}, s.Activities...) if len(s.Activities) > 40 { s.Activities = s.Activities[:40] } } func (item Member) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item Invite) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item ActivityEntry) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item BoardGroup) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item Label) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item Task) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item CalendarEvent) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item Note) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item FocusSession) GetWorkspaceSlug() string { return item.WorkspaceSlug }