Compare commits

..

2 Commits

Author SHA1 Message Date
92402f0fdb v3.42.17: Fix systemd service templates - remove invalid --config flag
All checks were successful
CI/CD / Test (push) Successful in 1m15s
CI/CD / Lint (push) Successful in 1m21s
CI/CD / Build & Release (push) Successful in 3m12s
- Service templates now use WorkingDirectory for config loading
- Config is read from .dbbackup.conf in /var/lib/dbbackup
- Updated SYSTEMD.md documentation to match actual CLI
- Removed non-existent --config flag from ExecStart
2026-01-08 11:57:16 +01:00
682510d1bc v3.42.16: TUI cleanup - remove STATUS box, add global styles
All checks were successful
CI/CD / Test (push) Successful in 1m19s
CI/CD / Lint (push) Successful in 1m24s
CI/CD / Build & Release (push) Successful in 3m19s
2026-01-08 11:17:46 +01:00
6 changed files with 186 additions and 49 deletions

View File

@@ -116,8 +116,9 @@ sudo chmod 755 /usr/local/bin/dbbackup
### Step 2: Create Configuration
```bash
# Main configuration
sudo tee /etc/dbbackup/dbbackup.conf << 'EOF'
# Main configuration in working directory (where service runs from)
# dbbackup reads .dbbackup.conf from WorkingDirectory
sudo tee /var/lib/dbbackup/.dbbackup.conf << 'EOF'
# DBBackup Configuration
db-type=postgres
host=localhost
@@ -128,6 +129,8 @@ compression=6
retention-days=30
min-backups=7
EOF
sudo chown dbbackup:dbbackup /var/lib/dbbackup/.dbbackup.conf
sudo chmod 600 /var/lib/dbbackup/.dbbackup.conf
# Instance credentials (secure permissions)
sudo tee /etc/dbbackup/env.d/cluster.conf << 'EOF'
@@ -157,13 +160,15 @@ Group=dbbackup
# Load configuration
EnvironmentFile=-/etc/dbbackup/env.d/cluster.conf
# Working directory
# Working directory (config is loaded from .dbbackup.conf here)
WorkingDirectory=/var/lib/dbbackup
# Execute backup
# Execute backup (reads .dbbackup.conf from WorkingDirectory)
ExecStart=/usr/local/bin/dbbackup backup cluster \
--config /etc/dbbackup/dbbackup.conf \
--backup-dir /var/lib/dbbackup/backups \
--host localhost \
--port 5432 \
--user postgres \
--allow-root
# Security hardening
@@ -443,12 +448,12 @@ sudo systemctl status dbbackup-cluster.service
# View detailed error
sudo journalctl -u dbbackup-cluster.service -n 50 --no-pager
# Test manually as dbbackup user
sudo -u dbbackup /usr/local/bin/dbbackup backup cluster --config /etc/dbbackup/dbbackup.conf
# Test manually as dbbackup user (run from working directory with .dbbackup.conf)
cd /var/lib/dbbackup && sudo -u dbbackup /usr/local/bin/dbbackup backup cluster
# Check permissions
ls -la /var/lib/dbbackup/
ls -la /etc/dbbackup/
ls -la /var/lib/dbbackup/.dbbackup.conf
```
### Permission Denied

View File

@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
## Build Information
- **Version**: 3.42.10
- **Build Time**: 2026-01-08_09:40:57_UTC
- **Git Commit**: 55d34be
- **Build Time**: 2026-01-08_10:18:23_UTC
- **Git Commit**: 682510d
## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output

View File

@@ -33,8 +33,11 @@ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# Environment
EnvironmentFile=-/etc/dbbackup/env.d/cluster.conf
# Working directory (config is loaded from .dbbackup.conf here)
WorkingDirectory=/var/lib/dbbackup
# Execution - cluster backup (all databases)
ExecStart={{.BinaryPath}} backup cluster --config {{.ConfigPath}}
ExecStart={{.BinaryPath}} backup cluster --backup-dir {{.BackupDir}}
TimeoutStartSec={{.TimeoutSeconds}}
# Post-backup metrics export

View File

@@ -33,8 +33,11 @@ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# Environment
EnvironmentFile=-/etc/dbbackup/env.d/%i.conf
# Working directory (config is loaded from .dbbackup.conf here)
WorkingDirectory=/var/lib/dbbackup
# Execution
ExecStart={{.BinaryPath}} backup {{.BackupType}} %i --config {{.ConfigPath}}
ExecStart={{.BinaryPath}} backup {{.BackupType}} %i --backup-dir {{.BackupDir}}
TimeoutStartSec={{.TimeoutSeconds}}
# Post-backup metrics export

