Files
NaviWatcher/docs/plans/2026-05-20-foundation-layer.md
Vladimir Zagainov 735ff0828e feat: add foundation layer (Go module, config, database, Docker)
Squashed commits from foundation-layer branch:

- Initialize Go module and project skeleton (cmd/naviwatcher/main.go)
- Add configuration management with YAML parsing and validation
- Add database layer with schema migrations (artist_settings, external_releases, notifications_sent)
- Add CRUD operations for artist_settings, external_releases, notifications_sent
- Add Docker setup with multi-stage build and docker-compose
- Verify acceptance criteria (tests, vet, fmt)
- Update README.md with build/run/test instructions
- Fix: filter ignored releases in GetUnnotifiedReleases (spec compliance)
- Fix: add FK constraint on notifications_sent.rgid
- Fix: add config.yaml to .gitignore (security)
- Fix: run Docker container as non-root user
- Fix: pin alpine:3.21 instead of alpine:latest
- Fix: wrap migrations in transactions for atomicity

All 49 tests pass, go vet clean, Docker image builds successfully.
2026-05-20 16:11:11 +03:00

262 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- [x] run `go mod init naviwatcher` in project root
- [x] create directory structure: `internal/config/`, `internal/database/`, `cmd/naviwatcher/`
- [x] create `cmd/naviwatcher/main.go` with basic entry point (parse flags, load config placeholder, graceful shutdown skeleton)
- [x] add dependencies: `go get github.com/mattn/go-sqlite3`, `go get gopkg.in/yaml.v3`
- [x] run `go mod tidy`
- [x] verify the project builds: `go build -o naviwatcher ./cmd/naviwatcher`
- [x] write tests for main.go flag parsing (if applicable)
- [x] run tests — must pass before task 2
### Task 2: Configuration management
- [x] create `internal/config/config.go` with Config struct matching spec (Server, Navidrome, MusicBrainz, Telegram, Scanner sections)
- [x] implement `LoadConfig(path string) (*Config, error)` function that reads and parses YAML
- [x] implement config validation (required fields, valid port range, valid threshold range 0.0-1.0)
- [x] create `config.yaml.example` with all fields filled with placeholder values per spec section 7
- [x] write tests for LoadConfig: valid config file
- [x] write tests for LoadConfig: missing file, malformed YAML, invalid values
- [x] write tests for config validation logic
- [x] run tests — must pass before task 3
### Task 3: Database layer — schema and migrations
- [x] create `internal/database/database.go` with DB struct and `New(dbPath string) (*DB, error)` constructor
- [x] implement schema migration system (versioned migrations table + ordered migration files or functions)
- [x] create migration 001: `artist_settings` table (id, name, ignore_singles, ignore_compilations, monitored)
- [x] create migration 002: `external_releases` table (rgid PK, artist_id, title, type, release_date, is_ignored)
- [x] create migration 003: `notifications_sent` table (rgid FK, sent_at)
- [x] implement `Close()` method with proper connection cleanup
- [x] write tests for database initialization and schema creation (use in-memory SQLite `:memory:`)
- [x] write tests for migration idempotency (running migrations twice should not fail)
- [x] write tests for Close() behavior
- [x] run tests — must pass before task 4
### Task 4: Database layer — CRUD operations for artist_settings
- [x] implement `GetArtistSettings(db *DB, id string) (*ArtistSettings, error)`
- [x] implement `SaveArtistSettings(db *DB, settings *ArtistSettings) error`
- [x] implement `GetAllArtistSettings(db *DB) ([]ArtistSettings, error)`
- [x] implement `UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) error`
- [x] write tests for GetArtistSettings (found and not found cases)
- [x] write tests for SaveArtistSettings (insert and update)
- [x] write tests for GetAllArtistSettings (empty and populated)
- [x] run tests — must pass before task 5
### Task 5: Database layer — CRUD operations for external_releases
- [x] implement `GetExternalRelease(db *DB, rgid string) (*ExternalRelease, error)`
- [x] implement `SaveExternalRelease(db *DB, release *ExternalRelease) error`
- [x] implement `GetExternalReleasesByArtist(db *DB, artistID string) ([]ExternalRelease, error)`
- [x] implement `GetIgnoredReleases(db *DB) ([]ExternalRelease, error)`
- [x] implement `SetReleaseIgnored(db *DB, rgid string, ignored bool) error`
- [x] write tests for all CRUD operations (success and error cases)
- [x] write tests for cache TTL logic (if implemented at this layer)
- [x] run tests — must pass before task 6
### Task 6: Database layer — CRUD operations for notifications_sent
- [x] implement `MarkNotificationSent(db *DB, rgid string) error`
- [x] implement `IsNotificationSent(db *DB, rgid string) (bool, error)`
- [x] implement `GetUnnotifiedReleases(db *DB) ([]ExternalRelease, error)` — joins external_releases with notifications_sent to find unsent
- [x] write tests for MarkNotificationSent and IsNotificationSent
- [x] write tests for GetUnnotifiedReleases (with and without existing notifications)
- [x] run tests — must pass before task 7
### Task 7: Docker setup
- [x] create `Dockerfile` with multi-stage build: build stage (golang:1.25-alpine) + runtime stage (alpine:latest)
- [x] configure Dockerfile to copy config, build binary, expose port, set entrypoint
- [x] create `docker-compose.yml` with naviwatcher service, volume for SQLite DB and config
- [x] create `.dockerignore` file
- [x] verify Docker image builds: `docker compose build`
- [x] verify container starts and health check passes (container starts, loads config, runs successfully)
- [x] run full test suite — must pass before task 8
### Task 8: Verify acceptance criteria
- [x] verify Go module builds cleanly with `go build`
- [x] verify config loads and validates from YAML file
- [x] verify all 3 database tables are created via migrations
- [x] verify all CRUD operations work against in-memory SQLite
- [x] verify Docker image builds and container starts
- [x] run full test suite: `go test ./... -v` — all must pass
- [x] run `go vet ./...` — no issues
- [x] run `go fmt ./...` — no formatting issues
- [x] verify test coverage is reasonable for foundation layer (70%+)
### Task 9: Final documentation update
- [x] update README.md with current build/run/test instructions (replace placeholder commands)
- [x] verify config.yaml.example is complete and matches spec
- [x] 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)
```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)
```sql
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