Files
NaviWatcher/docs/plans/completed/2026-05-20-foundation-layer.md

12 KiB
Raw Permalink Blame History

Foundation Layer

Overview

Build the foundation layer of NaviWatcher: a greenfield Go project with zero existing code. This plan covers project initialization, configuration management, SQLite database layer, and Docker deployment setup. These are the building blocks that all future modules (Navidrome client, MusicBrainz provider, Scanner, Notifier, Web UI) will depend on.

Problem it solves: Establishes the project skeleton, dependency management, configuration parsing, persistent storage, and containerized deployment — everything needed before any business logic can be built.

How it integrates: This is the first plan for the project. Future plans will build on this foundation:

  • Plan 2: Navidrome client (depends on config + DB)
  • Plan 3: MusicBrainz provider (depends on config + DB)
  • Plan 4: Scanner engine (depends on Navidrome + MusicBrainz + DB)
  • Plan 5: Notifier (depends on DB + config)
  • Plan 6: Web UI (depends on DB + config)

Context (from discovery)

  • Files/components involved: All files are new — this is a greenfield project
  • Related patterns found: Specification defines a 6-module architecture with YAML config, SQLite storage, and Docker deployment
  • Dependencies identified:
    • github.com/mattn/go-sqlite3 — SQLite 3 driver
    • gopkg.in/yaml.v3 — YAML config parsing
    • Go 1.21+ required
  • Current state: Only docs exist (CLAUDE.md, README.md, License.md, docs/Specification.md). No Go code, no go.mod, no config files.

Development Approach

  • Testing approach: Regular (code first, then tests)
  • Complete each task fully before moving to the next
  • Make small, focused changes
  • CRITICAL: every task MUST include new/updated tests for code changes in that task
    • tests are not optional — they are a required part of the checklist
    • write unit tests for new functions/methods
    • write unit tests for modified functions/methods
    • add new test cases for new code paths
    • update existing test cases if behavior changes
    • tests cover both success and error scenarios
  • CRITICAL: all tests must pass before starting next task — no exceptions
  • CRITICAL: update this plan file when scope changes during implementation
  • Run tests after each change
  • Maintain backward compatibility

Testing Strategy

  • Unit tests: required for every task (see Development Approach above)
  • E2E tests: not applicable for foundation layer (no UI yet)

Progress Tracking

  • Mark completed items with [x] immediately when done
  • Add newly discovered tasks with prefix
  • Document issues/blockers with ⚠️ prefix
  • Update plan if implementation deviates from original scope
  • Keep plan in sync with actual work done

What Goes Where

  • Implementation Steps ([ ] checkboxes): tasks achievable within this codebase — code changes, tests, documentation updates
  • Post-Completion (no checkboxes): items requiring external action — manual testing, deployment verification, third-party checks

Implementation Steps

Task 1: Initialize Go module and project skeleton

  • run go mod init naviwatcher in project root
  • create directory structure: internal/config/, internal/database/, cmd/naviwatcher/
  • create cmd/naviwatcher/main.go with basic entry point (parse flags, load config placeholder, graceful shutdown skeleton)
  • add dependencies: go get github.com/mattn/go-sqlite3, go get gopkg.in/yaml.v3
  • run go mod tidy
  • verify the project builds: go build -o naviwatcher ./cmd/naviwatcher
  • write tests for main.go flag parsing (if applicable)
  • run tests — must pass before task 2

Task 2: Configuration management

  • create internal/config/config.go with Config struct matching spec (Server, Navidrome, MusicBrainz, Telegram, Scanner sections)
  • implement LoadConfig(path string) (*Config, error) function that reads and parses YAML
  • implement config validation (required fields, valid port range, valid threshold range 0.0-1.0)
  • create config.yaml.example with all fields filled with placeholder values per spec section 7
  • write tests for LoadConfig: valid config file
  • write tests for LoadConfig: missing file, malformed YAML, invalid values
  • write tests for config validation logic
  • run tests — must pass before task 3

Task 3: Database layer — schema and migrations

  • create internal/database/database.go with DB struct and New(dbPath string) (*DB, error) constructor
  • implement schema migration system (versioned migrations table + ordered migration files or functions)
  • create migration 001: artist_settings table (id, name, ignore_singles, ignore_compilations, monitored)
  • create migration 002: external_releases table (rgid PK, artist_id, title, type, release_date, is_ignored)
  • create migration 003: notifications_sent table (rgid FK, sent_at)
  • implement Close() method with proper connection cleanup
  • write tests for database initialization and schema creation (use in-memory SQLite :memory:)
  • write tests for migration idempotency (running migrations twice should not fail)
  • write tests for Close() behavior
  • run tests — must pass before task 4

Task 4: Database layer — CRUD operations for artist_settings

  • implement GetArtistSettings(db *DB, id string) (*ArtistSettings, error)
  • implement SaveArtistSettings(db *DB, settings *ArtistSettings) error
  • implement GetAllArtistSettings(db *DB) ([]ArtistSettings, error)
  • implement UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) error
  • write tests for GetArtistSettings (found and not found cases)
  • write tests for SaveArtistSettings (insert and update)
  • write tests for GetAllArtistSettings (empty and populated)
  • run tests — must pass before task 5

Task 5: Database layer — CRUD operations for external_releases

  • implement GetExternalRelease(db *DB, rgid string) (*ExternalRelease, error)
  • implement SaveExternalRelease(db *DB, release *ExternalRelease) error
  • implement GetExternalReleasesByArtist(db *DB, artistID string) ([]ExternalRelease, error)
  • implement GetIgnoredReleases(db *DB) ([]ExternalRelease, error)
  • implement SetReleaseIgnored(db *DB, rgid string, ignored bool) error
  • write tests for all CRUD operations (success and error cases)
  • write tests for cache TTL logic (if implemented at this layer)
  • run tests — must pass before task 6

