v0.4.5 - Calendar View, iCal Export & More #54

Merged
bscott merged 6 commits from v0.4.5 into main 2025-11-13 22:03:48 -05:00
bscott commented 2025-11-13 21:50:41 -05:00 (Migrated from github.com)

🎉 Release v0.4.5

This PR includes all the features and improvements for v0.4.5:

New Features

  • 📅 Calendar View & iCal Export - Interactive calendar showing subscription renewal dates with export functionality
  • 📧 Enhanced Email Notifications - SMTP TLS/SSL support and high-cost alerts
  • 🎨 Subscription Icons/Logos - Automatic logo fetching from website URLs
  • 📊 Table Sorting - Sortable subscription columns
  • 📅 Quarterly Schedule Support - New quarterly subscription option
  • 🔄 Automatic Renewal Date Updates - Smart date management

🐛 Bug Fixes

  • Dark mode styling improvements
  • Dashboard subscription limit removed
  • JavaScript syntax fixes
  • Date calculation improvements

📝 Resolved Issues

  • PR #51 (SMTP TLS/SSL Support) - Resolves #50
  • Issue #48 (Dark Mode Fixes)
  • Issue #6 (Subscription Icons)
  • Issue #27 (Table Sorting)
  • Issue #49 (Quarterly Schedule)
  • Issue #29 (Auto-update Renewal Dates)

See the full release notes in plans/v0.4.5_RELEASE_NOTES.md for complete details.

Ready for review and merge!

## 🎉 Release v0.4.5 This PR includes all the features and improvements for v0.4.5: ### ✨ New Features - 📅 **Calendar View & iCal Export** - Interactive calendar showing subscription renewal dates with export functionality - 📧 **Enhanced Email Notifications** - SMTP TLS/SSL support and high-cost alerts - 🎨 **Subscription Icons/Logos** - Automatic logo fetching from website URLs - 📊 **Table Sorting** - Sortable subscription columns - 📅 **Quarterly Schedule Support** - New quarterly subscription option - 🔄 **Automatic Renewal Date Updates** - Smart date management ### 🐛 Bug Fixes - Dark mode styling improvements - Dashboard subscription limit removed - JavaScript syntax fixes - Date calculation improvements ### 📝 Resolved Issues - PR #51 (SMTP TLS/SSL Support) - Resolves #50 - Issue #48 (Dark Mode Fixes) - Issue #6 (Subscription Icons) - Issue #27 (Table Sorting) - Issue #49 (Quarterly Schedule) - Issue #29 (Auto-update Renewal Dates) See the full release notes in `plans/v0.4.5_RELEASE_NOTES.md` for complete details. Ready for review and merge!
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2025-11-13 22:00:15 -05:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull Request Overview

This release adds several major features to SubTrackr focused on calendar visualization, icon display, email notifications, and table sorting capabilities.

Key Changes:

  • Calendar view with iCal export functionality for visualizing subscription renewal dates
  • Automatic subscription logo/icon fetching from website URLs using Google's favicon service
  • Enhanced SMTP email service with TLS/SSL support (STARTTLS and implicit TLS) and high-cost alerts
  • Sortable subscription table with multiple column sorting options
  • Quarterly subscription schedule support
  • Automatic renewal date updates via GORM hooks
  • Dark mode improvements across settings and notification components

