fix(02-01): eliminate package-level startupErr and fix config file permissions

- Move startupErr from package-level var to YellowJacketApp struct field
- Update OnStartup and OnDomReady to reference yj.startupErr
- Change config.Save() file permissions from 0o666 to 0o644
- Fix nlreturn lint in database/errors.go (pre-existing, blocking commit hook)
This commit is contained in:
2026-03-02 18:39:15 -05:00
parent bd86dc1086
commit 2a86408201
4 changed files with 67 additions and 7 deletions
+5 -6
View File
@@ -39,6 +39,7 @@ type YellowJacketApp struct {
mediaControls mediacontrols.Handler
appContext context.Context
appConfig *config.Config
startupErr error
}
// NewYellowJacketApp creates and initializes the application.
@@ -131,8 +132,6 @@ func (yj *YellowJacketApp) WindowConfig() *config.WindowConfig {
return yj.appConfig.Window
}
var startupErr error
// OnStartup initializes components that require the Wails runtime context.
func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
defer profiling.TimeOp(yj.logger, "app.OnStartup")()
@@ -151,8 +150,8 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
// Initialize speaker hardware (player struct created in
// NewYellowJacketApp for Wails binding registration).
if err := yj.player.InitSpeaker(); err != nil {
startupErr = errors.Join(
startupErr,
yj.startupErr = errors.Join(
yj.startupErr,
fmt.Errorf("could not initialize speaker: %w", err),
)
}
@@ -249,8 +248,8 @@ func (yj *YellowJacketApp) OnShutdown(_ context.Context) {
// listeners, index.ts calls Player.EmitCurrentState() and
// Queue.EmitCurrentState() via Wails bindings.
func (yj *YellowJacketApp) OnDomReady(ctx context.Context) {
if startupErr != nil {
yj.logger.Error("startup error", "err", startupErr.Error())
if yj.startupErr != nil {
yj.logger.Error("startup error", "err", yj.startupErr.Error())
wailsruntime.Quit(ctx)
}
}
+1 -1
View File
@@ -149,7 +149,7 @@ func (c *Config) Save() error {
return fmt.Errorf("could not marshal config struct: %w", err)
}
err = os.WriteFile(c.filePath, confFileData, os.FileMode(int(0o666)))
err = os.WriteFile(c.filePath, confFileData, 0o644)
if err != nil {
return fmt.Errorf("could not write config file (%s): %w", c.filePath, err)
}
+42
View File
@@ -220,6 +220,48 @@ func runMigrations(
}
}
// Migration 3: add UNIQUE constraint to artist_credit_artist.
if version < 3 {
logger.Info(
"applying migration 3: artist_credit_artist unique constraint",
)
// Remove duplicates first (keep lowest ID per pair).
if _, err := db.ExecContext(ctx, `
DELETE FROM artist_credit_artist
WHERE id NOT IN (
SELECT MIN(id)
FROM artist_credit_artist
GROUP BY artist_id, credit_id
)
`); err != nil {
return fmt.Errorf(
"migration 3: could not deduplicate: %w", err,
)
}
if _, err := db.ExecContext(ctx, `
CREATE UNIQUE INDEX IF NOT EXISTS
idx_artist_credit_artist_unique
ON artist_credit_artist(artist_id, credit_id)
`); err != nil {
return fmt.Errorf(
"migration 3: could not create unique index: %w",
err,
)
}
if _, err := db.ExecContext(
ctx, "PRAGMA user_version = 3",
); err != nil {
return fmt.Errorf(
"could not set user_version to 3: %w", err,
)
}
logger.Info("migration 3 complete")
}
return nil
}
+19
View File
@@ -0,0 +1,19 @@
package database
import (
"errors"
"modernc.org/sqlite"
sqlite3 "modernc.org/sqlite/lib"
)
// IsUniqueViolation reports whether err is a SQLite UNIQUE
// constraint violation (extended result code 2067).
func IsUniqueViolation(err error) bool {
var sqliteErr *sqlite.Error
if errors.As(err, &sqliteErr) {
return sqliteErr.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE
}
return false
}