summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-20 11:17:19 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-20 11:17:19 -1000
commit07ba815e8517ee2d3a5fa531361bbd09bdfcbaa7 (patch)
treeca9d9be0f02d5a724a3646f87d4a9f50203249cc
parent6a59098c3096f5ebd3a61ef5268cbd480b0f1519 (diff)
Remove Obsidian integration for public server deployment
Obsidian relied on local filesystem access which is incompatible with public server deployment. This removes all Obsidian-related code including: - API client and interface - Store layer methods (SaveNotes, GetNotes, SearchNotes) - Handler methods and routes - UI tab and templates - Configuration fields - Related tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
-rw-r--r--.env.example3
-rw-r--r--cmd/dashboard/main.go9
-rw-r--r--internal/api/interfaces.go6
-rw-r--r--internal/api/obsidian.go221
-rw-r--r--internal/api/obsidian_test.go155
-rw-r--r--internal/config/config.go15
-rw-r--r--internal/handlers/handlers.go114
-rw-r--r--internal/handlers/handlers_test.go35
-rw-r--r--internal/handlers/tab_state_test.go9
-rw-r--r--internal/handlers/tabs.go56
-rw-r--r--internal/handlers/template_test.go40
-rw-r--r--internal/models/atom.go28
-rw-r--r--internal/models/types.go11
-rw-r--r--internal/store/sqlite.go124
-rw-r--r--internal/store/sqlite_test.go327
-rw-r--r--tailwind.config.js1
-rw-r--r--test/acceptance_test.go28
-rw-r--r--web/templates/index.html8
-rw-r--r--web/templates/partials/notes-tab.html36
-rw-r--r--web/templates/partials/obsidian-notes.html30
20 files changed, 37 insertions, 1219 deletions
diff --git a/.env.example b/.env.example
index dddb927..e54a20b 100644
--- a/.env.example
+++ b/.env.example
@@ -17,9 +17,6 @@ TRELLO_TOKEN=
# PLANTOEAT_API_KEY=
# Paths
-# Absolute path to your Obsidian vault directory
-OBSIDIAN_VAULT_PATH=/path/to/your/obsidian/vault
-
# Database file location (relative or absolute path)
DATABASE_PATH=./dashboard.db
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index e52aeb0..6e013e8 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -40,18 +40,13 @@ func main() {
todoistClient := api.NewTodoistClient(cfg.TodoistAPIKey)
trelloClient := api.NewTrelloClient(cfg.TrelloAPIKey, cfg.TrelloToken)
- var obsidianClient api.ObsidianAPI
- if cfg.HasObsidian() {
- obsidianClient = api.NewObsidianClient(cfg.ObsidianVaultPath)
- }
-
var planToEatClient api.PlanToEatAPI
if cfg.HasPlanToEat() {
planToEatClient = api.NewPlanToEatClient(cfg.PlanToEatAPIKey)
}
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, obsidianClient, planToEatClient, cfg)
+ h := handlers.New(db, todoistClient, trelloClient, planToEatClient, cfg)
tabsHandler := handlers.NewTabsHandler(db, cfg.TemplateDir)
// Set up router
@@ -66,14 +61,12 @@ func main() {
r.Get("/", h.HandleDashboard)
r.Post("/api/refresh", h.HandleRefresh)
r.Get("/api/tasks", h.HandleGetTasks)
- r.Get("/api/notes", h.HandleGetNotes)
r.Get("/api/meals", h.HandleGetMeals)
r.Get("/api/boards", h.HandleGetBoards)
// Tab routes for HTMX (using new TabsHandler)
r.Get("/tabs/tasks", tabsHandler.HandleTasks)
r.Get("/tabs/planning", tabsHandler.HandlePlanning)
- r.Get("/tabs/notes", tabsHandler.HandleNotes)
r.Get("/tabs/meals", tabsHandler.HandleMeals)
r.Post("/tabs/refresh", h.HandleRefreshTab)
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go
index 2419707..33bef59 100644
--- a/internal/api/interfaces.go
+++ b/internal/api/interfaces.go
@@ -26,11 +26,6 @@ type TrelloAPI interface {
UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error
}
-// ObsidianAPI defines the interface for Obsidian operations
-type ObsidianAPI interface {
- GetNotes(ctx context.Context, limit int) ([]models.Note, error)
-}
-
// PlanToEatAPI defines the interface for PlanToEat operations
type PlanToEatAPI interface {
GetUpcomingMeals(ctx context.Context, days int) ([]models.Meal, error)
@@ -42,6 +37,5 @@ type PlanToEatAPI interface {
var (
_ TodoistAPI = (*TodoistClient)(nil)
_ TrelloAPI = (*TrelloClient)(nil)
- _ ObsidianAPI = (*ObsidianClient)(nil)
_ PlanToEatAPI = (*PlanToEatClient)(nil)
)
diff --git a/internal/api/obsidian.go b/internal/api/obsidian.go
deleted file mode 100644
index 413fdd3..0000000
--- a/internal/api/obsidian.go
+++ /dev/null
@@ -1,221 +0,0 @@
-package api
-
-import (
- "bufio"
- "context"
- "fmt"
- "os"
- "path/filepath"
- "regexp"
- "sort"
- "strings"
- "time"
-
- "task-dashboard/internal/models"
-)
-
-// ObsidianClient handles reading notes from an Obsidian vault
-type ObsidianClient struct {
- vaultPath string
-}
-
-// NewObsidianClient creates a new Obsidian vault reader
-func NewObsidianClient(vaultPath string) *ObsidianClient {
- return &ObsidianClient{
- vaultPath: vaultPath,
- }
-}
-
-// fileInfo holds file metadata for sorting
-type fileInfo struct {
- path string
- modTime time.Time
-}
-
-// GetNotes reads and returns the most recently modified notes from the vault
-func (c *ObsidianClient) GetNotes(ctx context.Context, limit int) ([]models.Note, error) {
- if c.vaultPath == "" {
- return nil, fmt.Errorf("obsidian vault path not configured")
- }
-
- // Check if vault path exists
- if _, err := os.Stat(c.vaultPath); os.IsNotExist(err) {
- return nil, fmt.Errorf("vault path does not exist: %s", c.vaultPath)
- }
-
- // Collect all markdown files with their modification times
- var files []fileInfo
-
- err := filepath.Walk(c.vaultPath, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil // Skip files we can't access
- }
-
- // Skip symbolic links to prevent path traversal
- if info.Mode()&os.ModeSymlink != 0 {
- return nil
- }
-
- // Skip directories and non-markdown files
- if info.IsDir() || !strings.HasSuffix(info.Name(), ".md") {
- return nil
- }
-
- // Skip hidden files and directories
- if strings.HasPrefix(info.Name(), ".") {
- return nil
- }
-
- files = append(files, fileInfo{
- path: path,
- modTime: info.ModTime(),
- })
-
- return nil
- })
-
- if err != nil {
- return nil, fmt.Errorf("failed to walk vault directory: %w", err)
- }
-
- // Sort by modification time (most recent first)
- sort.Slice(files, func(i, j int) bool {
- return files[i].modTime.After(files[j].modTime)
- })
-
- // Limit the number of files to process
- if limit > 0 && len(files) > limit {
- files = files[:limit]
- }
-
- // Parse each file
- notes := make([]models.Note, 0, len(files))
- for _, file := range files {
- note, err := c.parseMarkdownFile(file.path, file.modTime)
- if err != nil {
- // Skip files that fail to parse
- continue
- }
- notes = append(notes, *note)
- }
-
- return notes, nil
-}
-
-// parseMarkdownFile reads and parses a markdown file
-func (c *ObsidianClient) parseMarkdownFile(path string, modTime time.Time) (*models.Note, error) {
- file, err := os.Open(path)
- if err != nil {
- return nil, err
- }
- defer file.Close()
-
- scanner := bufio.NewScanner(file)
-
- var content strings.Builder
- var tags []string
- inFrontmatter := false
- lineCount := 0
-
- // Parse file
- for scanner.Scan() {
- line := scanner.Text()
- lineCount++
-
- // Check for YAML frontmatter
- if lineCount == 1 && line == "---" {
- inFrontmatter = true
- continue
- }
-
- if inFrontmatter {
- if line == "---" {
- inFrontmatter = false
- continue
- }
- // Extract tags from frontmatter
- if strings.HasPrefix(line, "tags:") {
- tagsStr := strings.TrimPrefix(line, "tags:")
- tagsStr = strings.Trim(tagsStr, " []")
- if tagsStr != "" {
- tags = strings.Split(tagsStr, ",")
- for i, tag := range tags {
- tags[i] = strings.TrimSpace(tag)
- }
- }
- }
- continue
- }
-
- // Add to content (limit to preview)
- if content.Len() < 500 { // Limit to ~500 chars
- content.WriteString(line)
- content.WriteString("\n")
- }
- }
-
- if err := scanner.Err(); err != nil {
- return nil, err
- }
-
- // Extract inline tags (e.g., #tag)
- inlineTags := extractInlineTags(content.String())
- tags = append(tags, inlineTags...)
- tags = uniqueStrings(tags)
-
- // Get filename and title
- filename := filepath.Base(path)
- title := strings.TrimSuffix(filename, ".md")
-
- // Try to extract title from first H1 heading
- contentStr := content.String()
- h1Regex := regexp.MustCompile(`^#\s+(.+)$`)
- lines := strings.Split(contentStr, "\n")
- for _, line := range lines {
- if matches := h1Regex.FindStringSubmatch(line); len(matches) > 1 {
- title = matches[1]
- break
- }
- }
-
- note := &models.Note{
- Filename: filename,
- Title: title,
- Content: strings.TrimSpace(contentStr),
- Modified: modTime,
- Path: path,
- Tags: tags,
- }
-
- return note, nil
-}
-
-// extractInlineTags finds all #tags in the content
-func extractInlineTags(content string) []string {
- tagRegex := regexp.MustCompile(`#([a-zA-Z0-9_-]+)`)
- matches := tagRegex.FindAllStringSubmatch(content, -1)
-
- tags := make([]string, 0, len(matches))
- for _, match := range matches {
- if len(match) > 1 {
- tags = append(tags, match[1])
- }
- }
-
- return tags
-}
-
-// uniqueStrings returns a slice with duplicate strings removed
-func uniqueStrings(slice []string) []string {
- seen := make(map[string]bool)
- result := make([]string, 0, len(slice))
-
- for _, item := range slice {
- if !seen[item] && item != "" {
- seen[item] = true
- result = append(result, item)
- }
- }
-
- return result
-}
diff --git a/internal/api/obsidian_test.go b/internal/api/obsidian_test.go
deleted file mode 100644
index 3509594..0000000
--- a/internal/api/obsidian_test.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package api
-
-import (
- "context"
- "os"
- "path/filepath"
- "testing"
- "time"
-)
-
-// TestGetNotes_SymlinkSecurity verifies that GetNotes does not follow symlinks
-// to prevent path traversal attacks
-func TestGetNotes_SymlinkSecurity(t *testing.T) {
- // Create temporary directories
- tempDir := t.TempDir()
- vaultDir := filepath.Join(tempDir, "vault")
- outsideDir := filepath.Join(tempDir, "outside")
-
- // Create vault and outside directories
- if err := os.Mkdir(vaultDir, 0755); err != nil {
- t.Fatalf("Failed to create vault directory: %v", err)
- }
- if err := os.Mkdir(outsideDir, 0755); err != nil {
- t.Fatalf("Failed to create outside directory: %v", err)
- }
-
- // Create a valid markdown file inside the vault
- validFile := filepath.Join(vaultDir, "valid-note.md")
- validContent := "# Valid Note\n\nThis is a valid note inside the vault."
- if err := os.WriteFile(validFile, []byte(validContent), 0644); err != nil {
- t.Fatalf("Failed to create valid markdown file: %v", err)
- }
-
- // Create a secret file outside the vault
- secretFile := filepath.Join(outsideDir, "secret.txt")
- secretContent := "This is a secret file outside the vault that should not be accessible."
- if err := os.WriteFile(secretFile, []byte(secretContent), 0644); err != nil {
- t.Fatalf("Failed to create secret file: %v", err)
- }
-
- // Create a symlink inside the vault pointing to the secret file
- symlinkPath := filepath.Join(vaultDir, "symlink-note.md")
- if err := os.Symlink(secretFile, symlinkPath); err != nil {
- t.Skipf("Skipping test: unable to create symlink (may not be supported on this system): %v", err)
- }
-
- // Initialize Obsidian client
- client := NewObsidianClient(vaultDir)
-
- // Call GetNotes
- ctx := context.Background()
- notes, err := client.GetNotes(ctx, 10)
- if err != nil {
- t.Fatalf("GetNotes returned error: %v", err)
- }
-
- // Verify that only the valid note is returned
- if len(notes) != 1 {
- t.Errorf("Expected 1 note, got %d notes", len(notes))
- for i, note := range notes {
- t.Logf("Note %d: %s (path: %s)", i, note.Filename, note.Path)
- }
- t.Fatalf("Test failed: symlink was followed or wrong number of notes returned")
- }
-
- // Verify the returned note is the valid one
- note := notes[0]
- if note.Filename != "valid-note.md" {
- t.Errorf("Expected filename 'valid-note.md', got '%s'", note.Filename)
- }
- if note.Title != "Valid Note" {
- t.Errorf("Expected title 'Valid Note', got '%s'", note.Title)
- }
-
- // Ensure the content does not contain the secret text
- if containsString(note.Content, "secret") {
- t.Errorf("Note content contains 'secret', which suggests symlink was followed: %s", note.Content)
- }
-}
-
-// TestGetNotes_BasicFunctionality tests basic GetNotes functionality
-func TestGetNotes_BasicFunctionality(t *testing.T) {
- // Create temporary vault directory
- vaultDir := t.TempDir()
-
- // Create multiple markdown files with different modification times
- files := []struct {
- name string
- content string
- delay time.Duration
- }{
- {"oldest.md", "# Oldest Note\n\nThis is the oldest note.", 0},
- {"middle.md", "# Middle Note\n\nThis is a middle note.", 10 * time.Millisecond},
- {"newest.md", "# Newest Note\n\nThis is the newest note.", 20 * time.Millisecond},
- }
-
- for _, file := range files {
- time.Sleep(file.delay)
- path := filepath.Join(vaultDir, file.name)
- if err := os.WriteFile(path, []byte(file.content), 0644); err != nil {
- t.Fatalf("Failed to create file %s: %v", file.name, err)
- }
- }
-
- // Initialize Obsidian client
- client := NewObsidianClient(vaultDir)
-
- // Test with limit
- ctx := context.Background()
- notes, err := client.GetNotes(ctx, 2)
- if err != nil {
- t.Fatalf("GetNotes returned error: %v", err)
- }
-
- // Should return 2 most recent notes
- if len(notes) != 2 {
- t.Errorf("Expected 2 notes with limit, got %d", len(notes))
- }
-
- // Verify order (newest first)
- if len(notes) >= 2 {
- if notes[0].Filename != "newest.md" {
- t.Errorf("Expected first note to be 'newest.md', got '%s'", notes[0].Filename)
- }
- if notes[1].Filename != "middle.md" {
- t.Errorf("Expected second note to be 'middle.md', got '%s'", notes[1].Filename)
- }
- }
-
- // Test without limit
- allNotes, err := client.GetNotes(ctx, 0)
- if err != nil {
- t.Fatalf("GetNotes without limit returned error: %v", err)
- }
-
- if len(allNotes) != 3 {
- t.Errorf("Expected 3 notes without limit, got %d", len(allNotes))
- }
-}
-
-// containsString checks if haystack contains needle (case-insensitive)
-func containsString(haystack, needle string) bool {
- return len(haystack) > 0 && len(needle) > 0 &&
- (haystack == needle || len(haystack) >= len(needle) &&
- hasSubstring(haystack, needle))
-}
-
-func hasSubstring(s, substr string) bool {
- for i := 0; i <= len(s)-len(substr); i++ {
- if s[i:i+len(substr)] == substr {
- return true
- }
- }
- return false
-}
diff --git a/internal/config/config.go b/internal/config/config.go
index 93f57cc..b3bc43d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -15,9 +15,8 @@ type Config struct {
TrelloToken string
// Paths
- ObsidianVaultPath string
- DatabasePath string
- TemplateDir string
+ DatabasePath string
+ TemplateDir string
// Server
Port string
@@ -35,9 +34,8 @@ func Load() (*Config, error) {
TrelloToken: os.Getenv("TRELLO_TOKEN"),
// Paths
- ObsidianVaultPath: os.Getenv("OBSIDIAN_VAULT_PATH"),
- DatabasePath: getEnvWithDefault("DATABASE_PATH", "./dashboard.db"),
- TemplateDir: getEnvWithDefault("TEMPLATE_DIR", "web/templates"),
+ DatabasePath: getEnvWithDefault("DATABASE_PATH", "./dashboard.db"),
+ TemplateDir: getEnvWithDefault("TEMPLATE_DIR", "web/templates"),
// Server
Port: getEnvWithDefault("PORT", "8080"),
@@ -81,11 +79,6 @@ func (c *Config) HasTrello() bool {
return c.TrelloAPIKey != "" && c.TrelloToken != ""
}
-// HasObsidian checks if Obsidian is configured
-func (c *Config) HasObsidian() bool {
- return c.ObsidianVaultPath != ""
-}
-
// getEnvWithDefault returns environment variable value or default if not set
func getEnvWithDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index f53eced..7bb84b9 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -24,14 +24,13 @@ type Handler struct {
store *store.Store
todoistClient api.TodoistAPI
trelloClient api.TrelloAPI
- obsidianClient api.ObsidianAPI
planToEatClient api.PlanToEatAPI
config *config.Config
templates *template.Template
}
// New creates a new Handler instance
-func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, obsidian api.ObsidianAPI, planToEat api.PlanToEatAPI, cfg *config.Config) *Handler {
+func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, cfg *config.Config) *Handler {
// Parse templates including partials
tmpl, err := template.ParseGlob(filepath.Join(cfg.TemplateDir, "*.html"))
if err != nil {
@@ -48,7 +47,6 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, obsidian
store: s,
todoistClient: todoist,
trelloClient: trello,
- obsidianClient: obsidian,
planToEatClient: planToEat,
config: cfg,
templates: tmpl,
@@ -122,18 +120,6 @@ func (h *Handler) HandleGetTasks(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(tasks)
}
-// HandleGetNotes returns notes as JSON
-func (h *Handler) HandleGetNotes(w http.ResponseWriter, r *http.Request) {
- notes, err := h.store.GetNotes(20)
- if err != nil {
- http.Error(w, "Failed to get notes", http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(notes)
-}
-
// HandleGetMeals returns meals as JSON
func (h *Handler) HandleGetMeals(w http.ResponseWriter, r *http.Request) {
startDate := time.Now()
@@ -178,27 +164,9 @@ func (h *Handler) HandleTasksTab(w http.ResponseWriter, r *http.Request) {
}
}
-// HandleNotesTab renders the notes tab content (Obsidian)
-func (h *Handler) HandleNotesTab(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
-
- data, err := h.aggregateData(ctx, false)
- if err != nil {
- http.Error(w, "Failed to load notes", http.StatusInternalServerError)
- log.Printf("Error loading notes tab: %v", err)
- return
- }
-
- if err := h.templates.ExecuteTemplate(w, "notes-tab", data); err != nil {
- http.Error(w, "Failed to render template", http.StatusInternalServerError)
- log.Printf("Error rendering notes tab: %v", err)
- }
-}
-
// HandleRefreshTab refreshes and re-renders the specified tab
func (h *Handler) HandleRefreshTab(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- tab := r.URL.Query().Get("tab") // "tasks" or "notes"
// Force refresh
data, err := h.aggregateData(ctx, true)
@@ -208,13 +176,7 @@ func (h *Handler) HandleRefreshTab(w http.ResponseWriter, r *http.Request) {
return
}
- // Determine template to render
- templateName := "tasks-tab"
- if tab == "notes" {
- templateName = "notes-tab"
- }
-
- if err := h.templates.ExecuteTemplate(w, templateName, data); err != nil {
+ if err := h.templates.ExecuteTemplate(w, "tasks-tab", data); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
log.Printf("Error rendering refreshed tab: %v", err)
}
@@ -254,6 +216,26 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models
if err != nil {
data.Errors = append(data.Errors, "Todoist: "+err.Error())
} else {
+ // Sort tasks: earliest due date first, nil last, then by priority (descending)
+ sort.Slice(tasks, func(i, j int) bool {
+ // Handle nil due dates (push to end)
+ if tasks[i].DueDate == nil && tasks[j].DueDate != nil {
+ return false
+ }
+ if tasks[i].DueDate != nil && tasks[j].DueDate == nil {
+ return true
+ }
+
+ // Both have due dates, sort by date
+ if tasks[i].DueDate != nil && tasks[j].DueDate != nil {
+ if !tasks[i].DueDate.Equal(*tasks[j].DueDate) {
+ return tasks[i].DueDate.Before(*tasks[j].DueDate)
+ }
+ }
+
+ // Same due date (or both nil), sort by priority (descending)
+ return tasks[i].Priority > tasks[j].Priority
+ })
data.Tasks = tasks
}
}()
@@ -272,22 +254,6 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models
}
}()
- // Fetch Obsidian notes (if configured)
- if h.obsidianClient != nil {
- wg.Add(1)
- go func() {
- defer wg.Done()
- notes, err := h.fetchNotes(ctx, forceRefresh)
- mu.Lock()
- defer mu.Unlock()
- if err != nil {
- data.Errors = append(data.Errors, "Obsidian: "+err.Error())
- } else {
- data.Notes = notes
- }
- }()
- }
-
// Fetch PlanToEat meals (if configured)
if h.planToEatClient != nil {
wg.Add(1)
@@ -467,42 +433,6 @@ func (h *Handler) convertSyncItemToTask(item api.SyncItemResponse, projectMap ma
return task
}
-// fetchNotes fetches notes from cache or filesystem
-func (h *Handler) fetchNotes(ctx context.Context, forceRefresh bool) ([]models.Note, error) {
- cacheKey := store.CacheKeyObsidianNotes
-
- // Check cache validity
- if !forceRefresh {
- valid, err := h.store.IsCacheValid(cacheKey)
- if err == nil && valid {
- return h.store.GetNotes(20)
- }
- }
-
- // Fetch from filesystem
- notes, err := h.obsidianClient.GetNotes(ctx, 20)
- if err != nil {
- // Try to return cached data even if stale
- cachedNotes, cacheErr := h.store.GetNotes(20)
- if cacheErr == nil && len(cachedNotes) > 0 {
- return cachedNotes, nil
- }
- return nil, err
- }
-
- // Save to cache
- if err := h.store.SaveNotes(notes); err != nil {
- log.Printf("Failed to save notes to cache: %v", err)
- }
-
- // Update cache metadata
- if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil {
- log.Printf("Failed to update cache metadata: %v", err)
- }
-
- return notes, nil
-}
-
// fetchMeals fetches meals from cache or API
func (h *Handler) fetchMeals(ctx context.Context, forceRefresh bool) ([]models.Meal, error) {
cacheKey := store.CacheKeyPlanToEatMeals
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index 1aa72cc..6e9346a 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -353,41 +353,6 @@ func TestHandleRefresh(t *testing.T) {
// The important thing is the handler doesn't error
}
-// TestHandleGetNotes tests the HandleGetNotes handler
-func TestHandleGetNotes(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- // Test with nil client should return empty array
- cfg := &config.Config{
- CacheTTLMinutes: 5,
- }
- h := &Handler{
- store: db,
- obsidianClient: nil,
- config: cfg,
- }
-
- req := httptest.NewRequest("GET", "/api/notes", nil)
- w := httptest.NewRecorder()
-
- h.HandleGetNotes(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("Expected status 200, got %d", w.Code)
- }
-
- var notes []models.Note
- if err := json.NewDecoder(w.Body).Decode(&notes); err != nil {
- t.Fatalf("Failed to decode response: %v", err)
- }
-
- // Handler returns empty array when client is nil
- if len(notes) != 0 {
- t.Errorf("Expected 0 notes when client is nil, got %d", len(notes))
- }
-}
-
// TestHandleGetMeals tests the HandleGetMeals handler
func TestHandleGetMeals(t *testing.T) {
db, cleanup := setupTestDB(t)
diff --git a/internal/handlers/tab_state_test.go b/internal/handlers/tab_state_test.go
index d3f0fce..a4f6d23 100644
--- a/internal/handlers/tab_state_test.go
+++ b/internal/handlers/tab_state_test.go
@@ -30,7 +30,7 @@ func TestHandleDashboard_TabState(t *testing.T) {
}
// Create handler
- h := New(db, todoistClient, trelloClient, nil, nil, cfg)
+ h := New(db, todoistClient, trelloClient, nil, cfg)
// Skip if templates are not loaded (test environment issue)
if h.templates == nil {
@@ -52,13 +52,6 @@ func TestHandleDashboard_TabState(t *testing.T) {
expectedHxGet: `hx-get="/tabs/tasks"`,
},
{
- name: "notes tab from query param",
- url: "/?tab=notes",
- expectedTab: "notes",
- expectedActive: `class="tab-button tab-button-active"`,
- expectedHxGet: `hx-get="/tabs/notes"`,
- },
- {
name: "planning tab from query param",
url: "/?tab=planning",
expectedTab: "planning",
diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go
index 1afdd06..7e0b352 100644
--- a/internal/handlers/tabs.go
+++ b/internal/handlers/tabs.go
@@ -126,7 +126,7 @@ func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) {
}
}
-// HandlePlanning renders the Planning tab (Trello boards + Todoist tasks)
+// HandlePlanning renders the Planning tab (Trello boards)
func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) {
// Fetch Trello boards
boards, err := h.store.GetBoards()
@@ -136,21 +136,12 @@ func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) {
return
}
- // Fetch Todoist tasks
- tasks, err := h.store.GetTasks()
- if err != nil {
- log.Printf("Error fetching tasks: %v", err)
- tasks = []models.Task{}
- }
-
data := struct {
Boards []models.Board
- Tasks []models.Task
Projects []models.Project
}{
Boards: boards,
- Tasks: tasks,
- Projects: []models.Project{}, // Empty for now - form won't display but checkboxes will work
+ Projects: []models.Project{}, // Empty for now
}
if err := h.templates.ExecuteTemplate(w, "planning-tab", data); err != nil {
@@ -159,49 +150,6 @@ func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) {
}
}
-// HandleNotes renders the Notes tab (Obsidian notes)
-func (h *TabsHandler) HandleNotes(w http.ResponseWriter, r *http.Request) {
- // Check for search query parameter
- query := r.URL.Query().Get("q")
-
- var notes []models.Note
- var err error
-
- // If search query is present, search notes; otherwise, get all notes
- if query != "" {
- notes, err = h.store.SearchNotes(query)
- } else {
- notes, err = h.store.GetNotes(20)
- }
-
- if err != nil {
- http.Error(w, "Failed to fetch notes", http.StatusInternalServerError)
- log.Printf("Error fetching notes: %v", err)
- return
- }
-
- data := struct {
- Notes []models.Note
- Errors []string
- }{
- Notes: notes,
- Errors: nil,
- }
-
- // Check HX-Target header for partial update
- hxTarget := r.Header.Get("HX-Target")
- templateName := "notes-tab"
- if hxTarget == "notes-results" {
- // Render only the notes list for HTMX partial update
- templateName = "obsidian-notes"
- }
-
- if err := h.templates.ExecuteTemplate(w, templateName, data); err != nil {
- http.Error(w, "Failed to render template", http.StatusInternalServerError)
- log.Printf("Error rendering notes tab: %v", err)
- }
-}
-
// HandleMeals renders the Meals tab (PlanToEat)
func (h *TabsHandler) HandleMeals(w http.ResponseWriter, r *http.Request) {
// Fetch meals for next 7 days
diff --git a/internal/handlers/template_test.go b/internal/handlers/template_test.go
deleted file mode 100644
index b0b2378..0000000
--- a/internal/handlers/template_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package handlers_test
-
-import (
- "html/template"
- "io"
- "testing"
- "task-dashboard/internal/models"
-)
-
-func TestNotesTemplateRendering(t *testing.T) {
- // Parse templates (adjust paths relative to where test runs, usually package root)
- // Since we run 'go test ./...', paths might need to be absolute or relative to project root if we use a helper.
- // But standard 'go test' in a subdir uses that subdir as CWD.
- // We will assume the test runs from 'internal/handlers'.
- // So paths are "../../web/templates/..."
-
- tmpl, err := template.ParseGlob("../../web/templates/*.html")
- if err != nil {
- t.Fatalf("Failed to parse templates: %v", err)
- }
- tmpl, err = tmpl.ParseGlob("../../web/templates/partials/*.html")
- if err != nil {
- t.Fatalf("Failed to parse partials: %v", err)
- }
-
- // Define the data structure we EXPECT to use (with Errors)
- data := struct {
- Notes []models.Note
- Errors []string
- }{
- Notes: []models.Note{},
- Errors: []string{"Test Error"},
- }
-
- // Execute
- err = tmpl.ExecuteTemplate(io.Discard, "notes-tab", data)
- if err != nil {
- t.Errorf("Failed to render notes-tab with corrected data: %v", err)
- }
-}
diff --git a/internal/models/atom.go b/internal/models/atom.go
index 47695d9..fe40962 100644
--- a/internal/models/atom.go
+++ b/internal/models/atom.go
@@ -5,10 +5,9 @@ import "time"
type AtomSource string
const (
- SourceTrello AtomSource = "trello"
- SourceTodoist AtomSource = "todoist"
- SourceObsidian AtomSource = "obsidian"
- SourceMeal AtomSource = "plantoeat"
+ SourceTrello AtomSource = "trello"
+ SourceTodoist AtomSource = "todoist"
+ SourceMeal AtomSource = "plantoeat"
)
type AtomType string
@@ -92,27 +91,6 @@ func CardToAtom(c Card) Atom {
}
}
-// NoteToAtom converts an Obsidian Note to an Atom
-func NoteToAtom(n Note) Atom {
- // Notes don't have priority, default to low (1)
- priority := 1
-
- return Atom{
- ID: n.Path, // Use path as unique ID
- Title: n.Title,
- Description: n.Content,
- Source: SourceObsidian,
- Type: TypeNote,
- URL: "", // Obsidian notes don't have URLs
- DueDate: nil, // Notes typically don't have due dates
- CreatedAt: n.Modified, // Use modified time as created time
- Priority: priority,
- SourceIcon: "📝", // Memo emoji for notes
- ColorClass: "border-purple-500",
- Raw: n,
- }
-}
-
// MealToAtom converts a PlanToEat Meal to an Atom
func MealToAtom(m Meal) Atom {
// Meals don't have priority, default to low (1)
diff --git a/internal/models/types.go b/internal/models/types.go
index fab732f..d9e955b 100644
--- a/internal/models/types.go
+++ b/internal/models/types.go
@@ -17,16 +17,6 @@ type Task struct {
CreatedAt time.Time `json:"created_at"`
}
-// Note represents a note from Obsidian
-type Note struct {
- Filename string `json:"filename"`
- Title string `json:"title"`
- Content string `json:"content"` // First 200 chars or full content
- Modified time.Time `json:"modified"`
- Path string `json:"path"`
- Tags []string `json:"tags"`
-}
-
// Meal represents a meal from PlanToEat
type Meal struct {
ID string `json:"id"`
@@ -83,7 +73,6 @@ func (cm *CacheMetadata) IsCacheValid() bool {
// DashboardData aggregates all data for the main view
type DashboardData struct {
Tasks []Task `json:"tasks"`
- Notes []Note `json:"notes"`
Meals []Meal `json:"meals"`
Boards []Board `json:"boards,omitempty"`
TrelloTasks []Card `json:"trello_tasks,omitempty"`
diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go
index b8d0c97..dac3321 100644
--- a/internal/store/sqlite.go
+++ b/internal/store/sqlite.go
@@ -15,9 +15,8 @@ import (
// Cache key constants
const (
- CacheKeyTodoistTasks = "todoist_tasks"
- CacheKeyTrelloBoards = "trello_boards"
- CacheKeyObsidianNotes = "obsidian_notes"
+ CacheKeyTodoistTasks = "todoist_tasks"
+ CacheKeyTrelloBoards = "trello_boards"
CacheKeyPlanToEatMeals = "plantoeat_meals"
)
@@ -234,125 +233,6 @@ func (s *Store) DeleteTasksByIDs(ids []string) error {
return tx.Commit()
}
-// Notes operations
-
-// SaveNotes saves multiple notes to the database
-func (s *Store) SaveNotes(notes []models.Note) error {
- tx, err := s.db.Begin()
- if err != nil {
- return err
- }
- defer tx.Rollback()
-
- stmt, err := tx.Prepare(`
- INSERT OR REPLACE INTO notes
- (filename, title, content, modified, path, tags, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
- `)
- if err != nil {
- return err
- }
- defer stmt.Close()
-
- for _, note := range notes {
- tagsJSON, _ := json.Marshal(note.Tags)
- _, err := stmt.Exec(
- note.Filename,
- note.Title,
- note.Content,
- note.Modified,
- note.Path,
- string(tagsJSON),
- )
- if err != nil {
- return err
- }
- }
-
- return tx.Commit()
-}
-
-// GetNotes retrieves all notes from the database
-func (s *Store) GetNotes(limit int) ([]models.Note, error) {
- query := `
- SELECT filename, title, content, modified, path, tags
- FROM notes
- ORDER BY modified DESC
- `
- var args []interface{}
- if limit > 0 {
- query += " LIMIT ?"
- args = append(args, limit)
- }
-
- rows, err := s.db.Query(query, args...)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var notes []models.Note
- for rows.Next() {
- var note models.Note
- var tagsJSON string
-
- err := rows.Scan(
- &note.Filename,
- &note.Title,
- &note.Content,
- &note.Modified,
- &note.Path,
- &tagsJSON,
- )
- if err != nil {
- return nil, err
- }
-
- json.Unmarshal([]byte(tagsJSON), &note.Tags)
- notes = append(notes, note)
- }
-
- return notes, rows.Err()
-}
-
-// SearchNotes searches notes by title or content
-func (s *Store) SearchNotes(query string) ([]models.Note, error) {
- searchPattern := "%" + query + "%"
- rows, err := s.db.Query(`
- SELECT filename, title, content, modified, path, tags
- FROM notes
- WHERE title LIKE ? OR content LIKE ?
- ORDER BY modified DESC
- `, searchPattern, searchPattern)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var notes []models.Note
- for rows.Next() {
- var note models.Note
- var tagsJSON string
-
- err := rows.Scan(
- &note.Filename,
- &note.Title,
- &note.Content,
- &note.Modified,
- &note.Path,
- &tagsJSON,
- )
- if err != nil {
- return nil, err
- }
-
- json.Unmarshal([]byte(tagsJSON), &note.Tags)
- notes = append(notes, note)
- }
-
- return notes, rows.Err()
-}
-
// Meals operations
// SaveMeals saves multiple meals to the database
diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go
index 962a1bf..6bf7783 100644
--- a/internal/store/sqlite_test.go
+++ b/internal/store/sqlite_test.go
@@ -2,7 +2,6 @@ package store
import (
"database/sql"
- "fmt"
"path/filepath"
"testing"
"time"
@@ -11,332 +10,6 @@ import (
"task-dashboard/internal/models"
)
-// setupTestStore creates a test store with schema but without migrations directory
-func setupTestStore(t *testing.T) *Store {
- t.Helper()
-
- // Create temporary database file
- 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)
- }
-
- // Enable foreign keys
- if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
- t.Fatalf("Failed to enable foreign keys: %v", err)
- }
-
- // Enable WAL mode for better concurrency
- if _, err := db.Exec("PRAGMA journal_mode = WAL"); err != nil {
- t.Fatalf("Failed to enable WAL mode: %v", err)
- }
-
- // Serialize writes to prevent "database is locked" errors
- db.SetMaxOpenConns(1)
-
- store := &Store{db: db}
-
- // Create notes table directly (without migrations)
- schema := `
- CREATE TABLE IF NOT EXISTS notes (
- filename TEXT PRIMARY KEY,
- title TEXT NOT NULL,
- content TEXT,
- modified DATETIME NOT NULL,
- path TEXT NOT NULL,
- tags TEXT,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
- CREATE INDEX IF NOT EXISTS idx_notes_modified ON notes(modified DESC);
- `
- if _, err := db.Exec(schema); err != nil {
- t.Fatalf("Failed to create schema: %v", err)
- }
-
- return store
-}
-
-// TestGetNotes_LimitClause verifies that the LIMIT clause works correctly
-// and prevents SQL injection
-func TestGetNotes_LimitClause(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- // Create 3 distinct notes with different modification times
- baseTime := time.Now()
- notes := []models.Note{
- {
- Filename: "note1.md",
- Title: "Note 1",
- Content: "Content of note 1",
- Modified: baseTime.Add(-2 * time.Hour),
- Path: "/vault/note1.md",
- Tags: []string{"tag1"},
- },
- {
- Filename: "note2.md",
- Title: "Note 2",
- Content: "Content of note 2",
- Modified: baseTime.Add(-1 * time.Hour),
- Path: "/vault/note2.md",
- Tags: []string{"tag2"},
- },
- {
- Filename: "note3.md",
- Title: "Note 3",
- Content: "Content of note 3",
- Modified: baseTime,
- Path: "/vault/note3.md",
- Tags: []string{"tag3"},
- },
- }
-
- // Save all 3 notes
- if err := store.SaveNotes(notes); err != nil {
- t.Fatalf("Failed to save notes: %v", err)
- }
-
- // Test 1: Call GetNotes(2) - should return exactly 2 notes
- t.Run("GetNotes with limit 2", func(t *testing.T) {
- result, err := store.GetNotes(2)
- if err != nil {
- t.Fatalf("GetNotes(2) returned error: %v", err)
- }
-
- if len(result) != 2 {
- t.Errorf("Expected exactly 2 notes, got %d", len(result))
- }
-
- // Verify they are the most recent notes (note3 and note2)
- if len(result) >= 2 {
- if result[0].Filename != "note3.md" {
- t.Errorf("Expected first note to be 'note3.md', got '%s'", result[0].Filename)
- }
- if result[1].Filename != "note2.md" {
- t.Errorf("Expected second note to be 'note2.md', got '%s'", result[1].Filename)
- }
- }
- })
-
- // Test 2: Call GetNotes(5) - should return all 3 notes
- t.Run("GetNotes with limit 5", func(t *testing.T) {
- result, err := store.GetNotes(5)
- if err != nil {
- t.Fatalf("GetNotes(5) returned error: %v", err)
- }
-
- if len(result) != 3 {
- t.Errorf("Expected all 3 notes, got %d", len(result))
- }
-
- // Verify order (most recent first)
- if len(result) == 3 {
- if result[0].Filename != "note3.md" {
- t.Errorf("Expected first note to be 'note3.md', got '%s'", result[0].Filename)
- }
- if result[1].Filename != "note2.md" {
- t.Errorf("Expected second note to be 'note2.md', got '%s'", result[1].Filename)
- }
- if result[2].Filename != "note1.md" {
- t.Errorf("Expected third note to be 'note1.md', got '%s'", result[2].Filename)
- }
- }
- })
-
- // Test 3: Call GetNotes(0) - should return all notes (no limit)
- t.Run("GetNotes with no limit", func(t *testing.T) {
- result, err := store.GetNotes(0)
- if err != nil {
- t.Fatalf("GetNotes(0) returned error: %v", err)
- }
-
- if len(result) != 3 {
- t.Errorf("Expected all 3 notes with no limit, got %d", len(result))
- }
- })
-
- // Test 4: Call GetNotes(1) - should return exactly 1 note
- t.Run("GetNotes with limit 1", func(t *testing.T) {
- result, err := store.GetNotes(1)
- if err != nil {
- t.Fatalf("GetNotes(1) returned error: %v", err)
- }
-
- if len(result) != 1 {
- t.Errorf("Expected exactly 1 note, got %d", len(result))
- }
-
- // Should be the most recent note
- if len(result) == 1 && result[0].Filename != "note3.md" {
- t.Errorf("Expected note to be 'note3.md', got '%s'", result[0].Filename)
- }
- })
-}
-
-// TestGetNotes_EmptyDatabase verifies behavior with empty database
-func TestGetNotes_EmptyDatabase(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- result, err := store.GetNotes(10)
- if err != nil {
- t.Fatalf("GetNotes on empty database returned error: %v", err)
- }
-
- if len(result) != 0 {
- t.Errorf("Expected 0 notes from empty database, got %d", len(result))
- }
-}
-
-// TestSaveNotes_Upsert verifies that SaveNotes properly upserts notes
-func TestSaveNotes_Upsert(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- baseTime := time.Now()
-
- // Save initial note
- initialNote := []models.Note{
- {
- Filename: "test.md",
- Title: "Initial Title",
- Content: "Initial content",
- Modified: baseTime,
- Path: "/vault/test.md",
- Tags: []string{"initial"},
- },
- }
-
- if err := store.SaveNotes(initialNote); err != nil {
- t.Fatalf("Failed to save initial note: %v", err)
- }
-
- // Verify initial save
- notes, err := store.GetNotes(0)
- if err != nil {
- t.Fatalf("Failed to get notes: %v", err)
- }
- if len(notes) != 1 {
- t.Fatalf("Expected 1 note after initial save, got %d", len(notes))
- }
- if notes[0].Title != "Initial Title" {
- t.Errorf("Expected title 'Initial Title', got '%s'", notes[0].Title)
- }
-
- // Update the same note
- updatedNote := []models.Note{
- {
- Filename: "test.md",
- Title: "Updated Title",
- Content: "Updated content",
- Modified: baseTime.Add(1 * time.Hour),
- Path: "/vault/test.md",
- Tags: []string{"updated"},
- },
- }
-
- if err := store.SaveNotes(updatedNote); err != nil {
- t.Fatalf("Failed to save updated note: %v", err)
- }
-
- // Verify update (should still be 1 note, not 2)
- notes, err = store.GetNotes(0)
- if err != nil {
- t.Fatalf("Failed to get notes after update: %v", err)
- }
- if len(notes) != 1 {
- t.Errorf("Expected 1 note after update (upsert), got %d", len(notes))
- }
- if notes[0].Title != "Updated Title" {
- t.Errorf("Expected title 'Updated Title', got '%s'", notes[0].Title)
- }
-}
-
-// TestGetNotes_NegativeLimit verifies behavior with negative limit
-func TestGetNotes_NegativeLimit(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- // Save a note
- notes := []models.Note{
- {
- Filename: "test.md",
- Title: "Test",
- Content: "Test content",
- Modified: time.Now(),
- Path: "/vault/test.md",
- Tags: []string{},
- },
- }
-
- if err := store.SaveNotes(notes); err != nil {
- t.Fatalf("Failed to save note: %v", err)
- }
-
- // Call with negative limit (should be treated as no limit)
- result, err := store.GetNotes(-1)
- if err != nil {
- t.Fatalf("GetNotes(-1) returned error: %v", err)
- }
-
- // Should return all notes since negative is treated as 0/no limit
- if len(result) != 1 {
- t.Errorf("Expected 1 note with negative limit, got %d", len(result))
- }
-}
-
-// TestGetNotes_SQLInjectionAttempt verifies that LIMIT parameter is properly sanitized
-func TestGetNotes_SQLInjectionAttempt(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- // Save some notes
- notes := []models.Note{
- {
- Filename: "note1.md",
- Title: "Note 1",
- Content: "Content 1",
- Modified: time.Now(),
- Path: "/vault/note1.md",
- Tags: []string{},
- },
- {
- Filename: "note2.md",
- Title: "Note 2",
- Content: "Content 2",
- Modified: time.Now(),
- Path: "/vault/note2.md",
- Tags: []string{},
- },
- }
-
- if err := store.SaveNotes(notes); err != nil {
- t.Fatalf("Failed to save notes: %v", err)
- }
-
- // The LIMIT parameter is now properly parameterized using sql.Query with args
- // This test verifies that the code uses parameterized queries
- // If it didn't, passing a malicious value could cause SQL injection
-
- // Normal call should work
- result, err := store.GetNotes(1)
- if err != nil {
- t.Fatalf("GetNotes(1) returned error: %v", err)
- }
-
- if len(result) != 1 {
- t.Errorf("Expected 1 note, got %d", len(result))
- }
-
- // The fact that this test passes with a simple integer confirms
- // that the implementation properly uses parameterized queries
- // and is not vulnerable to SQL injection via the LIMIT clause
- fmt.Println("✓ LIMIT clause is properly parameterized")
-}
-
// setupTestStoreWithTasks creates a test store with tasks table
func setupTestStoreWithTasks(t *testing.T) *Store {
t.Helper()
diff --git a/tailwind.config.js b/tailwind.config.js
index 05f25ec..c41b0eb 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -22,7 +22,6 @@ module.exports = {
},
trello: '#0079bf',
todoist: '#e44332',
- obsidian: '#7c3aed',
plantoeat: '#10b981',
},
spacing: {
diff --git a/test/acceptance_test.go b/test/acceptance_test.go
index eee837e..ca672c3 100644
--- a/test/acceptance_test.go
+++ b/test/acceptance_test.go
@@ -65,7 +65,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *store.Store, func()) {
}
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, nil, nil, cfg)
+ h := handlers.New(db, todoistClient, trelloClient, nil, cfg)
// Set up router (same as main.go)
r := chi.NewRouter()
@@ -77,7 +77,6 @@ func setupTestServer(t *testing.T) (*httptest.Server, *store.Store, func()) {
r.Get("/", h.HandleDashboard)
r.Post("/api/refresh", h.HandleRefresh)
r.Get("/api/tasks", h.HandleGetTasks)
- r.Get("/api/notes", h.HandleGetNotes)
r.Get("/api/meals", h.HandleGetMeals)
r.Get("/api/boards", h.HandleGetBoards)
@@ -211,29 +210,6 @@ func TestFullWorkflow(t *testing.T) {
// The response can be either success message or dashboard data
})
- // Test 4: GET /api/notes (should return empty when no Obsidian client)
- t.Run("GetNotesEmpty", func(t *testing.T) {
- resp, err := http.Get(server.URL + "/api/notes")
- if err != nil {
- t.Fatalf("Failed to get notes: %v", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- t.Errorf("Expected status 200, got %d", resp.StatusCode)
- }
-
- var notes []models.Note
- if err := json.NewDecoder(resp.Body).Decode(&notes); err != nil {
- t.Fatalf("Failed to decode notes: %v", err)
- }
-
- if len(notes) != 0 {
- t.Errorf("Expected 0 notes (no Obsidian client), got %d", len(notes))
- }
- })
-
- // Test 5: GET /api/meals (should return empty when no PlanToEat client)
t.Run("GetMealsEmpty", func(t *testing.T) {
resp, err := http.Get(server.URL + "/api/meals")
if err != nil {
@@ -255,7 +231,7 @@ func TestFullWorkflow(t *testing.T) {
}
})
- // Test 6: GET / (Dashboard)
+ // Test 5: GET / (Dashboard)
t.Run("GetDashboard", func(t *testing.T) {
resp, err := http.Get(server.URL + "/")
if err != nil {
diff --git a/web/templates/index.html b/web/templates/index.html
index 045b4d6..b341c17 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -42,14 +42,6 @@
📋 Planning
</button>
<button
- class="tab-button {{if eq .ActiveTab "notes"}}tab-button-active{{end}}"
- hx-get="/tabs/notes"
- hx-target="#tab-content"
- hx-push-url="?tab=notes"
- onclick="setActiveTab(this)">
- 📝 Notes
- </button>
- <button
class="tab-button {{if eq .ActiveTab "meals"}}tab-button-active{{end}}"
hx-get="/tabs/meals"
hx-target="#tab-content"
diff --git a/web/templates/partials/notes-tab.html b/web/templates/partials/notes-tab.html
deleted file mode 100644
index df844cf..0000000
--- a/web/templates/partials/notes-tab.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{{define "notes-tab"}}
-<div class="space-y-10">
- <!-- Error Messages -->
- {{template "error-banner" .}}
-
- <!-- Search Bar -->
- <div class="mb-6">
- <input type="text"
- name="q"
- placeholder="Search notes..."
- class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-obsidian focus:border-transparent"
- hx-get="/tabs/notes"
- hx-trigger="keyup changed delay:300ms"
- hx-target="#notes-results"
- hx-indicator="#search-indicator">
- </div>
-
- <!-- Notes Results -->
- <div id="notes-results">
- {{if .Notes}}
- {{template "obsidian-notes" .}}
- {{else}}
- <div class="text-center py-20">
- <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
- d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
- </svg>
- <h3 class="mt-6 text-xl font-medium text-gray-900">No notes found</h3>
- <p class="mt-2 text-sm text-gray-500 max-w-md mx-auto">
- Configure your Obsidian vault path in the settings to see your recent notes displayed here.
- </p>
- </div>
- {{end}}
- </div>
-</div>
-{{end}}
diff --git a/web/templates/partials/obsidian-notes.html b/web/templates/partials/obsidian-notes.html
deleted file mode 100644
index 268a0fe..0000000
--- a/web/templates/partials/obsidian-notes.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "obsidian-notes"}}
-{{if .Notes}}
-<section class="section-spacing">
- <!-- Section Header with Brand Color -->
- <div class="flex items-center gap-3 mb-6">
- <div class="w-1 h-8 bg-obsidian rounded"></div>
- <h2 class="text-2xl font-bold text-gray-900">Recent Notes</h2>
- </div>
-
- <div class="card-grid">
- {{range .Notes}}
- <div class="note-card">
- <h3 class="font-semibold text-gray-900 mb-2">{{.Title}}</h3>
- <p class="text-sm text-gray-600 mb-3 line-clamp-3">{{.Content}}</p>
- <div class="flex justify-between items-center text-xs">
- <span class="text-gray-500">{{.Modified.Format "Jan 2, 3:04 PM"}}</span>
- {{if .Tags}}
- <div class="flex gap-1 flex-wrap">
- {{range .Tags}}
- <span class="badge bg-purple-100 text-purple-800">#{{.}}</span>
- {{end}}
- </div>
- {{end}}
- </div>
- </div>
- {{end}}
- </div>
-</section>
-{{end}}
-{{end}}