feat: create admin dashboard with user preferences and library settings

- Add admin dashboard at /admin route consolidating all user settings
- Move user preferences (username, email, password, theme) from separate page
- Add ebook library preferences section with folder management
- Add scan settings (frequency and auto-scan toggle) with database persistence
- Create database migration for scan_frequency_minutes and auto_scan_enabled columns
- Add API endpoints for scan settings management:
  - PUT /api/library/scan-settings - Update scan preferences
  - GET /api/library/scan-settings - Get current scan settings
- Update dashboard navigation to link to admin dashboard
- Remove old preferences.html template (functionality moved to admin)
- Create Bruno API testing files for library endpoints
- Add real-time folder loading and management in admin interface
- Implement scan settings persistence and retrieval from database
This commit is contained in:
2026-01-23 09:38:40 -05:00
parent 2dba1d6eb9
commit 11621462f2
12 changed files with 551 additions and 192 deletions
+9 -7
View File
@@ -55,11 +55,13 @@ type UserEbookFolders struct {
}
type Users struct {
ID pgtype.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
PasswordHash string `db:"password_hash" json:"password_hash"`
Theme pgtype.Text `db:"theme" json:"theme"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
ID pgtype.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
PasswordHash string `db:"password_hash" json:"password_hash"`
Theme pgtype.Text `db:"theme" json:"theme"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"`
AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"`
}
+2
View File
@@ -25,6 +25,7 @@ type Querier interface {
GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error)
GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error)
GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error)
GetScanSettings(ctx context.Context, id pgtype.UUID) (GetScanSettingsRow, error)
GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error)
GetUserByEmail(ctx context.Context, email string) (Users, error)
GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error)
@@ -38,6 +39,7 @@ type Querier interface {
UpdateEmail(ctx context.Context, arg UpdateEmailParams) error
UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error
UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error)
UpdateScanSettings(ctx context.Context, arg UpdateScanSettingsParams) error
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error
}
+43 -4
View File
@@ -131,7 +131,7 @@ func (q *Queries) CreateEbookRating(ctx context.Context, arg CreateEbookRatingPa
const CreateUser = `-- name: CreateUser :one
INSERT INTO users (email, username, password_hash, theme)
VALUES ($1, $2, $3, $4)
RETURNING id, email, username, password_hash, theme, created_at, updated_at
RETURNING id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled
`
type CreateUserParams struct {
@@ -157,6 +157,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (Users,
&i.Theme,
&i.CreatedAt,
&i.UpdatedAt,
&i.ScanFrequencyMinutes,
&i.AutoScanEnabled,
)
return i, err
}
@@ -373,6 +375,22 @@ func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgress
return i, err
}
const GetScanSettings = `-- name: GetScanSettings :one
SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1
`
type GetScanSettingsRow struct {
ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"`
AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"`
}
func (q *Queries) GetScanSettings(ctx context.Context, id pgtype.UUID) (GetScanSettingsRow, error) {
row := q.db.QueryRow(ctx, GetScanSettings, id)
var i GetScanSettingsRow
err := row.Scan(&i.ScanFrequencyMinutes, &i.AutoScanEnabled)
return i, err
}
const GetUser = `-- name: GetUser :one
SELECT id, email, username, theme, created_at, updated_at FROM users WHERE id = $1
`
@@ -401,7 +419,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro
}
const GetUserByEmail = `-- name: GetUserByEmail :one
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1
SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE email = $1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, error) {
@@ -415,12 +433,14 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, erro
&i.Theme,
&i.CreatedAt,
&i.UpdatedAt,
&i.ScanFrequencyMinutes,
&i.AutoScanEnabled,
)
return i, err
}
const GetUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1 OR username = $1
SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE email = $1 OR username = $1
`
func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) {
@@ -434,12 +454,14 @@ func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (U
&i.Theme,
&i.CreatedAt,
&i.UpdatedAt,
&i.ScanFrequencyMinutes,
&i.AutoScanEnabled,
)
return i, err
}
const GetUserByUsername = `-- name: GetUserByUsername :one
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE username = $1
SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE username = $1
`
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users, error) {
@@ -453,6 +475,8 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users
&i.Theme,
&i.CreatedAt,
&i.UpdatedAt,
&i.ScanFrequencyMinutes,
&i.AutoScanEnabled,
)
return i, err
}
@@ -753,6 +777,21 @@ func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingPr
return i, err
}
const UpdateScanSettings = `-- name: UpdateScanSettings :exec
UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at = NOW() WHERE id = $1
`
type UpdateScanSettingsParams struct {
ID pgtype.UUID `db:"id" json:"id"`
ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"`
AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"`
}
func (q *Queries) UpdateScanSettings(ctx context.Context, arg UpdateScanSettingsParams) error {
_, err := q.db.Exec(ctx, UpdateScanSettings, arg.ID, arg.ScanFrequencyMinutes, arg.AutoScanEnabled)
return err
}
const UpdateUserTheme = `-- name: UpdateUserTheme :exec
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1
`
+6
View File
@@ -84,6 +84,12 @@ UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
-- name: UpdateScanSettings :exec
UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at = NOW() WHERE id = $1;
-- name: GetScanSettings :one
SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1;
-- name: CreateEbookRating :one
INSERT INTO ebook_ratings (ebook_id, user_id, rating)
VALUES ($1, $2, $3)