Files
MindSift/internal/storage/system.go
T
sandrodachandClaude Opus 5.5 c4fc455850 feat(ui): open list items counter on Listen, none on Workspace
New stat listItemsOpenCount: unchecked items in all lists the user sees
(own, shared directly or through a group), shown as accent counter on
Listen in the header and the mobile bottom bar. Workspace loses its
counter. Stats refresh on list events and after deleting an item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:43:56 +01:00

212 lines
6.3 KiB
Go

package storage
import (
"encoding/json"
"strings"
"github.com/sandrodach/mindsift/internal/domain"
)
// --- STATS, SEARCH & EXPORT ---
func (s *Store) GetDashboardStats(userID string) (*domain.DashboardStats, error) {
st := &domain.DashboardStats{}
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND status = 'inbox'`, userID).Scan(&st.InboxCount)
// Same tasks as GetTasks without a list ("Alle Aufgaben"): own ones plus those in shared task lists
taskScope := `(user_id = ? OR task_list_id IN (` + s.accessibleTaskListIDsQuery() + `)) AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))`
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE `+taskScope+` AND is_completed = 0`, userID, userID, userID, userID).Scan(&st.TasksOpenCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE `+taskScope+` AND is_completed = 1`, userID, userID, userID, userID).Scan(&st.TasksDoneCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ? AND is_archived = 0`, userID).Scan(&st.DocsCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND status = 'discarded'`, userID).Scan(&st.DiscardedCount)
// Own lists and lists shared with the user directly or through a group
visibleLists := `
SELECT l.id
FROM lists l
LEFT JOIN list_shares ls ON ls.list_id = l.id AND (
(ls.target_type = 'user' AND ls.target_id = ?)
OR (ls.target_type = 'team' AND ls.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
)
WHERE l.user_id = ? OR ls.id IS NOT NULL`
_ = s.db.QueryRow(`SELECT COUNT(DISTINCT id) FROM (`+visibleLists+`)`, userID, userID, userID).Scan(&st.ListsCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM list_items WHERE is_completed = 0 AND list_id IN (`+visibleLists+`)`,
userID, userID, userID).Scan(&st.ListItemsOpenCount)
// Count open (uncompleted) To-Dos across accessible workspace documents (owned or shared)
docRows, err := s.db.Query(`
SELECT blocks_json FROM documents
WHERE (user_id = ? OR id IN (SELECT doc_id FROM doc_shares WHERE user_id = ?))
AND is_archived = 0`,
userID, userID,
)
if err == nil {
for docRows.Next() {
var bj string
if err := docRows.Scan(&bj); err == nil && bj != "" {
var blocks []domain.DocumentBlock
if err := json.Unmarshal([]byte(bj), &blocks); err == nil {
for _, b := range blocks {
if b.Type == "todo" && !b.Checked {
st.DocsTodosOpenCount++
}
}
}
}
}
_ = docRows.Close()
}
return st, nil
}
func (s *Store) SearchAll(userID, query string) ([]domain.Thought, []domain.Document, error) {
res, err := s.SearchGlobal(userID, query)
if err != nil {
return nil, nil, err
}
return res.Thoughts, res.Documents, nil
}
func (s *Store) ExportUserData(userID string) (*domain.FullExport, error) {
user, err := s.GetUserByID(userID)
if err != nil {
return nil, err
}
thoughts, _ := s.GetThoughts(userID, "all", 5000)
docs, _ := s.GetDocuments(userID)
lists, _ := s.GetAllListsForUser(userID)
taskLists, _ := s.GetTaskLists(userID)
return &domain.FullExport{
ExportedAt: domain.NowISO(),
Version: "1.0.0",
User: *user,
Thoughts: thoughts,
Documents: docs,
Lists: lists,
TaskLists: taskLists,
}, nil
}
// --- SYSTEM SETTINGS ---
func (s *Store) SeedSMTPDefaultsFromEnv(host, port, user, pass, from string, tls, skipVerify bool) {
cur, err := s.GetSystemSettings()
if err != nil || cur == nil {
return
}
// Only populate DB from env if DB has no SMTP host configured yet and env provides one
if cur.SMTPHost == "" && strings.TrimSpace(host) != "" {
cur.SMTPHost = strings.TrimSpace(host)
if strings.TrimSpace(port) != "" {
cur.SMTPPort = strings.TrimSpace(port)
}
cur.SMTPUser = strings.TrimSpace(user)
cur.SMTPPass = pass
cur.SMTPFrom = strings.TrimSpace(from)
cur.SMTPTLS = tls
cur.SMTPSkipVerify = skipVerify
_ = s.UpdateSystemSettings(cur)
}
}
func (s *Store) GetSystemSettings() (*domain.SystemSettings, error) {
settings := &domain.SystemSettings{
SharingMode: SharingOrganizations,
AllowRegistration: false,
InstanceName: "MindSift",
SMTPPort: "587",
SMTPTLS: true,
}
rows, err := s.db.Query(`SELECT key, value FROM system_settings`)
if err != nil {
return settings, nil
}
defer rows.Close()
for rows.Next() {
var k, v string
if err := rows.Scan(&k, &v); err == nil {
switch k {
case "sharing_mode":
settings.SharingMode = v
case "allow_registration":
settings.AllowRegistration = (v == "true" || v == "1")
case "instance_name":
settings.InstanceName = v
case "smtp_host":
settings.SMTPHost = v
case "smtp_port":
if v != "" {
settings.SMTPPort = v
}
case "smtp_user":
settings.SMTPUser = v
case "smtp_pass":
settings.SMTPPass = v
case "smtp_from":
settings.SMTPFrom = v
case "smtp_tls":
settings.SMTPTLS = (v != "false" && v != "0")
case "smtp_skip_verify":
settings.SMTPSkipVerify = v == "true"
case "allow_private_webhooks":
settings.AllowPrivateWebhooks = v == "true"
}
}
}
return settings, nil
}
func (s *Store) UpdateSystemSettings(settings *domain.SystemSettings) error {
regStr := "false"
if settings.AllowRegistration {
regStr = "true"
}
tlsStr := "false"
if settings.SMTPTLS {
tlsStr = "true"
}
skipVerifyStr := "false"
if settings.SMTPSkipVerify {
skipVerifyStr = "true"
}
privateHooksStr := "false"
if settings.AllowPrivateWebhooks {
privateHooksStr = "true"
}
if settings.SMTPPort == "" {
settings.SMTPPort = "587"
}
settings.SharingMode = NormalizeSharingMode(settings.SharingMode)
if settings.InstanceName == "" {
settings.InstanceName = "MindSift"
}
_, err := s.db.Exec(`INSERT INTO system_settings (key, value) VALUES
('sharing_mode', ?),
('allow_registration', ?),
('instance_name', ?),
('smtp_host', ?),
('smtp_port', ?),
('smtp_user', ?),
('smtp_pass', ?),
('smtp_from', ?),
('smtp_tls', ?),
('smtp_skip_verify', ?),
('allow_private_webhooks', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
settings.SharingMode,
regStr,
settings.InstanceName,
strings.TrimSpace(settings.SMTPHost),
strings.TrimSpace(settings.SMTPPort),
strings.TrimSpace(settings.SMTPUser),
settings.SMTPPass,
strings.TrimSpace(settings.SMTPFrom),
tlsStr,
skipVerifyStr,
privateHooksStr,
)
return err
}