Reviewed Changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
templates/subscriptions.html Converted list view to sortable table with icon display
templates/subscription-list.html Restructured to table format with sorting controls
templates/subscription-form.html Added Quarterly schedule option and renewal date tooltip
templates/smtp-message.html Added dark mode styling for error/success messages
templates/settings.html Enhanced SMTP config with To field and comprehensive dark mode support
templates/dashboard.html Added icon display for subscriptions
templates/calendar.html New calendar view with interactive renewal date visualization
templates/analytics.html Added calendar navigation link
internal/version/version.go Updated version to v0.4.5
internal/service/subscription.go Added GetAllSorted method for table sorting
internal/service/logo.go New service for fetching subscription logos from URLs
internal/service/email.go New email service with TLS/SSL support and high-cost alerts
internal/repository/subscription.go Added sorting logic and Quarterly schedule support
internal/models/subscription.go Added Quarterly schedule, IconURL field, and auto-renewal hooks
internal/models/settings.go Added To field to SMTP configuration
internal/handlers/subscription.go Added Calendar, ExportICal handlers and logo/email integration
internal/handlers/settings.go Enhanced SMTP testing with TLS/SSL support
internal/database/migrations.go Added migration for icon_url field
cmd/server/main.go Initialized new services and routes, added template functions
agent.md New comprehensive project documentation
.gitignore Excluded plans directory

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull Request Overview This release adds several major features to SubTrackr focused on calendar visualization, icon display, email notifications, and table sorting capabilities. **Key Changes:** - Calendar view with iCal export functionality for visualizing subscription renewal dates - Automatic subscription logo/icon fetching from website URLs using Google's favicon service - Enhanced SMTP email service with TLS/SSL support (STARTTLS and implicit TLS) and high-cost alerts - Sortable subscription table with multiple column sorting options - Quarterly subscription schedule support - Automatic renewal date updates via GORM hooks - Dark mode improvements across settings and notification components ### Reviewed Changes Copilot reviewed 20 out of 21 changed files in this pull request and generated 13 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | templates/subscriptions.html | Converted list view to sortable table with icon display | | templates/subscription-list.html | Restructured to table format with sorting controls | | templates/subscription-form.html | Added Quarterly schedule option and renewal date tooltip | | templates/smtp-message.html | Added dark mode styling for error/success messages | | templates/settings.html | Enhanced SMTP config with To field and comprehensive dark mode support | | templates/dashboard.html | Added icon display for subscriptions | | templates/calendar.html | New calendar view with interactive renewal date visualization | | templates/analytics.html | Added calendar navigation link | | internal/version/version.go | Updated version to v0.4.5 | | internal/service/subscription.go | Added GetAllSorted method for table sorting | | internal/service/logo.go | New service for fetching subscription logos from URLs | | internal/service/email.go | New email service with TLS/SSL support and high-cost alerts | | internal/repository/subscription.go | Added sorting logic and Quarterly schedule support | | internal/models/subscription.go | Added Quarterly schedule, IconURL field, and auto-renewal hooks | | internal/models/settings.go | Added To field to SMTP configuration | | internal/handlers/subscription.go | Added Calendar, ExportICal handlers and logo/email integration | | internal/handlers/settings.go | Enhanced SMTP testing with TLS/SSL support | | internal/database/migrations.go | Added migration for icon_url field | | cmd/server/main.go | Initialized new services and routes, added template functions | | agent.md | New comprehensive project documentation | | .gitignore | Excluded plans directory | </details> --- 💡 <a href="/bscott/subtrackr/new/main/.github/instructions?filename=*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:12 -05:00

The template function int is defined twice - once in the main function and again in the loadTemplates function. This creates code duplication. Consider extracting the template functions map into a shared variable or function to avoid duplication.

The template function `int` is defined twice - once in the main function and again in the `loadTemplates` function. This creates code duplication. Consider extracting the template functions map into a shared variable or function to avoid duplication.
@ -154,0 +166,4 @@
}
log.Println("Running migration: Adding subscription icon URLs...")
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:13 -05:00

The migration uses SQLite-specific syntax pragma_table_info which will fail on other database engines. While the project currently uses SQLite, this creates a hard dependency. Consider using GORM's cross-database Migrator().HasColumn() method instead for better portability.

