summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-06 18:19:27 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-06 18:19:27 +0000
commit06450fe69ade2928deb9274bb67b7ba60d394b4f (patch)
tree328c3e2f5608663669d75330bfde1e61ab713499
parentf7d18eae924a221f12293c3063e46b791468623f (diff)
Remove the feature toggle system (dead code)
Audited it (couldn't query the live DB directly -- auto-mode classifier blocks direct production reads without prior approval -- so this is a code-only audit): GetFeatureToggles/SetFeatureEnabled/IsFeatureEnabled/ CreateFeatureToggle/DeleteFeatureToggle had exactly one caller each, all inside their own CRUD handlers. Nothing anywhere else in the codebase read a toggle's Enabled state to gate any actual behavior -- confirmed by grepping every remaining .Enabled/IsFeatureEnabled reference back to either this dead code or its own tests. It was pure UI-managed CRUD with no consumer, unlike Trusted Agents (wired into agent.go/websocket.go) or Data Sources (wired into the sync pipeline) which stayed. Removes the Settings page section, the three /settings/features* routes and handlers, the five Store methods, the FeatureToggle model, and adds 028_drop_feature_toggles.sql (next free migration number, per this repo's convention of never renumbering -- see 021_drop_tasks.sql for the same drop-table-forward pattern) to drop the now-unused table. Also removed the now-dead tests for all of the above. go build ./... and go test ./... both clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
-rw-r--r--cmd/dashboard/main.go3
-rw-r--r--internal/handlers/buckets_web_test.go1
-rw-r--r--internal/handlers/handlers_test.go110
-rw-r--r--internal/handlers/settings.go54
-rw-r--r--internal/models/types.go7
-rw-r--r--internal/store/sqlite.go61
-rw-r--r--internal/store/sqlite_test.go94
-rw-r--r--migrations/028_drop_feature_toggles.sql4
-rw-r--r--web/templates/settings.html44
9 files changed, 4 insertions, 374 deletions
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index 89d12d1..f83fea1 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -381,9 +381,6 @@ func main() {
r.Post("/settings/sync", h.HandleSyncSources)
r.Post("/settings/clear-cache", h.HandleClearCache)
r.Post("/settings/toggle", h.HandleToggleSourceConfig)
- r.Post("/settings/features", h.HandleCreateFeature)
- r.Post("/settings/features/toggle", h.HandleToggleFeature)
- r.Delete("/settings/features/{name}", h.HandleDeleteFeature)
r.Delete("/settings/agents/{id}", h.HandleDeleteAgent)
// Maintenance bucket management (Settings page)
diff --git a/internal/handlers/buckets_web_test.go b/internal/handlers/buckets_web_test.go
index 9e09ca0..befdc01 100644
--- a/internal/handlers/buckets_web_test.go
+++ b/internal/handlers/buckets_web_test.go
@@ -169,7 +169,6 @@ func TestHandleSettingsPage_IncludesBucketsProjectsLabels(t *testing.T) {
data, ok := lastCall.Data.(struct {
Configs map[string][]models.SourceConfig
Sources []string
- Toggles []models.FeatureToggle
SyncLog []store.SyncLogEntry
Agents []models.Agent
Buckets []models.BucketSummary
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index 9d92faa..4c776b0 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -1122,116 +1122,6 @@ func TestHandleUnifiedAdd_MissingContent(t *testing.T) {
}
// =============================================================================
-// Settings Handler Tests
-// =============================================================================
-
-func TestHandleToggleFeature(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- h := &Handler{
- store: db,
- renderer: newTestRenderer(),
- config: &config.Config{},
- }
-
- // Create a feature toggle
- _ = db.CreateFeatureToggle("test_feature", "Test feature", false)
-
- req := httptest.NewRequest("POST", "/settings/feature/toggle", nil)
- req.Form = map[string][]string{
- "name": {"test_feature"},
- "enabled": {"true"},
- }
- w := httptest.NewRecorder()
-
- h.HandleToggleFeature(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("Expected status 200, got %d", w.Code)
- }
-
- // Verify feature was enabled
- if !db.IsFeatureEnabled("test_feature") {
- t.Error("Feature should be enabled after toggle")
- }
-}
-
-func TestHandleCreateFeature(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- h := &Handler{
- store: db,
- renderer: newTestRenderer(),
- config: &config.Config{},
- }
-
- req := httptest.NewRequest("POST", "/settings/feature/create", nil)
- req.Form = map[string][]string{
- "name": {"new_feature"},
- "description": {"A new feature"},
- }
- w := httptest.NewRecorder()
-
- h.HandleCreateFeature(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("Expected status 200, got %d", w.Code)
- }
-
- // Verify feature was created
- toggles, _ := db.GetFeatureToggles()
- found := false
- for _, t := range toggles {
- if t.Name == "new_feature" {
- found = true
- break
- }
- }
- if !found {
- t.Error("Feature should be created")
- }
-}
-
-func TestHandleDeleteFeature(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- h := &Handler{
- store: db,
- renderer: newTestRenderer(),
- config: &config.Config{},
- }
-
- // Create a feature to delete
- _ = db.CreateFeatureToggle("delete_me", "To be deleted", false)
-
- req := httptest.NewRequest("DELETE", "/settings/feature/delete_me", nil)
-
- // Add chi URL params
- rctx := chi.NewRouteContext()
- rctx.URLParams.Add("name", "delete_me")
- req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
-
- w := httptest.NewRecorder()
-
- h.HandleDeleteFeature(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("Expected status 200, got %d", w.Code)
- }
-
- // Verify feature was deleted
- toggles, _ := db.GetFeatureToggles()
- for _, toggle := range toggles {
- if toggle.Name == "delete_me" {
- t.Error("Feature should be deleted")
- }
- }
-}
-
-// =============================================================================
// Response Helper Tests
// =============================================================================
diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go
index 43fce20..5ba3724 100644
--- a/internal/handlers/settings.go
+++ b/internal/handlers/settings.go
@@ -4,8 +4,6 @@ import (
"encoding/json"
"net/http"
- "github.com/go-chi/chi/v5"
-
"task-dashboard/internal/auth"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
@@ -14,7 +12,6 @@ import (
// HandleSettingsPage renders the settings page
func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
configs, _ := h.store.GetSourceConfigs()
- toggles, _ := h.store.GetFeatureToggles()
syncLog, _ := h.store.GetRecentSyncLog(20)
agents, _ := h.store.GetAllAgents()
buckets, _ := BuildBucketSummaries(h.store)
@@ -30,7 +27,6 @@ func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
data := struct {
Configs map[string][]models.SourceConfig
Sources []string
- Toggles []models.FeatureToggle
SyncLog []store.SyncLogEntry
Agents []models.Agent
Buckets []models.BucketSummary
@@ -41,7 +37,6 @@ func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
}{
Configs: bySource,
Sources: []string{"trello", "gcal", "gtasks"},
- Toggles: toggles,
SyncLog: syncLog,
Agents: agents,
Buckets: buckets,
@@ -163,52 +158,3 @@ func (h *Handler) HandleToggleSourceConfig(w http.ResponseWriter, r *http.Reques
json.NewEncoder(w).Encode(map[string]bool{"enabled": enabled})
}
-// HandleToggleFeature toggles a feature flag
-func (h *Handler) HandleToggleFeature(w http.ResponseWriter, r *http.Request) {
- name, ok := requireFormValue(w, r, "name")
- if !ok {
- return
- }
- enabled := r.FormValue("enabled") == "true"
-
- 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) {
- name, ok := requireFormValue(w, r, "name")
- if !ok {
- return
- }
- description := r.FormValue("description")
-
- 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)
-}
diff --git a/internal/models/types.go b/internal/models/types.go
index 151ef54..41ba162 100644
--- a/internal/models/types.go
+++ b/internal/models/types.go
@@ -339,10 +339,3 @@ type SourceConfig struct {
Enabled bool `json:"enabled"`
}
-// FeatureToggle represents a feature flag
-type FeatureToggle struct {
- ID int64 `json:"id"`
- Name string `json:"name"`
- Description string `json:"description"`
- Enabled bool `json:"enabled"`
-}
diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go
index d48cb09..2a062ff 100644
--- a/internal/store/sqlite.go
+++ b/internal/store/sqlite.go
@@ -628,67 +628,6 @@ func (s *Store) InvalidateAllCaches() error {
return err
}
-// Feature toggles
-
-// GetFeatureToggles returns all feature toggles
-func (s *Store) GetFeatureToggles() ([]models.FeatureToggle, error) {
- rows, err := s.db.Query(`
- SELECT id, name, description, enabled FROM feature_toggles ORDER BY name
- `)
- if err != nil {
- return nil, err
- }
- defer func() { _ = rows.Close() }()
-
- var toggles []models.FeatureToggle
- for rows.Next() {
- var t models.FeatureToggle
- var desc sql.NullString
- if err := rows.Scan(&t.ID, &t.Name, &desc, &t.Enabled); err != nil {
- return nil, err
- }
- if desc.Valid {
- t.Description = desc.String
- }
- toggles = append(toggles, t)
- }
- return toggles, rows.Err()
-}
-
-// IsFeatureEnabled checks if a feature toggle is enabled
-func (s *Store) IsFeatureEnabled(name string) bool {
- var enabled bool
- err := s.db.QueryRow(`SELECT enabled FROM feature_toggles WHERE name = ?`, name).Scan(&enabled)
- if err != nil {
- return false
- }
- return enabled
-}
-
-// SetFeatureEnabled updates a feature toggle's enabled state
-func (s *Store) SetFeatureEnabled(name string, enabled bool) error {
- _, err := s.db.Exec(`
- UPDATE feature_toggles SET enabled = ?, updated_at = datetime('now', 'localtime')
- WHERE name = ?
- `, enabled, name)
- return err
-}
-
-// CreateFeatureToggle creates a new feature toggle
-func (s *Store) CreateFeatureToggle(name, description string, enabled bool) error {
- _, err := s.db.Exec(`
- INSERT INTO feature_toggles (name, description, enabled)
- VALUES (?, ?, ?)
- `, name, description, enabled)
- return err
-}
-
-// DeleteFeatureToggle removes a feature toggle
-func (s *Store) DeleteFeatureToggle(name string) error {
- _, err := s.db.Exec(`DELETE FROM feature_toggles WHERE name = ?`, name)
- return err
-}
-
// SyncLogEntry represents a single entry in the sync activity log
type SyncLogEntry struct {
ID int64
diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go
index 7f7e56d..c64e6b3 100644
--- a/internal/store/sqlite_test.go
+++ b/internal/store/sqlite_test.go
@@ -642,100 +642,6 @@ func TestGetCardsByDateRange(t *testing.T) {
// =============================================================================
// =============================================================================
-// Feature Toggles Tests
-// =============================================================================
-
-func setupTestStoreWithFeatureToggles(t *testing.T) *Store {
- t.Helper()
-
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
-
- db, err := sql.Open("sqlite3", dbPath)
- if err != nil {
- t.Fatalf("Failed to open test database: %v", err)
- }
-
- db.SetMaxOpenConns(1)
- store := &Store{db: db}
-
- schema := `
- CREATE TABLE IF NOT EXISTS feature_toggles (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT UNIQUE NOT NULL,
- description TEXT,
- enabled BOOLEAN DEFAULT FALSE,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
- `
- if _, err := db.Exec(schema); err != nil {
- t.Fatalf("Failed to create schema: %v", err)
- }
-
- return store
-}
-
-func TestFeatureToggles_CRUD(t *testing.T) {
- store := setupTestStoreWithFeatureToggles(t)
- defer func() { _ = store.Close() }()
-
- // Create feature toggle
- if err := store.CreateFeatureToggle("new_feature", "A new feature", false); err != nil {
- t.Fatalf("Failed to create feature toggle: %v", err)
- }
-
- // Get all toggles
- toggles, err := store.GetFeatureToggles()
- if err != nil {
- t.Fatalf("Failed to get feature toggles: %v", err)
- }
- if len(toggles) != 1 {
- t.Errorf("Expected 1 toggle, got %d", len(toggles))
- }
- if toggles[0].Name != "new_feature" {
- t.Errorf("Expected name 'new_feature', got '%s'", toggles[0].Name)
- }
- if toggles[0].Enabled {
- t.Error("New feature should be disabled")
- }
-
- // Check if enabled
- if store.IsFeatureEnabled("new_feature") {
- t.Error("IsFeatureEnabled should return false for disabled feature")
- }
-
- // Enable feature
- if err := store.SetFeatureEnabled("new_feature", true); err != nil {
- t.Fatalf("Failed to enable feature: %v", err)
- }
-
- if !store.IsFeatureEnabled("new_feature") {
- t.Error("IsFeatureEnabled should return true after enabling")
- }
-
- // Delete feature
- if err := store.DeleteFeatureToggle("new_feature"); err != nil {
- t.Fatalf("Failed to delete feature toggle: %v", err)
- }
-
- toggles, _ = store.GetFeatureToggles()
- if len(toggles) != 0 {
- t.Errorf("Expected 0 toggles after delete, got %d", len(toggles))
- }
-}
-
-func TestIsFeatureEnabled_NonExistent(t *testing.T) {
- store := setupTestStoreWithFeatureToggles(t)
- defer func() { _ = store.Close() }()
-
- // Non-existent feature should return false
- if store.IsFeatureEnabled("does_not_exist") {
- t.Error("Non-existent feature should return false")
- }
-}
-
-// =============================================================================
// Completed Tasks Tests
// =============================================================================
diff --git a/migrations/028_drop_feature_toggles.sql b/migrations/028_drop_feature_toggles.sql
new file mode 100644
index 0000000..dacb8f3
--- /dev/null
+++ b/migrations/028_drop_feature_toggles.sql
@@ -0,0 +1,4 @@
+-- The feature toggle system (012_feature_toggles.sql) never gated any actual
+-- behavior -- nothing in the codebase read a toggle's enabled state outside
+-- of its own CRUD handlers. Dead weight, removed from Settings.
+DROP TABLE IF EXISTS feature_toggles;
diff --git a/web/templates/settings.html b/web/templates/settings.html
index 9c1869f..f07700d 100644
--- a/web/templates/settings.html
+++ b/web/templates/settings.html
@@ -19,50 +19,6 @@
<h1 class="text-4xl font-light text-white mb-2 tracking-tight">Settings</h1>
<p class="text-slate-400 mb-10">Configure feature toggles and data sources.</p>
- <!-- Feature Toggles Section -->
- <section class="mb-12">
- <h2 class="text-xl font-medium text-white mb-6 pb-2 border-b border-white/10">Feature Toggles</h2>
- <div class="grid gap-4" id="toggles-list">
- {{if .Toggles}}
- {{range .Toggles}}
- <div class="card flex items-center gap-4" id="toggle-{{.Name}}">
- <div class="flex-1">
- <strong class="text-white">{{.Name}}</strong>
- {{if .Description}}<div class="text-sm text-slate-400">{{.Description}}</div>{{end}}
- </div>
- <div class="flex items-center gap-6">
- <label class="relative inline-flex items-center cursor-pointer">
- <input type="checkbox" value="" class="sr-only peer"
- {{if .Enabled}}checked{{end}}
- hx-post="/settings/features/toggle"
- hx-vals='{"name": "{{.Name}}", "enabled": "{{if .Enabled}}false{{else}}true{{end}}"}'
- hx-swap="none"
- hx-on::after-request="this.checked = !this.checked; if(event.detail.successful) this.checked = !this.checked;">
- <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
- </label>
- <button class="text-xs text-red-400 hover:text-red-300 transition-colors"
- hx-delete="/settings/features/{{.Name}}"
- hx-target="#toggle-{{.Name}}"
- hx-swap="outerHTML"
- hx-confirm="Delete feature toggle '{{.Name}}'?">
- Delete
- </button>
- </div>
- </div>
- {{end}}
- {{else}}
- <div class="card text-center text-slate-500 py-10">No feature toggles configured.</div>
- {{end}}
- </div>
- <form class="mt-4 flex flex-wrap gap-3 p-4 bg-slate-900/40 rounded-xl border border-white/5" hx-post="/settings/features" hx-target="#toggles-list" hx-swap="beforeend">
- <input type="text" name="name" placeholder="Feature name (snake_case)" required pattern="[a-z_]+"
- class="flex-1 bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm focus:ring-1 focus:ring-blue-500 outline-none">
- <input type="text" name="description" placeholder="Description (optional)"
- class="flex-[2] bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm focus:ring-1 focus:ring-blue-500 outline-none">
- <button type="submit" class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded-lg text-sm font-medium transition-colors">Add Toggle</button>
- </form>
- </section>
-
<!-- Passkeys Section -->
{{if .WebAuthnEnabled}}
<section class="mb-12" id="passkeys-card-section">