summaryrefslogtreecommitdiff
path: root/internal/handlers/settings.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-31 20:16:12 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-31 20:16:12 -1000
commitcbb0b53de1d06918c142171fd084f14f03798bc1 (patch)
treebeb642057178bce8f50e3ad67f5a62671e3e6dda /internal/handlers/settings.go
parentd39220eac03fbc5b714bde989665ed1c92dd24a5 (diff)
Add feature toggles system with settings UI (#74)
- Add feature_toggles table (migration 012) - Add source_config table for future source selection (migration 013) - Create settings page at /settings with: - Feature toggle management (enable/disable/create/delete) - Data source configuration (sync and toggle boards/calendars) - Add store methods for feature toggles and source config - Add GetCalendarList and GetTaskLists to Google API clients - Document feature toggle workflow in DESIGN.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers/settings.go')
-rw-r--r--internal/handlers/settings.go232
1 files changed, 232 insertions, 0 deletions
diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go
new file mode 100644
index 0000000..1eabdf5
--- /dev/null
+++ b/internal/handlers/settings.go
@@ -0,0 +1,232 @@
+package handlers
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+
+ "task-dashboard/internal/models"
+)
+
+// HandleSettingsPage renders the settings page
+func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
+ configs, _ := h.store.GetSourceConfigs()
+ toggles, _ := h.store.GetFeatureToggles()
+
+ // Group configs by source
+ bySource := make(map[string][]models.SourceConfig)
+ for _, cfg := range configs {
+ bySource[cfg.Source] = append(bySource[cfg.Source], cfg)
+ }
+
+ data := struct {
+ Configs map[string][]models.SourceConfig
+ Sources []string
+ Toggles []models.FeatureToggle
+ }{
+ Configs: bySource,
+ Sources: []string{"trello", "todoist", "gcal", "gtasks"},
+ Toggles: toggles,
+ }
+
+ if err := h.templates.ExecuteTemplate(w, "settings.html", data); err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed to render settings", err)
+ }
+}
+
+// HandleSyncSources fetches available items from all sources and syncs to config
+func (h *Handler) HandleSyncSources(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Sync Trello boards
+ if h.trelloClient != nil {
+ boards, err := h.trelloClient.GetBoards(ctx)
+ if err == nil {
+ var items []models.SourceConfig
+ for _, b := range boards {
+ items = append(items, models.SourceConfig{
+ Source: "trello",
+ ItemType: "board",
+ ItemID: b.ID,
+ ItemName: b.Name,
+ })
+ }
+ _ = h.store.SyncSourceConfigs("trello", "board", items)
+ }
+ }
+
+ // Sync Todoist projects
+ if h.todoistClient != nil {
+ projects, err := h.todoistClient.GetProjects(ctx)
+ if err == nil {
+ var items []models.SourceConfig
+ for _, p := range projects {
+ items = append(items, models.SourceConfig{
+ Source: "todoist",
+ ItemType: "project",
+ ItemID: p.ID,
+ ItemName: p.Name,
+ })
+ }
+ _ = h.store.SyncSourceConfigs("todoist", "project", items)
+ }
+ }
+
+ // Sync Google Calendar calendars
+ if h.googleCalendarClient != nil {
+ calendars, err := h.googleCalendarClient.GetCalendarList(ctx)
+ if err == nil {
+ var items []models.SourceConfig
+ for _, c := range calendars {
+ items = append(items, models.SourceConfig{
+ Source: "gcal",
+ ItemType: "calendar",
+ ItemID: c.ID,
+ ItemName: c.Name,
+ })
+ }
+ _ = h.store.SyncSourceConfigs("gcal", "calendar", items)
+ }
+ }
+
+ // Sync Google Tasks lists
+ if h.googleTasksClient != nil {
+ lists, err := h.googleTasksClient.GetTaskLists(ctx)
+ if err == nil {
+ var items []models.SourceConfig
+ for _, l := range lists {
+ items = append(items, models.SourceConfig{
+ Source: "gtasks",
+ ItemType: "tasklist",
+ ItemID: l.ID,
+ ItemName: l.Name,
+ })
+ }
+ _ = h.store.SyncSourceConfigs("gtasks", "tasklist", items)
+ }
+ }
+
+ // Return updated configs
+ h.HandleSettingsPage(w, r)
+}
+
+// HandleToggleSourceConfig toggles a source config item
+func (h *Handler) HandleToggleSourceConfig(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ JSONError(w, http.StatusBadRequest, "Failed to parse form", err)
+ return
+ }
+
+ source := r.FormValue("source")
+ itemType := r.FormValue("item_type")
+ itemID := r.FormValue("item_id")
+ enabled := r.FormValue("enabled") == "true"
+
+ if source == "" || itemType == "" || itemID == "" {
+ JSONError(w, http.StatusBadRequest, "Missing required fields", nil)
+ return
+ }
+
+ if err := h.store.SetSourceConfigEnabled(source, itemType, itemID, enabled); err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed to update config", err)
+ return
+ }
+
+ // Invalidate relevant cache
+ switch source {
+ case "trello":
+ _ = h.store.InvalidateCache("trello_boards")
+ case "todoist":
+ _ = h.store.InvalidateCache("todoist_tasks")
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]bool{"enabled": enabled})
+}
+
+// HandleGetSourceOptions returns available options for a source (HTMX partial)
+func (h *Handler) HandleGetSourceOptions(w http.ResponseWriter, r *http.Request) {
+ source := chi.URLParam(r, "source")
+ if source == "" {
+ JSONError(w, http.StatusBadRequest, "Source required", nil)
+ return
+ }
+
+ configs, err := h.store.GetSourceConfigsBySource(source)
+ if err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed to load configs", err)
+ return
+ }
+
+ data := struct {
+ Source string
+ Configs []models.SourceConfig
+ }{source, configs}
+
+ HTMLResponse(w, h.templates, "settings-source-options", data)
+}
+
+// HandleToggleFeature toggles a feature flag
+func (h *Handler) HandleToggleFeature(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ JSONError(w, http.StatusBadRequest, "Failed to parse form", err)
+ return
+ }
+
+ name := r.FormValue("name")
+ enabled := r.FormValue("enabled") == "true"
+
+ if name == "" {
+ JSONError(w, http.StatusBadRequest, "Feature name required", nil)
+ return
+ }
+
+ if err := h.store.SetFeatureEnabled(name, enabled); err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed to update feature", err)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]bool{"enabled": enabled})
+}
+
+// HandleCreateFeature creates a new feature toggle
+func (h *Handler) HandleCreateFeature(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ JSONError(w, http.StatusBadRequest, "Failed to parse form", err)
+ return
+ }
+
+ name := r.FormValue("name")
+ description := r.FormValue("description")
+
+ if name == "" {
+ JSONError(w, http.StatusBadRequest, "Feature name required", nil)
+ return
+ }
+
+ if err := h.store.CreateFeatureToggle(name, description, false); err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed to create feature", err)
+ return
+ }
+
+ // Return updated toggles list
+ h.HandleSettingsPage(w, r)
+}
+
+// HandleDeleteFeature removes a feature toggle
+func (h *Handler) HandleDeleteFeature(w http.ResponseWriter, r *http.Request) {
+ name := chi.URLParam(r, "name")
+ if name == "" {
+ JSONError(w, http.StatusBadRequest, "Feature name required", nil)
+ return
+ }
+
+ if err := h.store.DeleteFeatureToggle(name); err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed to delete feature", err)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}