View File

@@ -9,7 +9,6 @@ import (
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-runewidth"
"dbbackup/internal/config"
"dbbackup/internal/logger"
@@ -229,72 +228,66 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m BackupManagerModel) View() string {
var s strings.Builder
const boxWidth = 60
// Helper to pad string to box width (handles UTF-8)
padToWidth := func(text string, width int) string {
textWidth := runewidth.StringWidth(text)
if textWidth >= width {
return runewidth.Truncate(text, width-3, "...")
}
return text + strings.Repeat(" ", width-textWidth)
}
// Title
s.WriteString(titleStyle.Render("[DB] Backup Archive Manager"))
s.WriteString(TitleStyle.Render("[DB] Backup Archive Manager"))
s.WriteString("\n\n")
// Operation Status Box (always visible)
s.WriteString("+--[ STATUS ]" + strings.Repeat("-", boxWidth-13) + "+\n")
// Status line (no box, bold+color accents)
switch m.opState {
case OpVerifying:
spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Verifying: %s", spinner, m.opTarget)
s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n")
s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Verifying: %s", spinner, m.opTarget)))
s.WriteString("\n\n")
case OpDeleting:
spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Deleting: %s", spinner, m.opTarget)
s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n")
s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Deleting: %s", spinner, m.opTarget)))
s.WriteString("\n\n")
default:
if m.loading {
spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Loading archives...", spinner)
s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n")
s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Loading archives...", spinner)))
s.WriteString("\n\n")
} else if m.message != "" {
msgText := " " + m.message
s.WriteString("|" + padToWidth(msgText, boxWidth) + "|\n")
// Color based on message content
if strings.HasPrefix(m.message, "[+]") || strings.HasPrefix(m.message, "Valid") {
s.WriteString(StatusSuccessStyle.Render(m.message))
} else if strings.HasPrefix(m.message, "[-]") || strings.HasPrefix(m.message, "Error") {
s.WriteString(StatusErrorStyle.Render(m.message))
} else {
s.WriteString("|" + padToWidth(" Ready", boxWidth) + "|\n")
s.WriteString(StatusActiveStyle.Render(m.message))
}
s.WriteString("\n\n")
}
// No "Ready" message when idle - cleaner UI
}
s.WriteString("+" + strings.Repeat("-", boxWidth) + "+\n\n")
if m.loading {
return s.String()
}
if m.err != nil {
s.WriteString(errorStyle.Render(fmt.Sprintf("[FAIL] Error: %v", m.err)))
s.WriteString(StatusErrorStyle.Render(fmt.Sprintf("[FAIL] Error: %v", m.err)))
s.WriteString("\n\n")
s.WriteString(infoStyle.Render("Press Esc to go back"))
s.WriteString(ShortcutStyle.Render("Press Esc to go back"))
return s.String()
}
// Summary
s.WriteString(infoStyle.Render(fmt.Sprintf("Total Archives: %d | Total Size: %s",
s.WriteString(LabelStyle.Render(fmt.Sprintf("Total Archives: %d | Total Size: %s",
len(m.archives), formatSize(m.totalSize))))
s.WriteString("\n\n")
// Archives list
if len(m.archives) == 0 {
s.WriteString(infoStyle.Render("No backup archives found"))
s.WriteString(StatusReadyStyle.Render("No backup archives found"))
s.WriteString("\n\n")
s.WriteString(infoStyle.Render("Press Esc to go back"))
s.WriteString(ShortcutStyle.Render("Press Esc to go back"))
return s.String()
}
// Column headers with better alignment
s.WriteString(archiveHeaderStyle.Render(fmt.Sprintf(" %-32s %-22s %10s %-16s",
s.WriteString(ListHeaderStyle.Render(fmt.Sprintf(" %-32s %-22s %10s %-16s",
"FILENAME", "FORMAT", "SIZE", "MODIFIED")))
s.WriteString("\n")
s.WriteString(strings.Repeat("-", 90))
@@ -313,18 +306,18 @@ func (m BackupManagerModel) View() string {
for i := start; i < end; i++ {
archive := m.archives[i]
cursor := " "
style := archiveNormalStyle
style := ListNormalStyle
if i == m.cursor {
cursor = "> "
style = archiveSelectedStyle
style = ListSelectedStyle
}
// Status icon - consistent 4-char width
statusIcon := " [+]"
if !archive.Valid {
statusIcon = " [-]"
style = archiveInvalidStyle
style = ItemInvalidStyle
} else if time.Since(archive.Modified) > 30*24*time.Hour {
statusIcon = " [!]"
}
@@ -347,11 +340,11 @@ func (m BackupManagerModel) View() string {
// Footer
s.WriteString("\n")
s.WriteString(infoStyle.Render(fmt.Sprintf("Selected: %d/%d", m.cursor+1, len(m.archives))))
s.WriteString(StatusReadyStyle.Render(fmt.Sprintf("Selected: %d/%d", m.cursor+1, len(m.archives))))
s.WriteString("\n\n")
// Grouped keyboard shortcuts - simple aligned format
s.WriteString("SHORTCUTS: Up/Down=Move | r=Restore | v=Verify | d=Delete | i=Info | R=Refresh | Esc=Back | q=Quit")
// Grouped keyboard shortcuts
s.WriteString(ShortcutStyle.Render("SHORTCUTS: Up/Down=Move | r=Restore | v=Verify | d=Delete | i=Info | R=Refresh | Esc=Back | q=Quit"))
return s.String()
}

133
internal/tui/styles.go Normal file
View File

@@ -0,0 +1,133 @@
package tui
import "github.com/charmbracelet/lipgloss"
// =============================================================================
// GLOBAL TUI STYLE DEFINITIONS
// =============================================================================
// Design Language:
// - Bold text for labels and headers
// - Colors for semantic meaning (green=success, red=error, yellow=warning)
// - No emoticons - use simple text prefixes like [OK], [FAIL], [!]
// - No boxes for inline status - use bold+color accents
// - Consistent color palette across all views
// =============================================================================
// Color Palette (ANSI 256 colors for terminal compatibility)
const (
ColorWhite = lipgloss.Color("15") // Bright white
ColorGray = lipgloss.Color("250") // Light gray
ColorDim = lipgloss.Color("244") // Dim gray
ColorDimmer = lipgloss.Color("240") // Darker gray
ColorSuccess = lipgloss.Color("2") // Green
ColorError = lipgloss.Color("1") // Red
ColorWarning = lipgloss.Color("3") // Yellow
ColorInfo = lipgloss.Color("6") // Cyan
ColorAccent = lipgloss.Color("4") // Blue
)
// =============================================================================
// TITLE & HEADER STYLES
// =============================================================================
// TitleStyle - main view title (bold white on gray background)
var TitleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWhite).
Background(ColorDimmer).
Padding(0, 1)
// HeaderStyle - section headers (bold gray)
var HeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorDim)
// LabelStyle - field labels (bold cyan)
var LabelStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorInfo)
// =============================================================================
// STATUS STYLES
// =============================================================================
// StatusReadyStyle - idle/ready state (dim)
var StatusReadyStyle = lipgloss.NewStyle().
Foreground(ColorDim)
// StatusActiveStyle - operation in progress (bold cyan)
var StatusActiveStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorInfo)
// StatusSuccessStyle - success messages (bold green)
var StatusSuccessStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorSuccess)
// StatusErrorStyle - error messages (bold red)
var StatusErrorStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorError)
// StatusWarningStyle - warning messages (bold yellow)
var StatusWarningStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWarning)
// =============================================================================
// LIST & TABLE STYLES
// =============================================================================
// ListNormalStyle - unselected list items
var ListNormalStyle = lipgloss.NewStyle().
Foreground(ColorGray)
// ListSelectedStyle - selected/cursor item (bold white)
var ListSelectedStyle = lipgloss.NewStyle().
Foreground(ColorWhite).
Bold(true)
// ListHeaderStyle - column headers (bold dim)
var ListHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorDim)
// =============================================================================
// ITEM STATUS STYLES
// =============================================================================
// ItemValidStyle - valid/OK items (green)
var ItemValidStyle = lipgloss.NewStyle().
Foreground(ColorSuccess)
// ItemInvalidStyle - invalid/failed items (red)
var ItemInvalidStyle = lipgloss.NewStyle().
Foreground(ColorError)
// ItemOldStyle - old/stale items (yellow)
var ItemOldStyle = lipgloss.NewStyle().
Foreground(ColorWarning)
// =============================================================================
// SHORTCUT STYLE
// =============================================================================
// ShortcutStyle - keyboard shortcuts footer (dim)
var ShortcutStyle = lipgloss.NewStyle().
Foreground(ColorDim)
// =============================================================================
// HELPER PREFIXES (no emoticons)
// =============================================================================
const (
PrefixOK = "[OK]"
PrefixFail = "[FAIL]"
PrefixWarn = "[!]"
PrefixInfo = "[i]"
PrefixPlus = "[+]"
PrefixMinus = "[-]"
PrefixArrow = ">"
PrefixSpinner = "" // Spinner character added dynamically
)