The migration uses SQLite-specific syntax `pragma_table_info` which will fail on other database engines. While the project currently uses SQLite, this creates a hard dependency. Consider using GORM's cross-database `Migrator().HasColumn()` method instead for better portability.
@ -24,107 +25,156 @@ func NewSettingsHandler(service *service.SettingsService) *SettingsHandler {
// SaveSMTPSettings saves SMTP configuration
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:14 -05:00

The test SMTP connection function doesn't validate the To email field before testing, but this field is used in production email sending. While it checks Host, Port, Username, and Password, the missing validation for To could lead to confusion when the test passes but actual emails fail due to missing recipient. Consider adding config.To == "" to the validation check.

The test SMTP connection function doesn't validate the `To` email field before testing, but this field is used in production email sending. While it checks Host, Port, Username, and Password, the missing validation for `To` could lead to confusion when the test passes but actual emails fail due to missing recipient. Consider adding `config.To == ""` to the validation check.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:14 -05:00

The error message states "All SMTP fields are required (including To email)" but this doesn't mention FromName which is also being parsed and could be required in some contexts. Consider clarifying which fields are truly required vs optional in the error message.

			"Error": "Required SMTP fields: Host, Port, Username, Password, From email, To email.",
The error message states "All SMTP fields are required (including To email)" but this doesn't mention FromName which is also being parsed and could be required in some contexts. Consider clarifying which fields are truly required vs optional in the error message. ```suggestion "Error": "Required SMTP fields: Host, Port, Username, Password, From email, To email.", ```
@ -104,2 +99,2 @@
})
return
auth := smtp.PlainAuth("", config.Username, config.Password, config.Host)
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:14 -05:00

This assignment to To is useless since its value is never read.


This assignment to To is useless since its value is never read. ```suggestion ```
@ -176,3 +359,4 @@
"DarkMode": h.settingsService.IsDarkModeEnabled(),
"Version": version.GetVersion(),
"SMTPConfig": smtpConfig,
})
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:13 -05:00

Potential XSS vulnerability: The EventsByDate is serialized to JSON and then cast to template.JS without proper sanitization. While JSON encoding provides some protection, event names containing </script> or other HTML/JS sequences could potentially break out of the JavaScript context. Consider using proper HTML escaping or validating the JSON output before injecting into the template.

		"EventsByDate":   string(eventsJSON),
Potential XSS vulnerability: The `EventsByDate` is serialized to JSON and then cast to `template.JS` without proper sanitization. While JSON encoding provides some protection, event names containing `</script>` or other HTML/JS sequences could potentially break out of the JavaScript context. Consider using proper HTML escaping or validating the JSON output before injecting into the template. ```suggestion "EventsByDate": string(eventsJSON), ```
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:13 -05:00

Potential XSS vulnerability: User-supplied data from sub.Name and sub.URL are being interpolated directly into the iCal description without proper escaping. The description field construction uses fmt.Sprintf with unescaped strings. While the comment mentions "should escape commas, semicolons, etc.", this hasn't been implemented. iCal format requires escaping special characters like commas, semicolons, backslashes, and newlines to prevent format corruption or potential security issues.

Potential XSS vulnerability: User-supplied data from `sub.Name` and `sub.URL` are being interpolated directly into the iCal description without proper escaping. The description field construction uses `fmt.Sprintf` with unescaped strings. While the comment mentions "should escape commas, semicolons, etc.", this hasn't been implemented. iCal format requires escaping special characters like commas, semicolons, backslashes, and newlines to prevent format corruption or potential security issues.
@ -93,3 +142,3 @@
}
// Also calculate if renewal date is nil and status is Active
// Calculate if renewal date is nil and status is Active
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:12 -05:00

The AfterFind hook modifies the database using tx.Model(s).UpdateColumn which could cause performance issues when loading many subscriptions at once (N+1 updates). This also means every time subscriptions are fetched, there could be database writes. Consider using a scheduled background job or manual update trigger instead of automatic updates on every database read.

