Skip to content

Commit bb32c07

Browse files
committed
feat: new notification system and saved filter
1 parent 1c367fe commit bb32c07

File tree

64 files changed

+5193
-5195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+5193
-5195
lines changed

Dockerfile.webui

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ RUN make webui-full-rebuild
3333
# Also run the explicit tailwindcss build command
3434
RUN npx tailwindcss build -i ./internal/webui/static/css/input.css -o ./internal/webui/static/css/output.css --minify
3535

36-
# Build the webui binary from unified main.go (without GUI dependencies)
36+
# Build the webui binary from unified main.go (without desktop GUI dependencies)
3737
# Enable CGO for potential SQLite support
38-
RUN CGO_ENABLED=1 GOOS=linux go build -a -tags nogui -o webui .
38+
# Use both 'nogui' (exclude desktop) and 'webui' (include webui command) tags
39+
RUN CGO_ENABLED=1 GOOS=linux go build -a -tags "nogui,webui" -o webui .
3940

4041
# Stage 2: Runtime
4142
FROM alpine:latest

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ docker: docker-build-all docker-push-all ## Build and push all Docker images
206206
@echo "All Docker images built and pushed successfully!"
207207

208208
# Test command - build all images and restart docker-compose
209-
test: docker-build-all ## Build all Docker images and restart docker-compose
209+
test: webui-full-rebuild docker-build-all ## Build all Docker images and restart docker-compose
210210
@echo "Stopping docker-compose services..."
211211
docker-compose down
212212
@echo "Starting docker-compose services..."