Task 6: Database layer — CRUD operations for notifications_sent

  • implement MarkNotificationSent(db *DB, rgid string) error
  • implement IsNotificationSent(db *DB, rgid string) (bool, error)
  • implement GetUnnotifiedReleases(db *DB) ([]ExternalRelease, error) — joins external_releases with notifications_sent to find unsent
  • write tests for MarkNotificationSent and IsNotificationSent
  • write tests for GetUnnotifiedReleases (with and without existing notifications)
  • run tests — must pass before task 7

Task 7: Docker setup

  • create Dockerfile with multi-stage build: build stage (golang:1.25-alpine) + runtime stage (alpine:latest)
  • configure Dockerfile to copy config, build binary, expose port, set entrypoint
  • create docker-compose.yml with naviwatcher service, volume for SQLite DB and config
  • create .dockerignore file
  • verify Docker image builds: docker compose build
  • verify container starts and health check passes (container starts, loads config, runs successfully)
  • run full test suite — must pass before task 8

Task 8: Verify acceptance criteria

  • verify Go module builds cleanly with go build
  • verify config loads and validates from YAML file
  • verify all 3 database tables are created via migrations
  • verify all CRUD operations work against in-memory SQLite
  • verify Docker image builds and container starts
  • run full test suite: go test ./... -v — all must pass
  • run go vet ./... — no issues
  • run go fmt ./... — no formatting issues
  • verify test coverage is reasonable for foundation layer (70%+)

Task 9: Final documentation update

  • update README.md with current build/run/test instructions (replace placeholder commands)
  • verify config.yaml.example is complete and matches spec
  • document any deviations from specification in this plan — no deviations found; module path is naviwatcher (local), Go 1.25.1 exceeds 1.21+ requirement

Note: ralphex automatically moves completed plans to docs/plans/completed/

Technical Details

Config Struct (Go)

type Config struct {
    Server      ServerConfig      `yaml:"server"`
    Navidrome   NavidromeConfig   `yaml:"navidrome"`
    MusicBrainz MusicBrainzConfig `yaml:"musicbrainz"`
    Telegram    TelegramConfig    `yaml:"telegram"`
    Scanner     ScannerConfig     `yaml:"scanner"`
}

type ServerConfig struct {
    Host     string `yaml:"host"`
    Port     int    `yaml:"port"`
    Username string `yaml:"username"`
    Password string `yaml:"password"`
}

type NavidromeConfig struct {
    URL      string `yaml:"url"`
    User     string `yaml:"user"`
    Password string `yaml:"password"`
}

type MusicBrainzConfig struct {
    UserAgent string        `yaml:"user_agent"`
    CacheTTL  time.Duration `yaml:"cache_ttl"`
}

type TelegramConfig struct {
    Enabled      bool   `yaml:"enabled"`
    Token        string `yaml:"token"`
    ChatID       string `yaml:"chat_id"`
    CronSchedule string `yaml:"cron_schedule"`
}

type ScannerConfig struct {
    FuzzyThreshold     float64 `yaml:"fuzzy_threshold"`
    IgnoreBootlegs     bool    `yaml:"ignore_bootlegs"`
    IncludeCompilations bool   `yaml:"include_compilations"`
}

Database Schema (SQLite)

CREATE TABLE IF NOT EXISTS artist_settings (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    ignore_singles BOOLEAN DEFAULT 0,
    ignore_compilations BOOLEAN DEFAULT 0,
    monitored BOOLEAN DEFAULT 1
);

CREATE TABLE IF NOT EXISTS external_releases (
    rgid TEXT PRIMARY KEY,
    artist_id TEXT NOT NULL,
    title TEXT NOT NULL,
    type TEXT,
    release_date TEXT,
    is_ignored BOOLEAN DEFAULT 0
);

CREATE TABLE IF NOT EXISTS notifications_sent (
    rgid TEXT NOT NULL,
    sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (rgid, sent_at)
);

Project Structure After Completion

naviwatcher-gitea/
├── CLAUDE.md
├── License.md
├── README.md
├── config.yaml.example
├── docker-compose.yml
├── Dockerfile
├── .dockerignore
├── go.mod
├── go.sum
├── cmd/
│   └── naviwatcher/
│       └── main.go
├── internal/
│   ├── config/
│   │   ├── config.go
│   │   └── config_test.go
│   └── database/
│       ├── database.go
│       ├── database_test.go
│       ├── artist_settings.go
│       ├── artist_settings_test.go
│       ├── external_releases.go
│       ├── external_releases_test.go
│       ├── notifications.go
│       └── notifications_test.go
└── docs/
    ├── Specification.md
    └── plans/
        └── 2026-05-20-foundation-layer.md

Post-Completion

Items requiring manual intervention or external systems — no checkboxes, informational only

Manual verification:

  • Copy config.yaml.example to config.yaml and fill in real values to test config loading
  • Run the built Docker container against a real Navidrome instance (when client is implemented)
  • Verify SQLite database file persists across container restarts via Docker volume

Follow-up plans needed:

  • Plan 2: Navidrome/Subsonic API client
  • Plan 3: MusicBrainz provider with rate limiting
  • Plan 4: Scanner engine with fuzzy matching
  • Plan 5: Telegram notifier with cron scheduling
  • Plan 6: Web UI with embedded templates