Mark Task 9 checkboxes complete: README already has accurate build/run/test instructions, config.yaml.example matches spec, no deviations from specification found in the foundation layer implementation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
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 drivergopkg.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 naviwatcherin project root - create directory structure:
internal/config/,internal/database/,cmd/naviwatcher/ - create
cmd/naviwatcher/main.gowith 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.gowith 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.examplewith 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.gowith DB struct andNew(dbPath string) (*DB, error)constructor - implement schema migration system (versioned migrations table + ordered migration files or functions)
- create migration 001:
artist_settingstable (id, name, ignore_singles, ignore_compilations, monitored) - create migration 002:
external_releasestable (rgid PK, artist_id, title, type, release_date, is_ignored) - create migration 003:
notifications_senttable (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
Dockerfilewith 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.ymlwith naviwatcher service, volume for SQLite DB and config - create
.dockerignorefile - 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.exampletoconfig.yamland 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