alertmanager/fake/fake_alertmanager.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
},
102102
{
103103
"alertname": "NetworkLatencyHigh",
104-
"severity": "minor",
104+
"severity": "critical",
105105
"instance": "network-{instance}.example.com",
106106
"job": "ping-exporter",
107107
"team": "network",
@@ -119,7 +119,7 @@
119119
},
120120
{
121121
"alertname": "LogErrorRateHigh",
122-
"severity": "minor",
122+
"severity": "critical",
123123
"instance": "app-{instance}.example.com",
124124
"job": "log-exporter",
125125
"team": "application",
@@ -128,7 +128,7 @@
128128
},
129129
{
130130
"alertname": "BackupFailed",
131-
"severity": "major",
131+
"severity": "warning",
132132
"instance": "backup-{instance}.example.com",
133133
"job": "backup-exporter",
134134
"team": "operations",
@@ -160,10 +160,8 @@
160160

161161
# Severity levels with weights for random selection (higher weight = more frequent)
162162
SEVERITY_WEIGHTS = {
163-
"critical": 10,
164-
"major": 15,
163+
"critical": 20,
165164
"warning": 30,
166-
"minor": 25,
167165
"info": 20
168166
}
169167

@@ -276,11 +274,12 @@ def generate_random_alert():
276274
template = random.choice(ALERT_TEMPLATES)
277275
instance_id = random.randint(1, 20)
278276
instance_name = template["instance"].format(instance=instance_id)
279-
277+
280278
# 20% chance to override template severity with weighted random severity
281279
severity = get_weighted_severity() if random.random() < 0.2 else template["severity"]
282-
283-
starts_at = datetime.now(UTC) - timedelta(minutes=random.randint(1, 30))
280+
281+
# New alerts start at current time (0s duration)
282+
starts_at = datetime.now(UTC)
284283
ends_at = starts_at + timedelta(hours=random.randint(1, 6))
285284

286285
# Build base labels
@@ -730,7 +729,7 @@ def get_receivers_v1():
730729
for _ in range(20):
731730
alerts.append(generate_random_alert())
732731

733-
# Add a specific Sentry alert for testing
732+
# Add a specific Sentry alert for testing (starts at current time with 0s duration)
734733
sentry_alert = {
735734
"labels": {
736735
"alertname": "QuerylyApplicationError",
@@ -747,7 +746,7 @@ def get_receivers_v1():
747746
"summary": "Critical errors detected in Queryly backend on queryly-back-1.company.net",
748747
"runbook_url": "https://runbooks.example.com/querylyapplicationerror"
749748
},
750-
"startsAt": (datetime.now(UTC) - timedelta(minutes=15)).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
749+
"startsAt": datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
751750
"endsAt": (datetime.now(UTC) + timedelta(hours=2)).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
752751
"updatedAt": datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
753752
"generatorURL": "http://prometheus:9090/graph?g0.expr=up{job=\"queryly-back\"}&g0.tab=1",

cmd/webui.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
//go:build webui
2+
// +build webui
3+
14
package cmd
25

36
import (

docker-compose.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ services:
121121
- NOTIFICATOR_ALERTMANAGERS_0_URL=http://alertmanager:9093
122122
- NOTIFICATOR_ALERTMANAGERS_0_OAUTH_ENABLED=false
123123
- NOTIFICATOR_ALERTMANAGERS_0_OAUTH_PROXY_MODE=true
124-
- NOTIFICATOR_ALERTMANAGERS_1_NAME=AlertManager Numberly
125-
- NOTIFICATOR_ALERTMANAGERS_1_URL=https://alertmanager.prometheus1.numberly.in
126-
- NOTIFICATOR_ALERTMANAGERS_1_OAUTH_ENABLED=false
127-
- NOTIFICATOR_ALERTMANAGERS_1_OAUTH_PROXY_MODE=true
124+
# - NOTIFICATOR_ALERTMANAGERS_1_NAME=AlertManager Numberly
125+
# - NOTIFICATOR_ALERTMANAGERS_1_URL=https://alertmanager.prometheus1.numberly.in
126+
# - NOTIFICATOR_ALERTMANAGERS_1_OAUTH_ENABLED=false
127+
# - NOTIFICATOR_ALERTMANAGERS_1_OAUTH_PROXY_MODE=true
128128
# Logging
129129
- NOTIFICATOR_LOG_LEVEL=info
130130
- NOTIFICATOR_SENTRY_ENABLED=true

internal/backend/database/gorm_db.go

Lines changed: 111 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,13 @@ func (gdb *GormDB) AutoMigrate() error {
9898
&models.Acknowledgment{},
9999
&models.ResolvedAlert{},
100100
&mainmodels.UserColorPreference{},
101-
&mainmodels.UserNotificationPreference{},
101+
// Browser notifications
102+
&models.NotificationPreference{},
102103
// Hidden alerts tables
103104
&models.UserHiddenAlert{},
104105
&models.UserHiddenRule{},
106+
// Filter presets
107+
&models.FilterPreset{},
105108
// OAuth tables
106109
&models.UserGroup{},
107110
&models.OAuthToken{},
@@ -449,56 +452,6 @@ func (gdb *GormDB) DeleteUserColorPreference(userID, preferenceID string) error
449452
return nil
450453
}
451454

452-
// GetUserNotificationPreference gets the notification preference for a user
453-
func (gdb *GormDB) GetUserNotificationPreference(userID string) (*mainmodels.UserNotificationPreference, error) {
454-
var preference mainmodels.UserNotificationPreference
455-
err := gdb.db.Where("user_id = ?", userID).First(&preference).Error
456-
457-
if err != nil {
458-
if err == gorm.ErrRecordNotFound {
459-
// Return nil, nil to indicate no preference exists (not an error)
460-
return nil, nil
461-
}
462-
return nil, fmt.Errorf("failed to get user notification preference: %w", err)
463-
}
464-
465-
return &preference, nil
466-
}
467-
468-
// SaveUserNotificationPreference saves or updates a user's notification preference
469-
func (gdb *GormDB) SaveUserNotificationPreference(userID string, pref *mainmodels.UserNotificationPreference) error {
470-
// Set the user ID
471-
pref.UserID = userID
472-
473-
// Generate ID if not set
474-
if pref.ID == "" {
475-
pref.ID = generateUUID()
476-
}
477-
478-
// Try to find existing preference
479-
var existing mainmodels.UserNotificationPreference
480-
err := gdb.db.Where("user_id = ?", userID).First(&existing).Error
481-
482-
if err == gorm.ErrRecordNotFound {
483-
// Create new preference
484-
if err := gdb.db.Create(pref).Error; err != nil {
485-
return fmt.Errorf("failed to create user notification preference: %w", err)
486-
}
487-
log.Printf("Created new notification preference for user %s", userID)
488-
} else if err != nil {
489-
return fmt.Errorf("failed to query existing preference: %w", err)
490-
} else {
491-
// Update existing preference (keep the original ID)
492-
pref.ID = existing.ID
493-
if err := gdb.db.Where("user_id = ?", userID).Updates(pref).Error; err != nil {
494-
return fmt.Errorf("failed to update user notification preference: %w", err)
495-
}
496-
log.Printf("Updated notification preference for user %s", userID)
497-
}
498-
499-
return nil
500-
}
501-
502455
// generateUUID generates a simple UUID for database records
503456
func generateUUID() string {
504457
bytes := make([]byte, 16)
@@ -673,3 +626,110 @@ func (gdb *GormDB) ClearUserHiddenAlerts(userID string) (int64, error) {
673626
}
674627
return result.RowsAffected, nil
675628
}
629+
630+
// Filter Presets Methods
631+
632+
// CreateFilterPreset creates a new filter preset for a user
633+
func (gdb *GormDB) CreateFilterPreset(preset *models.FilterPreset) (*models.FilterPreset, error) {
634+
if err := gdb.db.Create(preset).Error; err != nil {
635+
return nil, fmt.Errorf("failed to create filter preset: %w", err)
636+
}
637+
return preset, nil
638+
}
639+
640+
// GetFilterPresets gets all filter presets for a user (private + shared)
641+
func (gdb *GormDB) GetFilterPresets(userID string, includeShared bool) ([]models.FilterPreset, error) {
642+
var presets []models.FilterPreset
643+
644+
query := gdb.db.Where("user_id = ?", userID)
645+
646+
if includeShared {
647+
// Get user's own presets + shared presets from others
648+
query = gdb.db.Where("user_id = ? OR is_shared = ?", userID, true)
649+
}
650+
651+
err := query.Order("is_default DESC, created_at DESC").Find(&presets).Error
652+
if err != nil {
653+
return nil, fmt.Errorf("failed to get filter presets: %w", err)
654+
}
655+
656+
return presets, nil
657+
}
658+
659+
// GetFilterPresetByID gets a specific filter preset by ID
660+
func (gdb *GormDB) GetFilterPresetByID(id string) (*models.FilterPreset, error) {
661+
var preset models.FilterPreset
662+
err := gdb.db.Where("id = ?", id).First(&preset).Error
663+
if err != nil {
664+
return nil, err
665+
}
666+
return &preset, nil
667+
}
668+
669+
// UpdateFilterPreset updates an existing filter preset
670+
func (gdb *GormDB) UpdateFilterPreset(preset *models.FilterPreset) error {
671+
if err := gdb.db.Save(preset).Error; err != nil {
672+
return fmt.Errorf("failed to update filter preset: %w", err)
673+
}
674+
return nil
675+
}
676+
677+
// DeleteFilterPreset deletes a filter preset (with ownership check)
678+
func (gdb *GormDB) DeleteFilterPreset(id, userID string) error {
679+
result := gdb.db.Where("id = ? AND user_id = ?", id, userID).Delete(&models.FilterPreset{})
680+
if result.Error != nil {
681+
return result.Error
682+
}
683+
if result.RowsAffected == 0 {
684+
return fmt.Errorf("filter preset not found or not authorized")
685+
}
686+
return nil
687+
}
688+
689+
// SetDefaultFilterPreset sets a filter preset as default (and unsets others)
690+
func (gdb *GormDB) SetDefaultFilterPreset(id, userID string) error {
691+
tx := gdb.db.Begin()
692+
if tx.Error != nil {
693+
return tx.Error
694+
}
695+
defer func() {
696+
if r := recover(); r != nil {
697+
tx.Rollback()
698+
}
699+
}()
700+
701+
// Unset all defaults for this user
702+
if err := tx.Model(&models.FilterPreset{}).
703+
Where("user_id = ?", userID).
704+
Update("is_default", false).Error; err != nil {
705+
tx.Rollback()
706+
return fmt.Errorf("failed to unset existing defaults: %w", err)
707+
}
708+
709+
// Set the new default (with ownership check)
710+
result := tx.Model(&models.FilterPreset{}).
711+
Where("id = ? AND user_id = ?", id, userID).
712+
Update("is_default", true)
713+
714+
if result.Error != nil {
715+
tx.Rollback()
716+
return result.Error
717+
}
718+
719+
if result.RowsAffected == 0 {
720+
tx.Rollback()
721+
return fmt.Errorf("filter preset not found or not authorized")
722+
}
723+
724+
return tx.Commit().Error
725+
}
726+
727+
// GetDefaultFilterPreset gets the default filter preset for a user
728+
func (gdb *GormDB) GetDefaultFilterPreset(userID string) (*models.FilterPreset, error) {
729+
var preset models.FilterPreset
730+
err := gdb.db.Where("user_id = ? AND is_default = ?", userID, true).First(&preset).Error
731+
if err != nil {
732+
return nil, err
733+
}
734+
return &preset, nil
735+
}

internal/backend/database/migrate.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
func (gdb *GormDB) RunCustomMigrations() error {
1010
log.Println("🔄 Running custom migrations...")
1111

12+
// Drop user_notification_preferences table (removed feature)
13+
if err := gdb.dropUserNotificationPreferences(); err != nil {
14+
return fmt.Errorf("failed to drop user_notification_preferences: %w", err)
15+
}
16+
1217
// Check if UserSentryConfig table needs user_id column type migration
1318
if err := gdb.migrateUserSentryConfigUserID(); err != nil {
1419
return fmt.Errorf("failed to migrate UserSentryConfig.user_id: %w", err)
@@ -80,5 +85,37 @@ func (gdb *GormDB) migrateUserSentryConfigUserID() error {
8085
log.Println("user_sentry_configs.user_id is already varchar type, no migration needed")
8186
}
8287

88+
return nil
89+
}
90+
91+
// dropUserNotificationPreferences drops the user_notification_preferences table if it exists
92+
func (gdb *GormDB) dropUserNotificationPreferences() error {
93+
// Try PostgreSQL-style check first
94+
var tableExists bool
95+
err := gdb.db.Raw("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_notification_preferences')").Scan(&tableExists).Error
96+
97+
if err != nil {
98+
// Probably SQLite - use simpler approach
99+
log.Println("Using SQLite-compatible approach for user_notification_preferences table drop")
100+
if err := gdb.db.Exec("DROP TABLE IF EXISTS user_notification_preferences").Error; err != nil {
101+
log.Printf("Warning: Could not drop user_notification_preferences table: %v", err)
102+
// Don't fail on this - table might not exist
103+
return nil
104+
}
105+
log.Println("✅ Dropped user_notification_preferences table (if it existed)")
106+
return nil
107+
}
108+
109+
// PostgreSQL path
110+
if tableExists {
111+
log.Println("🔄 Dropping user_notification_preferences table")
112+
if err := gdb.db.Exec("DROP TABLE IF EXISTS user_notification_preferences CASCADE").Error; err != nil {
113+
return fmt.Errorf("failed to drop user_notification_preferences table: %w", err)
114+
}
115+
log.Println("✅ Successfully dropped user_notification_preferences table")
116+
} else {
117+
log.Println("user_notification_preferences table does not exist, skipping drop")
118+
}
119+
83120
return nil
84121
}

0 commit comments

Comments
 (0)