initial summary page

This commit is contained in:
lowcarbdev
2026-02-28 22:52:23 -07:00
parent ed04eb409d
commit 69253e174d
8 changed files with 967 additions and 2 deletions
+174
View File
@@ -1032,3 +1032,177 @@ func SearchMessages(userDB *sql.DB, query string, limit int) ([]SearchResult, er
return results, nil
}
// GetAnalytics retrieves analytics data for the Summary tab
func GetAnalytics(userDB *sql.DB, startDate, endDate *time.Time, topN int) (*AnalyticsResponse, error) {
analytics := &AnalyticsResponse{}
// Build date filter
dateFilter := ""
args := []interface{}{}
if startDate != nil {
dateFilter += " AND date >= ?"
args = append(args, startDate.Unix())
}
if endDate != nil {
dateFilter += " AND date <= ?"
args = append(args, endDate.Unix())
}
// 1. Get summary statistics
if err := getSummaryStats(userDB, dateFilter, args, analytics); err != nil {
return nil, err
}
// 2. Get top contacts
topContacts, err := getTopContacts(userDB, dateFilter, args, topN)
if err != nil {
return nil, err
}
analytics.TopContacts = topContacts
// 3. Get hourly distribution
hourly, err := getHourlyDistribution(userDB, dateFilter, args)
if err != nil {
return nil, err
}
analytics.HourlyDistribution = hourly
// 4. Get daily trend
daily, err := getDailyTrend(userDB, dateFilter, args)
if err != nil {
return nil, err
}
analytics.DailyTrend = daily
return analytics, nil
}
func getSummaryStats(userDB *sql.DB, dateFilter string, args []interface{}, analytics *AnalyticsResponse) error {
query := `
SELECT
COUNT(*) as total,
SUM(CASE WHEN record_type = 1 THEN 1 ELSE 0 END) as sms_count,
SUM(CASE WHEN record_type = 2 THEN 1 ELSE 0 END) as mms_count,
SUM(CASE WHEN record_type = 3 THEN 1 ELSE 0 END) as call_count,
SUM(CASE WHEN record_type IN (1,2) AND type = 2 THEN 1 ELSE 0 END) as sent,
SUM(CASE WHEN record_type IN (1,2) AND type = 1 THEN 1 ELSE 0 END) as received,
SUM(CASE WHEN record_type = 3 AND type = 1 THEN 1 ELSE 0 END) as incoming_calls,
SUM(CASE WHEN record_type = 3 AND type = 2 THEN 1 ELSE 0 END) as outgoing_calls,
SUM(CASE WHEN record_type = 3 AND type = 3 THEN 1 ELSE 0 END) as missed_calls,
COALESCE(SUM(CASE WHEN record_type = 3 THEN duration ELSE 0 END), 0) as total_duration,
COALESCE(AVG(CASE WHEN record_type IN (1,2) AND body IS NOT NULL AND body != '' THEN LENGTH(body) END), 0) as avg_length
FROM messages
WHERE 1=1 ` + dateFilter
return userDB.QueryRow(query, args...).Scan(
&analytics.TotalMessages,
&analytics.TotalSMS,
&analytics.TotalMMS,
&analytics.TotalCalls,
&analytics.TotalSent,
&analytics.TotalReceived,
&analytics.IncomingCalls,
&analytics.OutgoingCalls,
&analytics.MissedCalls,
&analytics.TotalCallDuration,
&analytics.AvgMessageLength,
)
}
func getTopContacts(userDB *sql.DB, dateFilter string, args []interface{}, limit int) ([]TopContact, error) {
query := `
SELECT
address,
MAX(COALESCE(contact_name, '')) as contact_name,
COUNT(*) as message_count,
SUM(CASE WHEN type = 2 THEN 1 ELSE 0 END) as sent_count,
SUM(CASE WHEN type = 1 THEN 1 ELSE 0 END) as received_count
FROM messages
WHERE record_type IN (1, 2) ` + dateFilter + `
GROUP BY address
ORDER BY message_count DESC
LIMIT ?`
queryArgs := append(args, limit)
rows, err := userDB.Query(query, queryArgs...)
if err != nil {
return nil, err
}
defer rows.Close()
var contacts []TopContact
for rows.Next() {
var c TopContact
if err := rows.Scan(&c.Address, &c.ContactName, &c.MessageCount, &c.SentCount, &c.ReceivedCount); err != nil {
return nil, err
}
contacts = append(contacts, c)
}
return contacts, nil
}
func getHourlyDistribution(userDB *sql.DB, dateFilter string, args []interface{}) ([]HourlyDistribution, error) {
query := `
SELECT
CAST(strftime('%H', date, 'unixepoch', 'localtime') AS INTEGER) as hour,
COUNT(*) as count
FROM messages
WHERE record_type IN (1, 2) ` + dateFilter + `
GROUP BY hour
ORDER BY hour`
rows, err := userDB.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
// Initialize all 24 hours with 0
hourMap := make(map[int]int)
for i := 0; i < 24; i++ {
hourMap[i] = 0
}
for rows.Next() {
var hour, count int
if err := rows.Scan(&hour, &count); err != nil {
return nil, err
}
hourMap[hour] = count
}
// Convert to slice
result := make([]HourlyDistribution, 24)
for i := 0; i < 24; i++ {
result[i] = HourlyDistribution{Hour: i, Count: hourMap[i]}
}
return result, nil
}
func getDailyTrend(userDB *sql.DB, dateFilter string, args []interface{}) ([]DailyCount, error) {
query := `
SELECT
strftime('%Y-%m-%d', date, 'unixepoch', 'localtime') as day,
COUNT(*) as count
FROM messages
WHERE record_type IN (1, 2) ` + dateFilter + `
GROUP BY day
ORDER BY day`
rows, err := userDB.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var trend []DailyCount
for rows.Next() {
var d DailyCount
if err := rows.Scan(&d.Date, &d.Count); err != nil {
return nil, err
}
trend = append(trend, d)
}
return trend, nil
}
+45
View File
@@ -552,6 +552,51 @@ func HandleSearch(c echo.Context) error {
return c.JSON(http.StatusOK, results)
}
// HandleAnalytics returns analytics data for the Summary tab
func HandleAnalytics(c echo.Context) error {
userDB, err := getUserDB(c)
if err != nil {
slog.Error("Error getting user database", "error", err)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to get user database",
})
}
var startDate, endDate *time.Time
if startStr := c.QueryParam("start"); startStr != "" {
t, err := time.Parse(time.RFC3339, startStr)
if err == nil {
startDate = &t
}
}
if endStr := c.QueryParam("end"); endStr != "" {
t, err := time.Parse(time.RFC3339, endStr)
if err == nil {
endDate = &t
}
}
// Default to top 10 contacts
topN := 10
if topStr := c.QueryParam("top"); topStr != "" {
if val, err := strconv.Atoi(topStr); err == nil && val > 0 && val <= 50 {
topN = val
}
}
analytics, err := GetAnalytics(userDB, startDate, endDate, topN)
if err != nil {
slog.Error("Error getting analytics", "error", err)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to get analytics",
})
}
return c.JSON(http.StatusOK, analytics)
}
// HandleVersion returns the application version
func HandleVersion(c echo.Context) error {
// Try to read version from version.json file first (Docker builds)
+37
View File
@@ -110,3 +110,40 @@ type ChangePasswordRequest struct {
NewPassword string `json:"new_password"`
ConfirmPassword string `json:"confirm_password"`
}
// Analytics types
type TopContact struct {
Address string `json:"address"`
ContactName string `json:"contact_name,omitempty"`
MessageCount int `json:"message_count"`
SentCount int `json:"sent_count"`
ReceivedCount int `json:"received_count"`
}
type HourlyDistribution struct {
Hour int `json:"hour"`
Count int `json:"count"`
}
type DailyCount struct {
Date string `json:"date"`
Count int `json:"count"`
}
type AnalyticsResponse struct {
TotalMessages int `json:"total_messages"`
TotalSMS int `json:"total_sms"`
TotalMMS int `json:"total_mms"`
TotalCalls int `json:"total_calls"`
TotalSent int `json:"total_sent"`
TotalReceived int `json:"total_received"`
IncomingCalls int `json:"incoming_calls"`
OutgoingCalls int `json:"outgoing_calls"`
MissedCalls int `json:"missed_calls"`
TotalCallDuration int `json:"total_call_duration"`
AvgMessageLength float64 `json:"avg_message_length"`
TopContacts []TopContact `json:"top_contacts"`
HourlyDistribution []HourlyDistribution `json:"hourly_distribution"`
DailyTrend []DailyCount `json:"daily_trend"`
}