15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-backend-refactor | 01 | execute | 1 |
|
true |
|
|
Purpose: Establish the persistence abstraction layer and migration system that Plan 02 will wire into the Server struct and handlers. These are additive-only changes -- nothing existing breaks. Output: store.go, sqlite_store.go, migrate.go, migration SQL files, golang-migrate dependency installed.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-backend-refactor/02-RESEARCH.mdFrom pkg/diunwebhook/diunwebhook.go:
type DiunEvent struct {
DiunVersion string `json:"diun_version"`
Hostname string `json:"hostname"`
Status string `json:"status"`
Provider string `json:"provider"`
Image string `json:"image"`
HubLink string `json:"hub_link"`
MimeType string `json:"mime_type"`
Digest string `json:"digest"`
Created time.Time `json:"created"`
Platform string `json:"platform"`
Metadata struct {
ContainerName string `json:"ctn_names"`
ContainerID string `json:"ctn_id"`
State string `json:"ctn_state"`
Status string `json:"ctn_status"`
} `json:"metadata"`
}
type Tag struct {
ID int `json:"id"`
Name string `json:"name"`
}
type UpdateEntry struct {
Event DiunEvent `json:"event"`
ReceivedAt time.Time `json:"received_at"`
Acknowledged bool `json:"acknowledged"`
Tag *Tag `json:"tag"`
}
Create pkg/diunwebhook/store.go with exactly this interface (per REFAC-01):
package diunwebhook
// Store defines all persistence operations. Implementations must be safe
// for concurrent use from HTTP handlers.
type Store interface {
UpsertEvent(event DiunEvent) error
GetUpdates() (map[string]UpdateEntry, error)
AcknowledgeUpdate(image string) (found bool, err error)
ListTags() ([]Tag, error)
CreateTag(name string) (Tag, error)
DeleteTag(id int) (found bool, err error)
AssignTag(image string, tagID int) error
UnassignTag(image string) error
TagExists(id int) (bool, error)
}
Create pkg/diunwebhook/sqlite_store.go with SQLiteStore struct implementing all 9 Store methods:
package diunwebhook
import (
"database/sql"
"sync"
"time"
)
type SQLiteStore struct {
db *sql.DB
mu sync.Mutex
}
func NewSQLiteStore(db *sql.DB) *SQLiteStore {
return &SQLiteStore{db: db}
}
Move all SQL from current handlers/functions into Store methods:
-
UpsertEvent -- move the INSERT...ON CONFLICT from current
UpdateEvent()function. Keep exact same SQL includingON CONFLICT(image) DO UPDATE SETwith all 14 columns andacknowledged_at = NULL. Usetime.Now().Format(time.RFC3339)for received_at. Acquires.mu.Lock(). -
GetUpdates -- move the SELECT...LEFT JOIN from current
GetUpdates()function. Exact same query:SELECT u.image, u.diun_version, ...with LEFT JOIN on tag_assignments and tags. Same row scanning logic withsql.NullInt64/sql.NullStringfor tag fields. No mutex needed (read-only). -
AcknowledgeUpdate -- move SQL from
DismissHandler:UPDATE updates SET acknowledged_at = datetime('now') WHERE image = ?. Return(found bool, err error)where found = RowsAffected() > 0. Acquires.mu.Lock(). -
ListTags -- move SQL from
TagsHandlerGET case:SELECT id, name FROM tags ORDER BY name. Return([]Tag, error). No mutex. -
CreateTag -- move SQL from
TagsHandlerPOST case:INSERT INTO tags (name) VALUES (?). Return(Tag{ID: int(lastInsertId), Name: name}, error). Acquires.mu.Lock(). -
DeleteTag -- move SQL from
TagByIDHandler:DELETE FROM tags WHERE id = ?. Return(found bool, err error)where found = RowsAffected() > 0. Acquires.mu.Lock(). -
AssignTag -- move SQL from
TagAssignmentHandlerPUT case:INSERT OR REPLACE INTO tag_assignments (image, tag_id) VALUES (?, ?). KeepINSERT OR REPLACE(correct for SQLite, per research Pitfall 6). Acquires.mu.Lock(). -
UnassignTag -- move SQL from
TagAssignmentHandlerDELETE case:DELETE FROM tag_assignments WHERE image = ?. Acquires.mu.Lock(). -
TagExists -- move SQL from
TagAssignmentHandlerPUT check:SELECT COUNT(*) FROM tags WHERE id = ?. Return(bool, error)where bool = count > 0. No mutex (read-only).
CRITICAL: NewSQLiteStore must run PRAGMA foreign_keys = ON on the db connection and db.SetMaxOpenConns(1) -- these currently live in InitDB and must NOT be lost. Specifically:
func NewSQLiteStore(db *sql.DB) *SQLiteStore {
db.SetMaxOpenConns(1)
// PRAGMA foreign_keys must be set per-connection; with MaxOpenConns(1) this covers all queries
db.Exec("PRAGMA foreign_keys = ON")
return &SQLiteStore{db: db}
}
rows.Close() pattern: Use defer rows.Close() directly (not the verbose closure pattern from the current code). The error from Close() is safe to ignore in read paths.
cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go build ./pkg/diunwebhook/ && echo "BUILD OK"
<acceptance_criteria>
- pkg/diunwebhook/store.go contains type Store interface {
- pkg/diunwebhook/store.go contains exactly these 9 method signatures: UpsertEvent, GetUpdates, AcknowledgeUpdate, ListTags, CreateTag, DeleteTag, AssignTag, UnassignTag, TagExists
- pkg/diunwebhook/sqlite_store.go contains type SQLiteStore struct {
- pkg/diunwebhook/sqlite_store.go contains func NewSQLiteStore(db *sql.DB) *SQLiteStore
- pkg/diunwebhook/sqlite_store.go contains db.SetMaxOpenConns(1)
- pkg/diunwebhook/sqlite_store.go contains PRAGMA foreign_keys = ON
- pkg/diunwebhook/sqlite_store.go contains func (s *SQLiteStore) UpsertEvent(event DiunEvent) error
- pkg/diunwebhook/sqlite_store.go contains s.mu.Lock() (mutex usage in write methods)
- pkg/diunwebhook/sqlite_store.go contains INSERT OR REPLACE INTO tag_assignments (not ON CONFLICT for this table)
- pkg/diunwebhook/sqlite_store.go contains ON CONFLICT(image) DO UPDATE SET (UPSERT for updates table)
- go build ./pkg/diunwebhook/ exits 0
</acceptance_criteria>
Store interface defines 9 methods; SQLiteStore implements all 9 with exact SQL from current handlers; package compiles with no errors
Create directory pkg/diunwebhook/migrations/sqlite/.
0001_initial_schema.up.sql -- Full current schema as a single baseline migration. Use CREATE TABLE IF NOT EXISTS for backward compatibility with existing databases (per research recommendation):
CREATE TABLE IF NOT EXISTS updates (
image TEXT PRIMARY KEY,
diun_version TEXT NOT NULL DEFAULT '',
hostname TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL DEFAULT '',
hub_link TEXT NOT NULL DEFAULT '',
mime_type TEXT NOT NULL DEFAULT '',
digest TEXT NOT NULL DEFAULT '',
created TEXT NOT NULL DEFAULT '',
platform TEXT NOT NULL DEFAULT '',
ctn_name TEXT NOT NULL DEFAULT '',
ctn_id TEXT NOT NULL DEFAULT '',
ctn_state TEXT NOT NULL DEFAULT '',
ctn_status TEXT NOT NULL DEFAULT '',
received_at TEXT NOT NULL,
acknowledged_at TEXT
);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS tag_assignments (
image TEXT PRIMARY KEY,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE
);
0001_initial_schema.down.sql -- Reverse of up migration:
DROP TABLE IF EXISTS tag_assignments;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS updates;
Create pkg/diunwebhook/migrate.go:
package diunwebhook
import (
"database/sql"
"embed"
"errors"
"github.com/golang-migrate/migrate/v4"
sqlitemigrate "github.com/golang-migrate/migrate/v4/database/sqlite"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "modernc.org/sqlite"
)
//go:embed migrations/sqlite
var sqliteMigrations embed.FS
// RunMigrations applies all pending schema migrations to the given SQLite database.
// Returns nil if all migrations applied successfully or if database is already up to date.
func RunMigrations(db *sql.DB) error {
src, err := iofs.New(sqliteMigrations, "migrations/sqlite")
if err != nil {
return err
}
driver, err := sqlitemigrate.WithInstance(db, &sqlitemigrate.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithInstance("iofs", src, "sqlite", driver)
if err != nil {
return err
}
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
CRITICAL imports:
- Use
database/sqlite(NOTdatabase/sqlite3) -- the sqlite3 variant requires CGO which is forbidden - Import alias
sqlitemigrateforgithub.com/golang-migrate/migrate/v4/database/sqliteto avoid collision with the blank import ofmodernc.org/sqlite - The
_ "modernc.org/sqlite"blank import must be present so the "sqlite" driver is registered forsql.Open
After creating files, run:
cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go mod tidy
<success_criteria>
- Store interface with 9 methods exists in store.go
- SQLiteStore implements all 9 methods in sqlite_store.go with exact SQL semantics from current handlers
- NewSQLiteStore sets PRAGMA foreign_keys = ON and MaxOpenConns(1)
- RunMigrations in migrate.go uses golang-migrate + embed.FS + iofs, handles ErrNoChange
- Migration 0001 contains full current schema with CREATE TABLE IF NOT EXISTS
- All existing tests still pass (no existing code modified)
- No CGO dependency introduced </success_criteria>