data-flow: implement backend dashboard aggregation endpoint
- Created DashboardHandler that aggregates multiple data sources
- Fetches stats, activity, and library preview in parallel
- Aggregates stats from audit logs (tracks_played, messages_sent, favorites, active_friends)
- Converts audit logs to RecentActivity format with type mapping
- Converts tracks to TrackPreview format for library preview
- Supports query parameters: activity_limit, library_limit, stats_period
- Returns wrapped format {success: true, data: DashboardResponse}
- Registered route: GET /api/v1/dashboard (protected, requires auth)
- Uses interface-based approach to avoid import cycle
- Router creates wrapper function to adapt track service
- Build successful, all handlers compile correctly
- Action 2.1.1.2 complete - dashboard endpoint ready for frontend integration
2026-01-15 16:42:49 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
|
|
|
"veza-backend-api/internal/models"
|
|
|
|
|
"veza-backend-api/internal/services"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// DashboardServiceInterface defines methods needed for dashboard handler
|
|
|
|
|
type DashboardServiceInterface interface {
|
|
|
|
|
GetStats(ctx context.Context, startDate, endDate time.Time) ([]*services.AuditStats, error)
|
|
|
|
|
GetUserActivity(ctx context.Context, userID uuid.UUID, limit int) ([]*services.AuditLog, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TrackListParamsForDashboard represents parameters for listing tracks (to avoid import cycle)
|
|
|
|
|
type TrackListParamsForDashboard struct {
|
|
|
|
|
Page int
|
|
|
|
|
Limit int
|
|
|
|
|
UserID *uuid.UUID
|
|
|
|
|
Genre *string
|
|
|
|
|
Format *string
|
|
|
|
|
SortBy string
|
|
|
|
|
SortOrder string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TrackListFuncForDashboard is a function type for listing tracks (to avoid import cycle)
|
|
|
|
|
type TrackListFuncForDashboard func(ctx context.Context, params TrackListParamsForDashboard) ([]*models.Track, int64, error)
|
|
|
|
|
|
|
|
|
|
// TrackServiceInterfaceForDashboard defines methods needed for dashboard handler
|
|
|
|
|
type TrackServiceInterfaceForDashboard interface {
|
|
|
|
|
ListTracks(ctx context.Context, params TrackListParamsForDashboard) ([]*models.Track, int64, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DashboardHandler handles dashboard aggregation endpoint
|
|
|
|
|
// Action 2.1.1.2: Implements dashboard endpoint that aggregates multiple data sources
|
|
|
|
|
type DashboardHandler struct {
|
|
|
|
|
auditService DashboardServiceInterface
|
|
|
|
|
trackService TrackServiceInterfaceForDashboard
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDashboardHandler creates a new dashboard handler
|
|
|
|
|
// trackListFunc is a function that lists tracks (provided by router to avoid import cycle)
|
|
|
|
|
func NewDashboardHandler(
|
|
|
|
|
auditService *services.AuditService,
|
|
|
|
|
trackListFunc TrackListFuncForDashboard,
|
|
|
|
|
logger *zap.Logger,
|
|
|
|
|
) *DashboardHandler {
|
|
|
|
|
return &DashboardHandler{
|
|
|
|
|
auditService: &dashboardAuditServiceWrapper{auditService: auditService},
|
|
|
|
|
trackService: &dashboardTrackServiceWrapper{listTracksFunc: trackListFunc},
|
|
|
|
|
logger: logger,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDashboardHandlerWithInterface creates a new dashboard handler with interfaces (for testing)
|
|
|
|
|
func NewDashboardHandlerWithInterface(
|
|
|
|
|
auditService DashboardServiceInterface,
|
|
|
|
|
trackService TrackServiceInterfaceForDashboard,
|
|
|
|
|
logger *zap.Logger,
|
|
|
|
|
) *DashboardHandler {
|
|
|
|
|
return &DashboardHandler{
|
|
|
|
|
auditService: auditService,
|
|
|
|
|
trackService: trackService,
|
|
|
|
|
logger: logger,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dashboardAuditServiceWrapper wraps *services.AuditService to implement DashboardServiceInterface
|
|
|
|
|
type dashboardAuditServiceWrapper struct {
|
|
|
|
|
auditService *services.AuditService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *dashboardAuditServiceWrapper) GetStats(ctx context.Context, startDate, endDate time.Time) ([]*services.AuditStats, error) {
|
|
|
|
|
return w.auditService.GetStats(ctx, startDate, endDate)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *dashboardAuditServiceWrapper) GetUserActivity(ctx context.Context, userID uuid.UUID, limit int) ([]*services.AuditLog, error) {
|
|
|
|
|
return w.auditService.GetUserActivity(ctx, userID, limit)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dashboardTrackServiceWrapper wraps track service to implement TrackServiceInterfaceForDashboard
|
|
|
|
|
// This wrapper will be created in the router to avoid import cycles
|
|
|
|
|
type dashboardTrackServiceWrapper struct {
|
|
|
|
|
listTracksFunc func(ctx context.Context, params TrackListParamsForDashboard) ([]*models.Track, int64, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *dashboardTrackServiceWrapper) ListTracks(ctx context.Context, params TrackListParamsForDashboard) ([]*models.Track, int64, error) {
|
|
|
|
|
return w.listTracksFunc(ctx, params)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DashboardResponse represents the aggregated dashboard data
|
|
|
|
|
// Action 2.1.1.2: Matches DASHBOARD_ENDPOINT_CONTRACT.md
|
|
|
|
|
type DashboardResponse struct {
|
2026-01-15 18:31:40 +00:00
|
|
|
Stats DashboardStats `json:"stats"`
|
|
|
|
|
RecentActivity []RecentActivity `json:"recent_activity"`
|
|
|
|
|
LibraryPreview LibraryPreview `json:"library_preview"`
|
data-flow: implement backend dashboard aggregation endpoint
- Created DashboardHandler that aggregates multiple data sources
- Fetches stats, activity, and library preview in parallel
- Aggregates stats from audit logs (tracks_played, messages_sent, favorites, active_friends)
- Converts audit logs to RecentActivity format with type mapping
- Converts tracks to TrackPreview format for library preview
- Supports query parameters: activity_limit, library_limit, stats_period
- Returns wrapped format {success: true, data: DashboardResponse}
- Registered route: GET /api/v1/dashboard (protected, requires auth)
- Uses interface-based approach to avoid import cycle
- Router creates wrapper function to adapt track service
- Build successful, all handlers compile correctly
- Action 2.1.1.2 complete - dashboard endpoint ready for frontend integration
2026-01-15 16:42:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DashboardStats represents dashboard statistics
|
|
|
|
|
type DashboardStats struct {
|
|
|
|
|
TracksPlayed int64 `json:"tracks_played"`
|
|
|
|
|
MessagesSent int64 `json:"messages_sent"`
|
|
|
|
|
Favorites int64 `json:"favorites"`
|
|
|
|
|
ActiveFriends int64 `json:"active_friends"`
|
|
|
|
|
TracksPlayedChange *string `json:"tracks_played_change,omitempty"`
|
|
|
|
|
MessagesSentChange *string `json:"messages_sent_change,omitempty"`
|
|
|
|
|
FavoritesChange *string `json:"favorites_change,omitempty"`
|
|
|
|
|
ActiveFriendsChange *string `json:"active_friends_change,omitempty"`
|
|
|
|
|
Period string `json:"period"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecentActivity represents a recent activity item
|
|
|
|
|
type RecentActivity struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Description *string `json:"description,omitempty"`
|
|
|
|
|
Timestamp string `json:"timestamp"`
|
|
|
|
|
Icon *string `json:"icon,omitempty"`
|
|
|
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LibraryPreview represents library preview data
|
|
|
|
|
type LibraryPreview struct {
|
|
|
|
|
Items []TrackPreview `json:"items"`
|
|
|
|
|
TotalCount int64 `json:"total_count"`
|
|
|
|
|
HasMore bool `json:"has_more"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TrackPreview represents a track in library preview
|
|
|
|
|
type TrackPreview struct {
|
2026-01-15 18:31:40 +00:00
|
|
|
ID string `json:"id"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Artist string `json:"artist"`
|
|
|
|
|
Duration int `json:"duration"`
|
|
|
|
|
CoverArtPath *string `json:"cover_art_path,omitempty"`
|
|
|
|
|
PlayCount int64 `json:"play_count"`
|
|
|
|
|
LikeCount int64 `json:"like_count"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
data-flow: implement backend dashboard aggregation endpoint
- Created DashboardHandler that aggregates multiple data sources
- Fetches stats, activity, and library preview in parallel
- Aggregates stats from audit logs (tracks_played, messages_sent, favorites, active_friends)
- Converts audit logs to RecentActivity format with type mapping
- Converts tracks to TrackPreview format for library preview
- Supports query parameters: activity_limit, library_limit, stats_period
- Returns wrapped format {success: true, data: DashboardResponse}
- Registered route: GET /api/v1/dashboard (protected, requires auth)
- Uses interface-based approach to avoid import cycle
- Router creates wrapper function to adapt track service
- Build successful, all handlers compile correctly
- Action 2.1.1.2 complete - dashboard endpoint ready for frontend integration
2026-01-15 16:42:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetDashboard aggregates dashboard data from multiple sources
|
|
|
|
|
// @Summary Get Dashboard Data
|
|
|
|
|
// @Description Get aggregated dashboard data including stats, recent activity, and library preview
|
|
|
|
|
// @Tags Dashboard
|
|
|
|
|
// @Accept json
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Security BearerAuth
|
|
|
|
|
// @Param activity_limit query int false "Number of recent activity items (default: 10)"
|
|
|
|
|
// @Param library_limit query int false "Number of library items (default: 5)"
|
|
|
|
|
// @Param stats_period query string false "Time period for statistics: 7d, 30d, 90d, all (default: 30d)"
|
|
|
|
|
// @Success 200 {object} handlers.APIResponse{data=DashboardResponse}
|
|
|
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
|
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
|
|
|
// @Router /api/v1/dashboard [get]
|
|
|
|
|
// Action 2.1.1.2: Implement dashboard aggregation endpoint
|
|
|
|
|
func (h *DashboardHandler) GetDashboard() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
// Get user ID from context
|
|
|
|
|
userIDInterface, exists := c.Get("user_id")
|
|
|
|
|
if !exists {
|
|
|
|
|
RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "User not authenticated")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
|
|
|
if !ok {
|
|
|
|
|
RespondWithError(c, int(apperrors.ErrCodeInternal), "Invalid user ID type")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse query parameters
|
|
|
|
|
activityLimit := 10 // Default
|
|
|
|
|
if limitStr := c.Query("activity_limit"); limitStr != "" {
|
|
|
|
|
if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= 100 {
|
|
|
|
|
activityLimit = parsed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
libraryLimit := 5 // Default
|
|
|
|
|
if limitStr := c.Query("library_limit"); limitStr != "" {
|
|
|
|
|
if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= 50 {
|
|
|
|
|
libraryLimit = parsed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statsPeriod := c.DefaultQuery("stats_period", "30d")
|
|
|
|
|
if statsPeriod != "7d" && statsPeriod != "30d" && statsPeriod != "90d" && statsPeriod != "all" {
|
|
|
|
|
statsPeriod = "30d" // Default to 30d if invalid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate date range for stats
|
|
|
|
|
var startDate, endDate time.Time
|
|
|
|
|
endDate = time.Now()
|
|
|
|
|
switch statsPeriod {
|
|
|
|
|
case "7d":
|
|
|
|
|
startDate = endDate.AddDate(0, 0, -7)
|
|
|
|
|
case "30d":
|
|
|
|
|
startDate = endDate.AddDate(0, 0, -30)
|
|
|
|
|
case "90d":
|
|
|
|
|
startDate = endDate.AddDate(0, 0, -90)
|
|
|
|
|
case "all":
|
|
|
|
|
startDate = time.Time{} // Zero time for "all time"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
|
|
|
|
|
|
// Fetch data in parallel (as per contract performance considerations)
|
|
|
|
|
type statsResult struct {
|
|
|
|
|
stats []*services.AuditStats
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
type activityResult struct {
|
|
|
|
|
activity []*services.AuditLog
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
type tracksResult struct {
|
|
|
|
|
tracks []*models.Track
|
|
|
|
|
total int64
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statsChan := make(chan statsResult, 1)
|
|
|
|
|
activityChan := make(chan activityResult, 1)
|
|
|
|
|
tracksChan := make(chan tracksResult, 1)
|
|
|
|
|
|
|
|
|
|
// Fetch stats
|
|
|
|
|
go func() {
|
|
|
|
|
stats, err := h.auditService.GetStats(ctx, startDate, endDate)
|
|
|
|
|
statsChan <- statsResult{stats: stats, err: err}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Fetch recent activity
|
|
|
|
|
go func() {
|
|
|
|
|
activity, err := h.auditService.GetUserActivity(ctx, userID, activityLimit)
|
|
|
|
|
activityChan <- activityResult{activity: activity, err: err}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Fetch library preview
|
|
|
|
|
go func() {
|
|
|
|
|
tracks, total, err := h.trackService.ListTracks(ctx, TrackListParamsForDashboard{
|
|
|
|
|
UserID: &userID,
|
|
|
|
|
Limit: libraryLimit,
|
|
|
|
|
Page: 1,
|
|
|
|
|
SortBy: "created_at",
|
|
|
|
|
SortOrder: "desc",
|
|
|
|
|
})
|
|
|
|
|
tracksChan <- tracksResult{tracks: tracks, total: total, err: err}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Collect results
|
|
|
|
|
statsRes := <-statsChan
|
|
|
|
|
if statsRes.err != nil {
|
|
|
|
|
h.logger.Error("Failed to get dashboard stats",
|
|
|
|
|
zap.Error(statsRes.err),
|
|
|
|
|
zap.String("user_id", userID.String()),
|
|
|
|
|
)
|
|
|
|
|
RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to fetch dashboard stats")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
activityRes := <-activityChan
|
|
|
|
|
if activityRes.err != nil {
|
|
|
|
|
h.logger.Error("Failed to get dashboard activity",
|
|
|
|
|
zap.Error(activityRes.err),
|
|
|
|
|
zap.String("user_id", userID.String()),
|
|
|
|
|
)
|
|
|
|
|
RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to fetch dashboard activity")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracksRes := <-tracksChan
|
|
|
|
|
if tracksRes.err != nil {
|
|
|
|
|
h.logger.Error("Failed to get dashboard library preview",
|
|
|
|
|
zap.Error(tracksRes.err),
|
|
|
|
|
zap.String("user_id", userID.String()),
|
|
|
|
|
)
|
|
|
|
|
RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to fetch library preview")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Aggregate stats from audit logs
|
|
|
|
|
stats := h.aggregateStats(statsRes.stats, statsPeriod)
|
|
|
|
|
|
|
|
|
|
// Convert activity logs to RecentActivity format
|
|
|
|
|
recentActivity := h.convertActivityLogs(activityRes.activity)
|
|
|
|
|
|
|
|
|
|
// Convert tracks to TrackPreview format
|
|
|
|
|
libraryPreview := h.convertTracksToPreview(tracksRes.tracks, tracksRes.total, libraryLimit)
|
|
|
|
|
|
|
|
|
|
// Build response
|
|
|
|
|
response := DashboardResponse{
|
|
|
|
|
Stats: stats,
|
|
|
|
|
RecentActivity: recentActivity,
|
|
|
|
|
LibraryPreview: libraryPreview,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Action 1.3.2.1: Use wrapped format helper
|
|
|
|
|
RespondSuccess(c, http.StatusOK, response)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// aggregateStats aggregates audit stats into dashboard stats format
|
|
|
|
|
func (h *DashboardHandler) aggregateStats(auditStats []*services.AuditStats, period string) DashboardStats {
|
|
|
|
|
stats := DashboardStats{
|
|
|
|
|
Period: period,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Aggregate stats from audit logs
|
|
|
|
|
for _, stat := range auditStats {
|
|
|
|
|
switch stat.Action {
|
|
|
|
|
case "play", "track_play":
|
|
|
|
|
stats.TracksPlayed += stat.ActionCount
|
|
|
|
|
case "message", "message_sent":
|
|
|
|
|
stats.MessagesSent += stat.ActionCount
|
|
|
|
|
case "favorite", "like", "track_like":
|
|
|
|
|
stats.Favorites += stat.ActionCount
|
|
|
|
|
}
|
|
|
|
|
// Active friends: count unique users from interactions
|
|
|
|
|
// This is a simplified calculation - could be enhanced
|
|
|
|
|
if stat.UniqueUsers > stats.ActiveFriends {
|
|
|
|
|
stats.ActiveFriends = stat.UniqueUsers
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 08:43:25 +00:00
|
|
|
// NOTE: Change percentages (vs previous period) could be added
|
data-flow: implement backend dashboard aggregation endpoint
- Created DashboardHandler that aggregates multiple data sources
- Fetches stats, activity, and library preview in parallel
- Aggregates stats from audit logs (tracks_played, messages_sent, favorites, active_friends)
- Converts audit logs to RecentActivity format with type mapping
- Converts tracks to TrackPreview format for library preview
- Supports query parameters: activity_limit, library_limit, stats_period
- Returns wrapped format {success: true, data: DashboardResponse}
- Registered route: GET /api/v1/dashboard (protected, requires auth)
- Uses interface-based approach to avoid import cycle
- Router creates wrapper function to adapt track service
- Build successful, all handlers compile correctly
- Action 2.1.1.2 complete - dashboard endpoint ready for frontend integration
2026-01-15 16:42:49 +00:00
|
|
|
// This requires fetching stats for previous period, which is out of scope for initial implementation
|
|
|
|
|
// Change percentages can be added in a follow-up task
|
|
|
|
|
|
|
|
|
|
return stats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// convertActivityLogs converts audit logs to RecentActivity format
|
|
|
|
|
func (h *DashboardHandler) convertActivityLogs(logs []*services.AuditLog) []RecentActivity {
|
|
|
|
|
activities := make([]RecentActivity, 0, len(logs))
|
|
|
|
|
|
|
|
|
|
for _, log := range logs {
|
|
|
|
|
activity := RecentActivity{
|
|
|
|
|
ID: log.ID.String(),
|
|
|
|
|
Timestamp: log.Timestamp.Format(time.RFC3339),
|
|
|
|
|
Metadata: make(map[string]interface{}),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse metadata from json.RawMessage
|
|
|
|
|
var metadataMap map[string]interface{}
|
|
|
|
|
if len(log.Metadata) > 0 {
|
|
|
|
|
if err := json.Unmarshal(log.Metadata, &metadataMap); err == nil {
|
|
|
|
|
activity.Metadata = metadataMap
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map action/resource to activity type and title
|
|
|
|
|
switch log.Action {
|
|
|
|
|
case "upload", "track_upload":
|
|
|
|
|
activity.Type = "track_upload"
|
|
|
|
|
activity.Title = "Nouvelle piste ajoutée"
|
|
|
|
|
if metadataMap != nil {
|
|
|
|
|
if fileName, ok := metadataMap["file_name"].(string); ok {
|
|
|
|
|
desc := fileName
|
|
|
|
|
activity.Description = &desc
|
|
|
|
|
}
|
|
|
|
|
if trackID, ok := metadataMap["track_id"].(string); ok {
|
|
|
|
|
activity.Metadata["track_id"] = trackID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case "message", "message_received":
|
|
|
|
|
activity.Type = "message_received"
|
|
|
|
|
activity.Title = "Message reçu"
|
|
|
|
|
if metadataMap != nil {
|
|
|
|
|
if fromUser, ok := metadataMap["from_user_id"].(string); ok {
|
|
|
|
|
activity.Metadata["from_user_id"] = fromUser
|
|
|
|
|
}
|
|
|
|
|
if convID, ok := metadataMap["conversation_id"].(string); ok {
|
|
|
|
|
activity.Metadata["conversation_id"] = convID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case "favorite", "like", "track_like":
|
|
|
|
|
activity.Type = "favorite_added"
|
|
|
|
|
activity.Title = "Piste ajoutée aux favoris"
|
|
|
|
|
if metadataMap != nil {
|
|
|
|
|
if trackID, ok := metadataMap["track_id"].(string); ok {
|
|
|
|
|
activity.Metadata["track_id"] = trackID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case "playlist_create":
|
|
|
|
|
activity.Type = "playlist_created"
|
|
|
|
|
activity.Title = "Playlist créée"
|
|
|
|
|
if metadataMap != nil {
|
|
|
|
|
if playlistID, ok := metadataMap["playlist_id"].(string); ok {
|
|
|
|
|
activity.Metadata["playlist_id"] = playlistID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case "comment", "comment_add":
|
|
|
|
|
activity.Type = "comment_added"
|
|
|
|
|
activity.Title = "Commentaire ajouté"
|
|
|
|
|
if metadataMap != nil {
|
|
|
|
|
if trackID, ok := metadataMap["track_id"].(string); ok {
|
|
|
|
|
activity.Metadata["track_id"] = trackID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
// Generic activity
|
|
|
|
|
activity.Type = "other"
|
|
|
|
|
activity.Title = fmt.Sprintf("%s: %s", log.Action, log.Resource)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
activities = append(activities, activity)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return activities
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// convertTracksToPreview converts tracks to TrackPreview format
|
|
|
|
|
func (h *DashboardHandler) convertTracksToPreview(tracks []*models.Track, total int64, limit int) LibraryPreview {
|
|
|
|
|
previews := make([]TrackPreview, 0, len(tracks))
|
|
|
|
|
|
|
|
|
|
for _, track := range tracks {
|
|
|
|
|
preview := TrackPreview{
|
2026-01-15 18:31:40 +00:00
|
|
|
ID: track.ID.String(),
|
|
|
|
|
Title: track.Title,
|
|
|
|
|
Artist: track.Artist,
|
|
|
|
|
Duration: int(track.Duration),
|
|
|
|
|
PlayCount: track.PlayCount,
|
|
|
|
|
LikeCount: track.LikeCount,
|
|
|
|
|
CreatedAt: track.CreatedAt.Format(time.RFC3339),
|
data-flow: implement backend dashboard aggregation endpoint
- Created DashboardHandler that aggregates multiple data sources
- Fetches stats, activity, and library preview in parallel
- Aggregates stats from audit logs (tracks_played, messages_sent, favorites, active_friends)
- Converts audit logs to RecentActivity format with type mapping
- Converts tracks to TrackPreview format for library preview
- Supports query parameters: activity_limit, library_limit, stats_period
- Returns wrapped format {success: true, data: DashboardResponse}
- Registered route: GET /api/v1/dashboard (protected, requires auth)
- Uses interface-based approach to avoid import cycle
- Router creates wrapper function to adapt track service
- Build successful, all handlers compile correctly
- Action 2.1.1.2 complete - dashboard endpoint ready for frontend integration
2026-01-15 16:42:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if track.CoverArtPath != "" {
|
|
|
|
|
preview.CoverArtPath = &track.CoverArtPath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
previews = append(previews, preview)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return LibraryPreview{
|
|
|
|
|
Items: previews,
|
|
|
|
|
TotalCount: total,
|
|
|
|
|
HasMore: int64(len(previews)) < total,
|
|
|
|
|
}
|
|
|
|
|
}
|