Merge branch 'main' of ssh://git.linuxhg.com:2222/Bookhoard/bookhoard
This commit is contained in:
@@ -507,5 +507,6 @@ type Users struct {
|
||||
Theme pgtype.Text `db:"theme" json:"theme"`
|
||||
MaxDevices pgtype.Int4 `db:"max_devices" json:"max_devices"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
Timezone pgtype.Text `db:"timezone" json:"timezone"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -247,6 +247,7 @@ type Querier interface {
|
||||
GetSystemConfig(ctx context.Context, key string) (SystemConfig, error)
|
||||
// System Settings queries
|
||||
GetSystemSetting(ctx context.Context, settingKey string) (string, error)
|
||||
GetSystemTimezone(ctx context.Context) (string, error)
|
||||
// Get universal progress for a book
|
||||
GetUniversalProgress(ctx context.Context, arg GetUniversalProgressParams) (GetUniversalProgressRow, error)
|
||||
// Get unlinked book by ContentId
|
||||
@@ -373,6 +374,7 @@ type Querier interface {
|
||||
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
|
||||
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UpdateUserRoleRow, error)
|
||||
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
|
||||
UpdateUserTimezone(ctx context.Context, arg UpdateUserTimezoneParams) error
|
||||
UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error
|
||||
UpsertDashboardPreferences(ctx context.Context, arg UpsertDashboardPreferencesParams) (UserDashboardPreferences, error)
|
||||
UpsertPanelData(ctx context.Context, arg UpsertPanelDataParams) (PanelData, error)
|
||||
|
||||
@@ -5408,6 +5408,17 @@ func (q *Queries) GetSystemSetting(ctx context.Context, settingKey string) (stri
|
||||
return setting_value, err
|
||||
}
|
||||
|
||||
const GetSystemTimezone = `-- name: GetSystemTimezone :one
|
||||
SELECT setting_value FROM system_settings WHERE setting_key = 'default_timezone'
|
||||
`
|
||||
|
||||
func (q *Queries) GetSystemTimezone(ctx context.Context) (string, error) {
|
||||
row := q.db.QueryRow(ctx, GetSystemTimezone)
|
||||
var setting_value string
|
||||
err := row.Scan(&setting_value)
|
||||
return setting_value, err
|
||||
}
|
||||
|
||||
const GetUniversalProgress = `-- name: GetUniversalProgress :one
|
||||
SELECT
|
||||
rp.id,
|
||||
@@ -5651,6 +5662,7 @@ SELECT
|
||||
u.max_devices,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
u.timezone,
|
||||
(SELECT COUNT(*) FROM devices WHERE user_id = u.id) as device_count
|
||||
FROM users u
|
||||
WHERE u.id = $1
|
||||
@@ -5667,6 +5679,7 @@ type GetUserRow struct {
|
||||
MaxDevices pgtype.Int4 `db:"max_devices" json:"max_devices"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
Timezone pgtype.Text `db:"timezone" json:"timezone"`
|
||||
DeviceCount int64 `db:"device_count" json:"device_count"`
|
||||
}
|
||||
|
||||
@@ -5684,6 +5697,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro
|
||||
&i.MaxDevices,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Timezone,
|
||||
&i.DeviceCount,
|
||||
)
|
||||
return i, err
|
||||
@@ -10256,7 +10270,7 @@ func (q *Queries) UpdateUniversalProgress(ctx context.Context, arg UpdateUnivers
|
||||
|
||||
const UpdateUserMaxDevices = `-- name: UpdateUserMaxDevices :one
|
||||
UPDATE users SET max_devices = $2, updated_at = NOW() WHERE id = $1
|
||||
RETURNING id, email, username, password_hash, first_name, last_name, role, theme, max_devices, created_at, updated_at
|
||||
RETURNING id, email, username, password_hash, first_name, last_name, role, theme, max_devices, created_at, timezone, updated_at
|
||||
`
|
||||
|
||||
type UpdateUserMaxDevicesParams struct {
|
||||
@@ -10278,6 +10292,7 @@ func (q *Queries) UpdateUserMaxDevices(ctx context.Context, arg UpdateUserMaxDev
|
||||
&i.Theme,
|
||||
&i.MaxDevices,
|
||||
&i.CreatedAt,
|
||||
&i.Timezone,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
@@ -10341,6 +10356,20 @@ func (q *Queries) UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateUserTimezone = `-- name: UpdateUserTimezone :exec
|
||||
UPDATE users SET timezone = $2, updated_at = NOW() WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateUserTimezoneParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
Timezone pgtype.Text `db:"timezone" json:"timezone"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserTimezone(ctx context.Context, arg UpdateUserTimezoneParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateUserTimezone, arg.ID, arg.Timezone)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateUsername = `-- name: UpdateUsername :exec
|
||||
UPDATE users SET username = $2, updated_at = NOW() WHERE id = $1
|
||||
`
|
||||
|
||||
@@ -27,6 +27,7 @@ SELECT
|
||||
u.max_devices,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
u.timezone,
|
||||
(SELECT COUNT(*) FROM devices WHERE user_id = u.id) as device_count
|
||||
FROM users u
|
||||
WHERE u.id = $1;
|
||||
@@ -300,6 +301,12 @@ RETURNING *;
|
||||
UPDATE users SET role = $2, updated_at = NOW() WHERE id = $1
|
||||
RETURNING id, email, username, role;
|
||||
|
||||
-- name: UpdateUserTimezone :exec
|
||||
UPDATE users SET timezone = $2, updated_at = NOW() WHERE id = $1;
|
||||
|
||||
-- name: GetSystemTimezone :one
|
||||
SELECT setting_value FROM system_settings WHERE setting_key = 'default_timezone';
|
||||
|
||||
-- name: CountUserDevices :one
|
||||
SELECT COUNT(*) FROM devices WHERE user_id = $1;
|
||||
|
||||
|
||||
@@ -75,18 +75,18 @@ func (h *AnalyticsHandler) GetReadingStats(c *echo.Context) error {
|
||||
endDate := c.QueryParam("end_date")
|
||||
|
||||
if startDate == "" {
|
||||
startDate = time.Now().AddDate(0, -1, 0).Format("2006-01-02")
|
||||
startDate = time.Now().AddDate(0, -1, 0).Format("01-02-2006")
|
||||
}
|
||||
if endDate == "" {
|
||||
endDate = time.Now().Format("2006-01-02")
|
||||
endDate = time.Now().Format("01-02-2006")
|
||||
}
|
||||
|
||||
startTime, err := time.Parse("2006-01-02", startDate)
|
||||
startTime, err := time.Parse("01-02-2006", startDate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid start_date format")
|
||||
}
|
||||
|
||||
endTime, err := time.Parse("2006-01-02", endDate)
|
||||
endTime, err := time.Parse("01-02-2006", endDate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid end_date format")
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func (h *AnalyticsHandler) calculateReadingStats(history []database.GetUserReadi
|
||||
longestSession = int(minutes)
|
||||
}
|
||||
|
||||
dateKey := entry.CreatedAt.Time.Format("2006-01-02")
|
||||
dateKey := entry.CreatedAt.Time.Format("01-02-2006")
|
||||
if dailyMap[dateKey] == nil {
|
||||
dailyMap[dateKey] = &DailyReading{
|
||||
Date: dateKey,
|
||||
@@ -141,7 +141,7 @@ func (h *AnalyticsHandler) calculateReadingStats(history []database.GetUserReadi
|
||||
|
||||
if entry.PagesRead.Valid {
|
||||
totalPages += int(entry.PagesRead.Int32)
|
||||
dateKey := entry.CreatedAt.Time.Format("2006-01-02")
|
||||
dateKey := entry.CreatedAt.Time.Format("01-02-2006")
|
||||
if dailyMap[dateKey] != nil {
|
||||
dailyMap[dateKey].Pages += int(entry.PagesRead.Int32)
|
||||
}
|
||||
@@ -222,7 +222,7 @@ func (h *AnalyticsHandler) GetDeviceUsage(c *echo.Context) error {
|
||||
lastSync := ""
|
||||
if u.LastSync != nil {
|
||||
if t, ok := u.LastSync.(time.Time); ok {
|
||||
lastSync = t.Format("2006-01-02 15:04:05")
|
||||
lastSync = t.Format("01-02-2006 03:04:05 PM")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ func (h *AnalyticsHandler) GetPopularBooks(c *echo.Context) error {
|
||||
lastRead := ""
|
||||
if book.LastRead != nil {
|
||||
if t, ok := book.LastRead.(time.Time); ok {
|
||||
lastRead = t.Format("2006-01-02 15:04:05")
|
||||
lastRead = t.Format("01-02-2006 03:04:05 PM")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ type UpdateProfileRequest struct {
|
||||
FirstName string `json:"first_name,omitempty" validate:"omitempty,max=100"`
|
||||
LastName string `json:"last_name,omitempty" validate:"omitempty,max=100"`
|
||||
Theme string `json:"theme,omitempty" validate:"omitempty"`
|
||||
Timezone string `json:"timezone,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type AdminUpdateUserRequest struct {
|
||||
@@ -95,6 +96,7 @@ type AdminUpdateUserRequest struct {
|
||||
FirstName string `json:"first_name,omitempty" validate:"omitempty,max=100"`
|
||||
LastName string `json:"last_name,omitempty" validate:"omitempty,max=100"`
|
||||
Theme string `json:"theme,omitempty" validate:"omitempty"`
|
||||
Timezone string `json:"timezone,omitempty" validate:"omitempty"`
|
||||
Role string `json:"role,omitempty" validate:"omitempty,oneof=user admin"`
|
||||
}
|
||||
|
||||
@@ -564,6 +566,22 @@ func (h *AuthHandler) UpdateProfile(c *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Update timezone
|
||||
if req.Timezone != "" {
|
||||
if _, err := time.LoadLocation(req.Timezone); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "Invalid timezone",
|
||||
})
|
||||
}
|
||||
err := h.db.UpdateUserTimezone(c.Request().Context(), database.UpdateUserTimezoneParams{
|
||||
ID: targetUserUUID,
|
||||
Timezone: pgtype.Text{String: req.Timezone, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update email (if provided)
|
||||
if req.Email != "" {
|
||||
existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email)
|
||||
|
||||
@@ -306,7 +306,7 @@ func (h *Handler) GetAllProgress(c *echo.Context) error {
|
||||
|
||||
lastUpdated := ""
|
||||
if progress.LastReadAt.Valid {
|
||||
lastUpdated = progress.LastReadAt.Time.Format("2006-01-02 15:04")
|
||||
lastUpdated = progress.LastReadAt.Time.Format("01-02-2006 03:04 PM")
|
||||
}
|
||||
|
||||
progressList = append(progressList, ProgressWithMedia{
|
||||
|
||||
@@ -388,6 +388,23 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
|
||||
|
||||
// Update each config value
|
||||
for key, value := range req {
|
||||
if key == "default_timezone" {
|
||||
if _, err := time.LoadLocation(value); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid timezone",
|
||||
})
|
||||
}
|
||||
err := h.db.UpdateSystemSetting(ctx, database.UpdateSystemSettingParams{
|
||||
SettingKey: "default_timezone",
|
||||
SettingValue: value,
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to update default timezone",
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, err := h.db.SetSystemConfig(ctx, database.SetSystemConfigParams{
|
||||
Key: key,
|
||||
Value: value,
|
||||
@@ -408,6 +425,12 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to fetch updated configuration</div>`)
|
||||
}
|
||||
|
||||
defaultTimezone := "UTC"
|
||||
tz, err := h.db.GetSystemTimezone(ctx)
|
||||
if err == nil && tz != "" {
|
||||
defaultTimezone = tz
|
||||
}
|
||||
|
||||
// Render success message with updated form
|
||||
return c.HTML(http.StatusOK, fmt.Sprintf(`
|
||||
<div class="mb-4 p-4 rounded-lg" style="background-color: var(--bg-secondary); border: 1px solid var(--accent);">
|
||||
@@ -437,6 +460,44 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">System Defaults</h3>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Default Timezone</label>
|
||||
<select name="default_timezone" id="default_timezone" class="w-full px-4 py-2 rounded-lg border" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
||||
<option value="UTC"%s>UTC (UTC+0)</option>
|
||||
<option value="Pacific/Honolulu"%s>Hawaii (UTC-10)</option>
|
||||
<option value="America/Anchorage"%s>Alaska (UTC-9/-8)</option>
|
||||
<option value="America/Los_Angeles"%s>Pacific (UTC-8/-7)</option>
|
||||
<option value="America/Denver"%s>Mountain (UTC-7/-6)</option>
|
||||
<option value="America/Phoenix"%s>Mountain - no DST (UTC-7)</option>
|
||||
<option value="America/Chicago"%s>Central (UTC-6/-5)</option>
|
||||
<option value="America/New_York"%s>Eastern (UTC-5/-4)</option>
|
||||
<option value="America/Sao_Paulo"%s>Brasilia (UTC-3/-2)</option>
|
||||
<option value="Europe/London"%s>British (UTC+0/+1)</option>
|
||||
<option value="Europe/Paris"%s>Central European (UTC+1/+2)</option>
|
||||
<option value="Europe/Helsinki"%s>Eastern European (UTC+2/+3)</option>
|
||||
<option value="Europe/Moscow"%s>Moscow (UTC+3)</option>
|
||||
<option value="Asia/Tehran"%s>Iran (UTC+3:30)</option>
|
||||
<option value="Asia/Dubai"%s>Gulf (UTC+4)</option>
|
||||
<option value="Asia/Karachi"%s>Pakistan (UTC+5)</option>
|
||||
<option value="Asia/Kolkata"%s>India (UTC+5:30)</option>
|
||||
<option value="Asia/Dhaka"%s>Bangladesh (UTC+6)</option>
|
||||
<option value="Asia/Bangkok"%s>Indochina (UTC+7)</option>
|
||||
<option value="Asia/Shanghai"%s>China (UTC+8)</option>
|
||||
<option value="Asia/Tokyo"%s>Japan/Korea (UTC+9)</option>
|
||||
<option value="Australia/Darwin"%s>Australian Central (UTC+9:30)</option>
|
||||
<option value="Australia/Sydney"%s>Australian Eastern (UTC+10/+11)</option>
|
||||
<option value="Pacific/Auckland"%s>New Zealand (UTC+12/+13)</option>
|
||||
</select>
|
||||
<p class="text-sm mt-1" style="color: var(--text-secondary)">Default timezone for users who haven't set their own.</p>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
@@ -447,7 +508,32 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
|
||||
<p><strong>Device Sync:</strong> %s/api/sync</p>
|
||||
</div>
|
||||
</div>
|
||||
`, baseURL.Value, baseURL.Value, baseURL.Value, baseURL.Value))
|
||||
`, baseURL.Value,
|
||||
selectedAttr(defaultTimezone, "UTC"),
|
||||
selectedAttr(defaultTimezone, "Pacific/Honolulu"),
|
||||
selectedAttr(defaultTimezone, "America/Anchorage"),
|
||||
selectedAttr(defaultTimezone, "America/Los_Angeles"),
|
||||
selectedAttr(defaultTimezone, "America/Denver"),
|
||||
selectedAttr(defaultTimezone, "America/Phoenix"),
|
||||
selectedAttr(defaultTimezone, "America/Chicago"),
|
||||
selectedAttr(defaultTimezone, "America/New_York"),
|
||||
selectedAttr(defaultTimezone, "America/Sao_Paulo"),
|
||||
selectedAttr(defaultTimezone, "Europe/London"),
|
||||
selectedAttr(defaultTimezone, "Europe/Paris"),
|
||||
selectedAttr(defaultTimezone, "Europe/Helsinki"),
|
||||
selectedAttr(defaultTimezone, "Europe/Moscow"),
|
||||
selectedAttr(defaultTimezone, "Asia/Tehran"),
|
||||
selectedAttr(defaultTimezone, "Asia/Dubai"),
|
||||
selectedAttr(defaultTimezone, "Asia/Karachi"),
|
||||
selectedAttr(defaultTimezone, "Asia/Kolkata"),
|
||||
selectedAttr(defaultTimezone, "Asia/Dhaka"),
|
||||
selectedAttr(defaultTimezone, "Asia/Bangkok"),
|
||||
selectedAttr(defaultTimezone, "Asia/Shanghai"),
|
||||
selectedAttr(defaultTimezone, "Asia/Tokyo"),
|
||||
selectedAttr(defaultTimezone, "Australia/Darwin"),
|
||||
selectedAttr(defaultTimezone, "Australia/Sydney"),
|
||||
selectedAttr(defaultTimezone, "Pacific/Auckland"),
|
||||
baseURL.Value, baseURL.Value, baseURL.Value))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
@@ -477,3 +563,10 @@ func sanitizeAll(s string, old string, new string) string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func selectedAttr(current, value string) string {
|
||||
if current == value {
|
||||
return " selected"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/labstack/echo/v5"
|
||||
@@ -31,6 +32,28 @@ type ScanSettingsResponse struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateTimezoneSettingsRequest struct {
|
||||
DefaultTimezone string `json:"default_timezone" validate:"required"`
|
||||
}
|
||||
|
||||
func (h *SystemSettingsHandler) UpdateTimezoneSettings(c *echo.Context) error {
|
||||
var req UpdateTimezoneSettingsRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
||||
}
|
||||
if _, err := time.LoadLocation(req.DefaultTimezone); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid timezone"})
|
||||
}
|
||||
err := h.db.UpdateSystemSetting(c.Request().Context(), database.UpdateSystemSettingParams{
|
||||
SettingKey: "default_timezone",
|
||||
SettingValue: req.DefaultTimezone,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"message": "Timezone updated"})
|
||||
}
|
||||
|
||||
func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
|
||||
var req UpdateScanSettingsRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
|
||||
@@ -932,7 +932,13 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
}
|
||||
|
||||
systemConfig := map[string]string{
|
||||
"base_url": baseURL,
|
||||
"base_url": baseURL,
|
||||
"default_timezone": "UTC",
|
||||
}
|
||||
|
||||
defaultTimezone, err := cfg.Queries.GetSystemTimezone(c.Request().Context())
|
||||
if err == nil && defaultTimezone != "" {
|
||||
systemConfig["default_timezone"] = defaultTimezone
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -35,6 +35,11 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
|
||||
userTheme = userDB.Theme.String
|
||||
}
|
||||
|
||||
userTimezone := "UTC"
|
||||
if userDB.Timezone.Valid {
|
||||
userTimezone = userDB.Timezone.String
|
||||
}
|
||||
|
||||
// Extract JWT token for WebSocket authentication
|
||||
token := ""
|
||||
if cookie, err := c.Cookie("token"); err == nil {
|
||||
@@ -48,6 +53,7 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
|
||||
Role: userRole,
|
||||
Theme: userTheme,
|
||||
Token: token,
|
||||
Timezone: userTimezone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user