The AfterFind hook modifies the database using `tx.Model(s).UpdateColumn` which could cause performance issues when loading many subscriptions at once (N+1 updates). This also means every time subscriptions are fetched, there could be database writes. Consider using a scheduled background job or manual update trigger instead of automatic updates on every database read.
@ -90,3 +132,4 @@
func (r *SubscriptionRepository) GetByID(id uint) (*models.Subscription, error) {
var subscription models.Subscription
if err := r.db.Preload("Category").First(&subscription, id).Error; err != nil {
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:14 -05:00

[nitpick] The GetAllSorted function is vulnerable to SQL injection through the sortColumn variable. While there is validation against a whitelist (validSortColumns), the sortColumn value is then directly concatenated into the SQL ORDER BY clause on line 120. For the "category" sort, this could be exploited if the validation logic has any flaws. Consider using GORM's safer ordering methods or ensure the validation is airtight. The current implementation appears safe due to the whitelist, but it's a pattern that could be fragile if modified.

	// Build order clause using GORM's safer method
	// Special handling for category (requires join)
	if sortBy == "category" {
		query = query.Joins("LEFT JOIN categories ON subscriptions.category_id = categories.id")
	}

	// Use GORM's Order with separate arguments to avoid SQL injection
	if err := query.Order(sortColumn + " " + order).Find(&subscriptions).Error; err != nil {
[nitpick] The `GetAllSorted` function is vulnerable to SQL injection through the `sortColumn` variable. While there is validation against a whitelist (`validSortColumns`), the `sortColumn` value is then directly concatenated into the SQL ORDER BY clause on line 120. For the "category" sort, this could be exploited if the validation logic has any flaws. Consider using GORM's safer ordering methods or ensure the validation is airtight. The current implementation appears safe due to the whitelist, but it's a pattern that could be fragile if modified. ```suggestion // Build order clause using GORM's safer method // Special handling for category (requires join) if sortBy == "category" { query = query.Joins("LEFT JOIN categories ON subscriptions.category_id = categories.id") } // Use GORM's Order with separate arguments to avoid SQL injection if err := query.Order(sortColumn + " " + order).Find(&subscriptions).Error; err != nil { ```
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:12 -05:00

[nitpick] The SQL query uses a long CASE statement with hardcoded schedule types. After adding "Quarterly", this same pattern needs updating. The query has been updated correctly, but consider extracting schedule-to-multiplier logic into a database function or Go function to avoid maintaining this logic in multiple places (this appears in at least 3 places: model methods, handler, and this query).

[nitpick] The SQL query uses a long CASE statement with hardcoded schedule types. After adding "Quarterly", this same pattern needs updating. The query has been updated correctly, but consider extracting schedule-to-multiplier logic into a database function or Go function to avoid maintaining this logic in multiple places (this appears in at least 3 places: model methods, handler, and this query).
@ -0,0 +156,4 @@
if err != nil {
return fmt.Errorf("failed to close writer: %w", err)
}
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:13 -05:00

The SSL/TLS connection handling has code duplication between lines 44-101 and lines 107-158. The message building logic (lines 81-92 and 138-149) is duplicated. Consider extracting the common message building and sending logic into a helper function to improve maintainability.

The SSL/TLS connection handling has code duplication between lines 44-101 and lines 107-158. The message building logic (lines 81-92 and 138-149) is duplicated. Consider extracting the common message building and sending logic into a helper function to improve maintainability.
@ -0,0 +84,4 @@
}
return fetchedURL
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:14 -05:00

The LogoService makes external HTTP requests to fetch favicons without any rate limiting or caching mechanism. This could lead to performance issues and potential abuse if many subscriptions are created/updated rapidly. Consider implementing caching (e.g., in-memory cache with TTL) or rate limiting for external requests.

The LogoService makes external HTTP requests to fetch favicons without any rate limiting or caching mechanism. This could lead to performance issues and potential abuse if many subscriptions are created/updated rapidly. Consider implementing caching (e.g., in-memory cache with TTL) or rate limiting for external requests.
@ -0,0 +226,4 @@
${iconHtml}<span class="truncate">${eventName}</span>
</span>
<span class="ml-2 flex-shrink-0 font-medium">${currencySymbol}${cost}</span>
</button>`;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-11-13 22:00:12 -05:00

The calendar JavaScript performs client-side HTML construction with user data that uses basic escaping (.replace(/"/g, '&quot;').replace(/'/g, '&#39;')). However, this doesn't escape other potentially dangerous characters like <, >, or & itself. While the iconURL escaping is done, the event name could still contain HTML that breaks the layout or causes issues. Consider using proper HTML escaping or the browser's textContent property when creating elements.

                        // Create button element
                        const button = document.createElement('button');
                        button.setAttribute('hx-get', `/form/subscription/${eventId}`);
                        button.setAttribute('hx-target', '#modal-content');
                        button.setAttribute('hx-swap', 'innerHTML');
                        button.setAttribute('hx-trigger', 'click');
                        button.className = 'w-full text-left text-xs px-2 py-1 rounded bg-primary/20 dark:bg-primary/30 text-primary dark:text-primary-light hover:bg-primary/30 dark:hover:bg-primary/40 transition-colors cursor-pointer flex items-center justify-between';
                        button.title = `${eventName} - ${currencySymbol}${cost}`;
                        button.onclick = function() { setTimeout(function() { document.getElementById('modal').classList.remove('hidden'); }, 50); };

                        // Left side: icon + event name
                        const leftSpan = document.createElement('span');
                        leftSpan.className = 'flex items-center min-w-0 flex-1';
                        if (hasIcon) {
                            const img = document.createElement('img');
                            img.src = iconURL.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
                            img.alt = eventName;
                            img.className = 'w-3 h-3 rounded mr-1.5 flex-shrink-0 inline-block';
                            img.style.objectFit = 'contain';
                            img.onerror = function() { this.style.display = 'none'; };
                            leftSpan.appendChild(img);
                        }
                        const nameSpan = document.createElement('span');
                        nameSpan.className = 'truncate';
                        nameSpan.textContent = eventName;
                        leftSpan.appendChild(nameSpan);

                        // Right side: cost
                        const rightSpan = document.createElement('span');
                        rightSpan.className = 'ml-2 flex-shrink-0 font-medium';
                        rightSpan.textContent = `${currencySymbol}${cost}`;

                        button.appendChild(leftSpan);
                        button.appendChild(rightSpan);

                        // Add button to content
                        const wrapper = document.createElement('div');
                        wrapper.appendChild(button);
                        content += wrapper.innerHTML;
The calendar JavaScript performs client-side HTML construction with user data that uses basic escaping (`.replace(/"/g, '&quot;').replace(/'/g, '&#39;')`). However, this doesn't escape other potentially dangerous characters like `<`, `>`, or `&` itself. While the `iconURL` escaping is done, the event name could still contain HTML that breaks the layout or causes issues. Consider using proper HTML escaping or the browser's `textContent` property when creating elements. ```suggestion // Create button element const button = document.createElement('button'); button.setAttribute('hx-get', `/form/subscription/${eventId}`); button.setAttribute('hx-target', '#modal-content'); button.setAttribute('hx-swap', 'innerHTML'); button.setAttribute('hx-trigger', 'click'); button.className = 'w-full text-left text-xs px-2 py-1 rounded bg-primary/20 dark:bg-primary/30 text-primary dark:text-primary-light hover:bg-primary/30 dark:hover:bg-primary/40 transition-colors cursor-pointer flex items-center justify-between'; button.title = `${eventName} - ${currencySymbol}${cost}`; button.onclick = function() { setTimeout(function() { document.getElementById('modal').classList.remove('hidden'); }, 50); }; // Left side: icon + event name const leftSpan = document.createElement('span'); leftSpan.className = 'flex items-center min-w-0 flex-1'; if (hasIcon) { const img = document.createElement('img'); img.src = iconURL.replace(/"/g, '&quot;').replace(/'/g, '&#39;'); img.alt = eventName; img.className = 'w-3 h-3 rounded mr-1.5 flex-shrink-0 inline-block'; img.style.objectFit = 'contain'; img.onerror = function() { this.style.display = 'none'; }; leftSpan.appendChild(img); } const nameSpan = document.createElement('span'); nameSpan.className = 'truncate'; nameSpan.textContent = eventName; leftSpan.appendChild(nameSpan); // Right side: cost const rightSpan = document.createElement('span'); rightSpan.className = 'ml-2 flex-shrink-0 font-medium'; rightSpan.textContent = `${currencySymbol}${cost}`; button.appendChild(leftSpan); button.appendChild(rightSpan); // Add button to content const wrapper = document.createElement('div'); wrapper.appendChild(button); content += wrapper.innerHTML; ```
bscott (Migrated from github.com) reviewed 2025-11-13 22:02:52 -05:00
@ -104,2 +99,2 @@
})
return
auth := smtp.PlainAuth("", config.Username, config.Password, config.Host)
bscott (Migrated from github.com) commented 2025-11-13 22:02:52 -05:00

invalid, ignoring

invalid, ignoring
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
bscott/subtrackr!54
No description provided.