refactor: use slog instead of log

This commit is contained in:
Patryk Hegenberg 2025-06-11 22:41:48 +02:00
parent fcffccc145
commit 5a8537f911
8 changed files with 180 additions and 165 deletions

101
app.go
View file

@ -2,7 +2,7 @@ package main
import (
"fmt"
"log"
"log/slog"
"os"
"os/exec"
"strings"
@ -44,7 +44,7 @@ func (a *App) Close() error {
func (a *App) connect() (*SSHConnection, error) { // Rückgabetyp geändert
if err := a.timeStore.StartTracking(TagWork); err != nil {
log.Printf("WARN: Failed to start time tracking for '%s': %v", TagWork, err)
slog.Warn(fmt.Sprintf("Failed to start time tracking for '%s': %v", TagWork, err))
}
a.wakeWorkstation()
@ -54,24 +54,31 @@ func (a *App) connect() (*SSHConnection, error) { // Rückgabetyp geändert
return nil, fmt.Errorf("failed to establish primary SSH connection: %w", err)
}
log.Println("INFO: SSH connection established. Setting up tunnels...")
// slog.Info("SSH connection established. Setting up tunnels...")
slog.Info("SSH connection established. Setting up tunnels...")
sshForwarder := NewPortForwarder(sshCon.client, "2048", "22", a.cfg.WorkstationIP)
go func() {
log.Println("INFO: Starting SSH forwarder (local :2048 -> remote workstation:22)")
// slog.Info("Starting SSH forwarder (local :2048 -> remote workstation:22)")
slog.Info("Starting SSH forwarder (local :2048 -> remote workstation:22)")
if err := sshForwarder.forward(); err != nil {
log.Printf("ERROR: SSH forwarder failed: %v", err)
// slog.Error(fmt.Sprintf("SSH forwarder failed: %v", err)
slog.Error(fmt.Sprintf("SSH forwarder failed: %v", err))
}
log.Println("INFO: SSH forwarder stopped.")
// slog.Info("SSH forwarder stopped.")
slog.Info("SSH forwarder stopped.")
}()
rdpForwarder := NewPortForwarder(sshCon.client, "6000", "3389", a.cfg.WorkstationIP)
go func() {
log.Println("INFO: Starting RDP forwarder (local :6000 -> remote workstation:3389)")
// slog.Info("Starting RDP forwarder (local :6000 -> remote workstation:3389)")
slog.Info("Starting RDP forwarder (local :6000 -> remote workstation:3389)")
if err := rdpForwarder.forward(); err != nil {
log.Printf("ERROR: RDP forwarder failed: %v", err)
// slog.Error(fmt.Sprintf("RDP forwarder failed: %v", err)
slog.Error(fmt.Sprintf("ERROR: RDP forwarder failed: %v", err))
}
log.Println("INFO: RDP forwarder stopped.")
// slog.Info("RDP forwarder stopped.")
slog.Info("RDP forwarder stopped.")
}()
time.Sleep(500 * time.Millisecond)
@ -80,22 +87,22 @@ func (a *App) connect() (*SSHConnection, error) { // Rückgabetyp geändert
}
func (a *App) runCommand(name string, args ...string) error {
log.Printf("INFO: Executing command: %s %s", name, strings.Join(args, " "))
slog.Info(fmt.Sprintf("Executing command: %s %s", name, strings.Join(args, " ")))
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err := cmd.Run()
if err != nil {
log.Printf("ERROR: Command failed: %s %s -> %v", name, strings.Join(args, " "), err)
slog.Error(fmt.Sprintf("Command failed: %s %s -> %v", name, strings.Join(args, " "), err))
return fmt.Errorf("command execution failed: %w", err)
}
log.Printf("INFO: Command finished successfully: %s", name)
slog.Info(fmt.Sprintf("Command finished successfully: %s", name))
return nil
}
func (a *App) wakeWorkstation() {
log.Println("INFO: Attempting to wake workstation...")
slog.Info("Attempting to wake workstation...")
innerSSHCmd := fmt.Sprintf("ssh -tt %s@%s \"wakeonlan %s && echo 'Wake-on-LAN packet sent.' && exit\"",
a.cfg.JumpUser,
a.cfg.JumpHost,
@ -109,14 +116,14 @@ func (a *App) wakeWorkstation() {
}
if err := a.runCommand("ssh", outerSSHCmd...); err != nil {
log.Println("WARN: Failed to send Wake-on-LAN packet via SSH jump. Workstation might already be awake or command failed.")
slog.Warn("Failed to send Wake-on-LAN packet via SSH jump. Workstation might already be awake or command failed.")
} else {
log.Println("INFO: Wake-on-LAN command executed.")
slog.Info("Wake-on-LAN command executed.")
}
}
func (a *App) connectToJump() {
log.Println("INFO: Connecting to Jump Host with Port Forwarding...")
slog.Info("Connecting to Jump Host with Port Forwarding...")
sshArgs := []string{
"-tt",
"-L", fmt.Sprintf("2048:%s:22", a.cfg.WorkstationHost),
@ -128,7 +135,7 @@ func (a *App) connectToJump() {
}
func (a *App) connectToWorkstation() {
log.Println("INFO: Connecting to Workstation via local tunnel (localhost:2048)...")
slog.Info("Connecting to Workstation via local tunnel (localhost:2048)...")
sshArgs := []string{
"-tt",
"-L", fmt.Sprintf("6000:%s:3389", a.cfg.WorkstationHost),
@ -140,7 +147,7 @@ func (a *App) connectToWorkstation() {
}
func (a *App) startRDPConnection() {
log.Println("INFO: Starting RDP connection to localhost:6000...")
slog.Info("Starting RDP connection to localhost:6000...")
rdpCommand := fmt.Sprintf("xfreerdp /u:%s /p:%s /v:127.0.0.1:6000 /size:3000x1350 +clipboard /dynamic-resolution",
a.cfg.RDPUser,
a.cfg.SSHPassword,
@ -182,7 +189,7 @@ func (a *App) makeChoice() {
fmt.Println("Operation cancelled.")
return
}
log.Printf("ERROR: Form execution failed: %v", err)
slog.Error(fmt.Sprintf("Form execution failed: %v", err))
return
}
@ -191,30 +198,30 @@ func (a *App) makeChoice() {
a.connect()
case "stop work":
if err := a.timeStore.StopTracking(); err != nil {
log.Printf("ERROR: Failed to stop time tracking: %v", err)
slog.Error(fmt.Sprintf("Failed to stop time tracking: %v", err))
}
if err := a.killForwardings(); err != nil {
log.Printf("WARN: Could not kill all forwardings: %v", err)
slog.Warn(fmt.Sprintf("Could not kill all forwardings: %v", err))
}
case "start break":
if err := a.timeStore.StartTracking(TagBreak); err != nil {
log.Printf("ERROR: Failed to start break tracking: %v", err)
slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err))
}
case "stop break":
if err := a.timeStore.StartTracking(TagWork); err != nil {
log.Printf("ERROR: Failed to stop break (start work): %v", err)
slog.Error(fmt.Sprintf("Failed to stop break (start work): %v", err))
}
case "show day summary":
if err := a.timeStore.ShowSummary("today"); err != nil {
log.Printf("ERROR: Failed to show day summary: %v", err)
slog.Error(fmt.Sprintf("Failed to show day summary: %v", err))
}
case "show week summary":
if err := a.timeStore.ShowSummary("week"); err != nil {
log.Printf("ERROR: Failed to show week summary: %v", err)
slog.Error(fmt.Sprintf("ERROR: Failed to show week summary: %v", err))
}
case "show month summary":
if err := a.timeStore.ShowSummary("month"); err != nil {
log.Printf("ERROR: Failed to show month summary: %v", err)
slog.Error(fmt.Sprintf("Failed to show month summary: %v", err))
}
case "export":
filename := "Arbeitszeiten_" + time.Now().Format("2006") + ".xlsx"
@ -222,7 +229,7 @@ func (a *App) makeChoice() {
filename = a.flags.ExportName
}
if err := a.timeStore.ExportSummary(filename); err != nil {
log.Printf("ERROR: Failed to export summary to '%s': %v", filename, err)
slog.Error(fmt.Sprintf("Failed to export summary to '%s': %v", filename, err))
}
case "connect to jump":
a.connectToJump()
@ -234,15 +241,15 @@ func (a *App) makeChoice() {
a.wakeWorkstation()
case "kill tunnels":
if err := a.killForwardings(); err != nil {
log.Printf("ERROR: Failed to kill forwardings: %v", err)
slog.Error(fmt.Sprintf("Failed to kill forwardings: %v", err))
} else {
log.Println("INFO: Attempted to kill processes on ports 2048 and 6000.")
slog.Info("Attempted to kill processes on ports 2048 and 6000.")
}
case "exit":
fmt.Println("Exiting.")
return
default:
log.Printf("WARN: Unhandled choice '%s'", choice)
slog.Warn(fmt.Sprintf("Unhandled choice '%s'", choice))
}
if choice != "exit" && choice != "connect to jump" && choice != "connect to workstation" && choice != "start rdp connection" {
@ -257,7 +264,7 @@ func (a *App) getSSHAuth() ssh.AuthMethod {
keyBytes, err := os.ReadFile(keyPath)
if err != nil {
log.Printf("ERROR: Unable to read private key '%s': %v", keyPath, err)
slog.Error(fmt.Sprintf("Unable to read private key '%s': %v", keyPath, err))
return nil
}
@ -265,19 +272,19 @@ func (a *App) getSSHAuth() ssh.AuthMethod {
key, err = ssh.ParsePrivateKey(keyBytes)
if err != nil {
if _, ok := err.(*ssh.PassphraseMissingError); ok {
log.Printf("INFO: Private key '%s' requires a passphrase. Trying with RDP password from config.", keyPath)
slog.Info(fmt.Sprintf("Private key '%s' requires a passphrase. Trying with RDP password from config.", keyPath))
key, err = ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(a.cfg.RDPPassword))
if err != nil {
log.Printf("ERROR: Unable to parse private key '%s' with passphrase: %v", keyPath, err)
slog.Error(fmt.Sprintf("Unable to parse private key '%s' with passphrase: %v", keyPath, err))
return nil
}
} else {
log.Printf("ERROR: Unable to parse private key '%s': %v", keyPath, err)
slog.Error(fmt.Sprintf("Unable to parse private key '%s': %v", keyPath, err))
return nil
}
}
log.Printf("INFO: Successfully loaded private key '%s'", keyPath)
slog.Info(fmt.Sprintf("Successfully loaded private key '%s'", keyPath))
return ssh.PublicKeys(key)
}
@ -295,13 +302,13 @@ func (a *App) newSSHConnection() (*SSHConnection, error) {
}
target := fmt.Sprintf("%s:%d", a.cfg.SSHHost, a.cfg.SSHPort)
log.Printf("INFO: Dialing SSH to %s...", target)
slog.Info(fmt.Sprintf("Dialing SSH to %s...", target))
client, err := ssh.Dial("tcp", target, sshConfig)
if err != nil {
return nil, fmt.Errorf("SSH dial to %s failed: %w", target, err)
}
log.Printf("INFO: SSH connection to %s successful.", target)
slog.Info(fmt.Sprintf("SSH connection to %s successful.", target))
session, err := client.NewSession()
if err != nil {
@ -320,16 +327,16 @@ func (a *App) killForwardings() error {
killedSomething := false
var lastErr error
log.Println("INFO: Attempting to kill processes listening on ports:", strings.Join(ports, ", "))
slog.Info(fmt.Sprintf("Attempting to kill processes listening on ports: %v", strings.Join(ports, ", ")))
for _, port := range ports {
cmd := exec.Command("lsof", "-i", "tcp:"+port, "-t")
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
log.Printf("INFO: No process found listening on port %s.", port)
slog.Info(fmt.Sprintf("No process found listening on port %s.", port))
} else {
log.Printf("WARN: 'lsof' command failed for port %s: %v", port, err)
slog.Warn(fmt.Sprintf("'lsof' command failed for port %s: %v", port, err))
lastErr = fmt.Errorf("lsof failed for port %s: %w", port, err)
}
continue
@ -341,29 +348,29 @@ func (a *App) killForwardings() error {
if pid == "" {
continue
}
log.Printf("INFO: Found process PID %s on port %s. Attempting to kill...", pid, port)
slog.Info(fmt.Sprintf("Found process PID %s on port %s. Attempting to kill...", pid, port))
killCmd := exec.Command("kill", pid)
if err := killCmd.Run(); err != nil {
log.Printf("WARN: Failed to kill PID %s (port %s): %v. Trying kill -9...", pid, port, err)
slog.Warn(fmt.Sprintf("Failed to kill PID %s (port %s): %v. Trying kill -9...", pid, port, err))
forceKillCmd := exec.Command("kill", "-9", pid)
if err := forceKillCmd.Run(); err != nil {
log.Printf("ERROR: Failed to force kill PID %s (port %s): %v", pid, port, err)
slog.Error(fmt.Sprintf("Failed to force kill PID %s (port %s): %v", pid, port, err))
lastErr = fmt.Errorf("kill -9 failed for PID %s: %w", pid, err)
} else {
log.Printf("INFO: Force killed PID %s (port %s).", pid, port)
slog.Info(fmt.Sprintf("Force killed PID %s (port %s).", pid, port))
killedSomething = true
}
} else {
log.Printf("INFO: Killed PID %s (port %s).", pid, port)
slog.Info(fmt.Sprintf("Killed PID %s (port %s).", pid, port))
killedSomething = true
}
}
}
if killedSomething {
log.Println("INFO: Finished attempting to kill forwarding processes.")
slog.Info("Finished attempting to kill forwarding processes.")
} else {
log.Println("INFO: No forwarding processes found or killed.")
slog.Info("No forwarding processes found or killed.")
}
return lastErr

134
cmd.go
View file

@ -2,7 +2,7 @@ package main
import (
"fmt"
"log"
"log/slog"
"os"
"os/signal"
"strings"
@ -51,11 +51,11 @@ Use --background (-b) to keep tunnels running in the background without auto-con
}
defer func() {
log.Println("INFO: Closing SSH connection to jump host (defer)...")
slog.Info("Closing SSH connection to jump host (defer)...")
if err := sshCon.Close(); err != nil {
log.Printf("WARN: Error closing SSH connection in defer: %v", err)
slog.Warn(fmt.Sprintf("Error closing SSH connection in defer: %v", err))
} else {
log.Println("INFO: SSH connection closed via defer.")
slog.Info("SSH connection closed via defer.")
}
}()
@ -70,11 +70,11 @@ Use --background (-b) to keep tunnels running in the background without auto-con
<-sigChan
fmt.Println("\nINFO: Received interrupt signal. Shutting down background process...")
log.Println("INFO: Received signal, cleanup via defer sshCon.Close() will run.")
slog.Info("Received signal, cleanup via defer sshCon.Close() will run.")
if err := a.timeStore.StopTracking(); err != nil {
log.Printf("WARN: Failed to stop time tracking: %v", err)
slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else {
log.Println("INFO: Time tracking stopped.")
slog.Info("Time tracking stopped.")
}
fmt.Println("INFO: Background shutdown complete.")
@ -83,11 +83,11 @@ Use --background (-b) to keep tunnels running in the background without auto-con
a.connectToWorkstation()
fmt.Println("INFO: Workstation SSH session finished.")
log.Println("INFO: Foreground session ended, cleanup via defer sshCon.Close() will run.")
slog.Info("Foreground session ended, cleanup via defer sshCon.Close() will run.")
if err := a.timeStore.StopTracking(); err != nil {
log.Printf("WARN: Failed to stop time tracking: %v", err)
slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else {
log.Println("INFO: Time tracking stopped.")
slog.Info("Time tracking stopped.")
}
}
@ -108,13 +108,13 @@ func (a *App) stopCommand() *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Stopping workday procedures...")
if err := a.timeStore.StopTracking(); err != nil {
log.Printf("ERROR: Failed to stop time tracking: %v", err)
slog.Error(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else {
fmt.Println("Time tracking stopped.")
}
if err := a.killForwardings(); err != nil {
log.Printf("WARN: Could not kill all forwarding processes: %v", err)
slog.Warn(fmt.Sprintf("Could not kill all forwarding processes: %v", err))
} else {
fmt.Println("Attempted to stop SSH tunnels.")
}
@ -160,7 +160,7 @@ This also stops any currently running timer.`,
default:
fmt.Printf("Attempting to start tracking interval '%s'...\n", tag)
if err := a.timeStore.StartTracking(tag); err != nil {
log.Printf("ERROR: Failed to start tracking '%s': %v", tag, err)
slog.Error(fmt.Sprintf("Failed to start tracking '%s': %v", tag, err))
return fmt.Errorf("could not start tracking '%s': %w", tag, err)
}
return nil // Erfolg
@ -174,7 +174,7 @@ This also stops any currently running timer.`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Starting break...")
if err := a.timeStore.StartTracking(TagBreak); err != nil {
log.Printf("ERROR: Failed to start break tracking: %v", err)
slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err))
return fmt.Errorf("could not start break: %w", err)
}
return nil
@ -204,27 +204,27 @@ Export: Use the --export flag or the 'export' subcommand.`,
filename := a.flags.ExportName
if filename == "" || filename == "Arbeitszeiten.xlsx" {
filename = "Arbeitszeiten_" + time.Now().Format("2006") + ".xlsx"
log.Printf("INFO: No export name specified, using default: %s", filename)
slog.Info(fmt.Sprintf("No export name specified, using default: %s", filename))
}
fmt.Printf("Exporting yearly timetable to '%s'...\n", filename)
if err := a.timeStore.ExportSummary(filename); err != nil {
log.Printf("ERROR: Failed to export summary to '%s': %v", filename, err)
slog.Error(fmt.Sprintf("Failed to export summary to '%s': %v", filename, err))
fmt.Printf("Error: Could not export to '%s'.\n", filename)
}
} else if a.flags.ShowWeek {
fmt.Println("Showing weekly summary...")
if err := a.timeStore.ShowSummary("week"); err != nil {
log.Printf("ERROR: Failed to show week summary: %v", err)
slog.Error(fmt.Sprintf("Failed to show week summary: %v", err))
}
} else if a.flags.ShowMonth {
fmt.Println("Showing monthly summary...")
if err := a.timeStore.ShowSummary("month"); err != nil {
log.Printf("ERROR: Failed to show month summary: %v", err)
slog.Error(fmt.Sprintf("Failed to show month summary: %v", err))
}
} else {
fmt.Printf("Showing summary for period: %s...\n", period)
if err := a.timeStore.ShowSummary(period); err != nil {
log.Printf("ERROR: Failed to show summary for '%s': %v", period, err)
slog.Error(fmt.Sprintf("Failed to show summary for '%s': %v", period, err))
}
}
},
@ -248,7 +248,7 @@ Export: Use the --export flag or the 'export' subcommand.`,
}
fmt.Printf("Exporting yearly timetable to '%s'...\n", filename)
if err := a.timeStore.ExportSummary(filename); err != nil {
log.Printf("ERROR: Failed to export summary to '%s': %v", filename, err)
slog.Error(fmt.Sprintf("Failed to export summary to '%s': %v", filename, err))
fmt.Printf("Error: Could not export to '%s'.\n", filename)
}
},
@ -321,12 +321,12 @@ Example: workctl import-timew /path/to/timew-summary.txt`,
count, err := a.runImport(filepath)
if err != nil {
log.Printf("ERROR: Import failed: %v", err)
slog.Error(fmt.Sprintf("Import failed: %v", err))
return fmt.Errorf("import failed: %w", err)
}
fmt.Printf("Successfully imported %d time entries.\n", count)
log.Printf("INFO: Successfully imported %d time entries from %s", count, filepath)
slog.Info(fmt.Sprintf("Successfully imported %d time entries from %s", count, filepath))
return nil
},
}
@ -353,8 +353,8 @@ func (a *App) runImport(filepath string) (int, error) {
}
defer stmt.Close()
var current_date_str string
imported_count := 0
var currentDateStr string
importedCount := 0
location := time.Local
for i, line := range lines {
@ -364,94 +364,94 @@ func (a *App) runImport(filepath string) (int, error) {
}
fields := strings.Fields(line)
var tag, start_str, end_str string
has_date := false
var tag, startStr, endStr string
hasDate := false
if len(fields) >= 7 && strings.Contains(fields[1], "-") && len(fields[1]) == 10 {
current_date_str = fields[1]
currentDateStr = fields[1]
tag = fields[3]
start_str = fields[4]
end_str = fields[5]
has_date = true
startStr = fields[4]
endStr = fields[5]
hasDate = true
} else if len(fields) >= 4 && strings.Contains(fields[1], ":") && strings.Contains(fields[2], ":") {
if current_date_str == "" {
log.Printf("WARN: Skipping line without preceding date: %s", line)
if currentDateStr == "" {
slog.Warn(fmt.Sprintf("Skipping line without preceding date: %s", line))
continue
}
tag = fields[0]
start_str = fields[1]
end_str = fields[2]
has_date = false
startStr = fields[1]
endStr = fields[2]
hasDate = false
} else if len(fields) >= 6 && strings.Contains(fields[1], "-") && len(fields[1]) == 10 {
current_date_str = fields[1]
currentDateStr = fields[1]
tag = fields[3]
start_str = fields[4]
end_str = fields[5]
has_date = true
if start_str == "0:00:00" && end_str == "0:00:00" {
start_time, err := time.ParseInLocation("2006-01-02", current_date_str, location)
startStr = fields[4]
endStr = fields[5]
hasDate = true
if startStr == "0:00:00" && endStr == "0:00:00" {
startTime, err := time.ParseInLocation("2006-01-02", currentDateStr, location)
if err != nil {
log.Printf("WARN: Skipping line with invalid date '%s': %v", current_date_str, err)
slog.Warn(fmt.Sprintf("Skipping line with invalid date '%s': %v", currentDateStr, err))
continue
}
end_time := start_time.Add(24 * time.Hour)
endTime := startTime.Add(24 * time.Hour)
_, err = stmt.Exec(tag, start_time, end_time)
_, err = stmt.Exec(tag, startTime, endTime)
if err != nil {
log.Printf("ERROR: Failed to insert full-day entry for %s (%s): %v", current_date_str, tag, err)
slog.Error(fmt.Sprintf("Failed to insert full-day entry for %s (%s): %v", currentDateStr, tag, err))
} else {
imported_count++
importedCount++
}
continue
}
} else {
log.Printf("WARN: Skipping unrecognized line format: %s", line)
slog.Warn(fmt.Sprintf("Skipping unrecognized line format: %s", line))
continue
}
if end_str == "-" {
log.Printf("INFO: Skipping currently running entry: %s", line)
if endStr == "-" {
slog.Info(fmt.Sprintf("Skipping currently running entry: %s", line))
continue
}
start_datetime_str := current_date_str + " " + start_str
end_datetime_str := current_date_str + " " + end_str
startDatetimeStr := currentDateStr + " " + startStr
endDatetimeStr := currentDateStr + " " + endStr
start_time, err_start := time.ParseInLocation("2006-01-02 15:04:05", start_datetime_str, location)
end_time, err_end := time.ParseInLocation("2006-01-02 15:04:05", end_datetime_str, location)
startTime, errStart := time.ParseInLocation("2006-01-02 15:04:05", startDatetimeStr, location)
endTime, errEnd := time.ParseInLocation("2006-01-02 15:04:05", endDatetimeStr, location)
if err_start != nil || err_end != nil {
log.Printf("WARN: Skipping line with invalid date/time format ('%s' / '%s'): %v / %v", start_datetime_str, end_datetime_str, err_start, err_end)
if errStart != nil || errEnd != nil {
slog.Warn(fmt.Sprintf("Skipping line with invalid date/time format ('%s' / '%s'): %v / %v", startDatetimeStr, endDatetimeStr, errStart, errEnd))
continue
}
if end_time.Before(start_time) {
if has_date {
log.Printf("WARN: End time is before start time on the same date line, skipping: %s", line)
if endTime.Before(startTime) {
if hasDate {
slog.Warn(fmt.Sprintf("End time is before start time on the same date line, skipping: %s", line))
continue
}
}
db_tag := strings.ToLower(tag)
switch db_tag {
dbTag := strings.ToLower(tag)
switch dbTag {
case "work":
db_tag = TagWork
dbTag = TagWork
case "break":
db_tag = TagBreak
dbTag = TagBreak
}
_, err = stmt.Exec(db_tag, start_time, end_time)
_, err = stmt.Exec(dbTag, startTime, endTime)
if err != nil {
log.Printf("ERROR: Failed to insert entry for %s (%s, %s -> %s): %v", current_date_str, db_tag, start_time.Format(time.RFC3339), end_time.Format(time.RFC3339), err)
slog.Error(fmt.Sprintf("Failed to insert entry for %s (%s, %s -> %s): %v", currentDateStr, dbTag, startTime.Format(time.RFC3339), endTime.Format(time.RFC3339), err))
} else {
imported_count++
importedCount++
}
}
if err := tx.Commit(); err != nil {
return imported_count, fmt.Errorf("failed to commit transaction: %w", err)
return importedCount, fmt.Errorf("failed to commit transaction: %w", err)
}
return imported_count, nil
return importedCount, nil
}

View file

@ -2,7 +2,7 @@ package main
import (
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
@ -43,7 +43,7 @@ func loadConfig() (Config, error) {
workConfigPath := filepath.Join(configPath, "work")
configFile := filepath.Join(workConfigPath, "config.toml")
if err := os.MkdirAll(workConfigPath, 0750); err != nil {
if err := os.MkdirAll(workConfigPath, 0o750); err != nil {
return cfg, fmt.Errorf("could not create config directory '%s': %w", workConfigPath, err)
}
@ -55,7 +55,7 @@ func loadConfig() (Config, error) {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return cfg, fmt.Errorf("error reading config file '%s': %w", configFile, err)
}
log.Printf("INFO: Config file '%s' not found, using defaults/env vars.", configFile)
slog.Info(fmt.Sprintf("Config file '%s' not found, using defaults/env vars.", configFile))
}
if err := viper.UnmarshalKey("default", &cfg); err != nil {

View file

@ -2,7 +2,7 @@ package main
import (
"fmt"
"log"
"log/slog"
"sort"
"strings"
"time"
@ -35,7 +35,6 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
now := time.Now().In(location)
currentDay := yearStart
log.Println(currentDay)
for currentDay.Before(yearEnd) {
dayStr := currentDay.Format("2006-01-02")
weekday := currentDay.Weekday()
@ -56,7 +55,7 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
for _, entry := range entries {
if entry.StartTime.IsZero() {
log.Printf("WARN: Skipping entry with zero start time (ID: %d)", entry.ID)
slog.Warn(fmt.Sprintf("Skipping entry with zero start time (ID: %d)", entry.ID))
continue
}
@ -100,7 +99,7 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
summary, exists := dailyMap[dayStr]
if !exists {
log.Printf("WARN: Day %s not found in initial map during entry processing (ID: %d)", dayStr, entry.ID)
slog.Warn(fmt.Sprintf("Day %s not found in initial map during entry processing (ID: %d)", dayStr, entry.ID))
loopTime = dayEnd
continue
}
@ -141,7 +140,7 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
case TagBreak:
summary.BreakDuration += segmentDuration
default:
log.Printf("INFO: Encountered unknown tag '%s' during interval processing for entry ID %d on %s. Counting duration as 'work'.", entry.Tag, entry.ID, dayStr)
slog.Info(fmt.Sprintf("Encountered unknown tag '%s' during interval processing for entry ID %d on %s. Counting duration as 'work'.", entry.Tag, entry.ID, dayStr))
summary.WorkDuration += segmentDuration
if summary.WorkStart == "" || timeStr < summary.WorkStart {
summary.WorkStart = timeStr
@ -261,7 +260,7 @@ func getSollExcelTime(dayOfWeek string) any {
sollDur, err := time.Parse("15:04", sollString)
if err != nil {
log.Printf("ERROR: Could not parse hardcoded soll string '%s': %v", sollString, err)
slog.Error(fmt.Sprintf("Could not parse hardcoded soll string '%s': %v", sollString, err))
return nil
}
return float64(sollDur.Hour())/24.0 + float64(sollDur.Minute())/(24.0*60.0)
@ -271,7 +270,7 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
log.Printf("ERROR: Failed to close excel file handle: %v", err)
slog.Error(fmt.Sprintf("Failed to close excel file handle: %v", err))
}
}()

View file

@ -3,7 +3,7 @@ package main
import (
"fmt"
"io"
"log"
"log/slog"
"net"
"sync"
@ -31,28 +31,28 @@ func (pf *PortForwarder) forward() error {
localAddr := "127.0.0.1:" + pf.localPort
remoteAddr := net.JoinHostPort(pf.remoteHost, pf.remotePort)
pf.logf("INFO: Starting port forwarder: local %s -> remote %s (via SSH)", localAddr, remoteAddr)
pf.logf("INFO", "Starting port forwarder: local %s -> remote %s (via SSH)", localAddr, remoteAddr)
listener, err := net.Listen("tcp", localAddr)
if err != nil {
pf.logf("ERROR: Failed to open local listener on %s: %v", localAddr, err)
pf.logf("ERROR", "Failed to open local listener on %s: %v", localAddr, err)
return fmt.Errorf("failed to listen on %s: %w", localAddr, err)
}
defer listener.Close()
pf.logf("INFO: Listener active on %s", localAddr)
pf.logf("INFO", "Listener active on %s", localAddr)
for {
localConn, err := listener.Accept()
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Err.Error() == "use of closed network connection" {
pf.logf("INFO: Listener on %s closed, stopping forwarder.", localAddr)
pf.logf("INFO", "Listener on %s closed, stopping forwarder.", localAddr)
return nil
}
pf.logf("ERROR: Failed to accept incoming connection on %s: %v", localAddr, err)
pf.logf("ERROR", "Failed to accept incoming connection on %s: %v", localAddr, err)
continue
}
pf.logf("INFO: Accepted connection from %s on %s", localConn.RemoteAddr(), localAddr)
pf.logf("INFO", "Accepted connection from %s on %s", localConn.RemoteAddr(), localAddr)
go pf.handleConnection(localConn, remoteAddr)
}
}
@ -60,14 +60,14 @@ func (pf *PortForwarder) forward() error {
func (pf *PortForwarder) handleConnection(localConn net.Conn, remoteAddr string) {
defer localConn.Close()
pf.logf("INFO: Dialing remote host %s via SSH tunnel for %s", remoteAddr, localConn.RemoteAddr())
pf.logf("INFO", "Dialing remote host %s via SSH tunnel for %s", remoteAddr, localConn.RemoteAddr())
remoteConn, err := pf.sshCon.Dial("tcp", remoteAddr)
if err != nil {
pf.logf("ERROR: Failed to dial remote host %s via SSH: %v", remoteAddr, err)
pf.logf("ERROR", "Failed to dial remote host %s via SSH: %v", remoteAddr, err)
return
}
defer remoteConn.Close()
pf.logf("INFO: Connection to %s established. Starting data copy.", remoteAddr)
pf.logf("INFO", "Connection to %s established. Starting data copy.", remoteAddr)
var wg sync.WaitGroup
wg.Add(2)
@ -78,7 +78,7 @@ func (pf *PortForwarder) handleConnection(localConn net.Conn, remoteAddr string)
bytesCopied, err := io.Copy(localConn, remoteConn)
if err != nil {
}
pf.logf("INFO: Finished copying remote->local (%d bytes) for %s", bytesCopied, localConn.RemoteAddr())
pf.logf("INFO", "Finished copying remote->local (%d bytes) for %s", bytesCopied, localConn.RemoteAddr())
}()
go func() {
@ -87,15 +87,22 @@ func (pf *PortForwarder) handleConnection(localConn net.Conn, remoteAddr string)
bytesCopied, err := io.Copy(remoteConn, localConn)
if err != nil {
}
pf.logf("INFO: Finished copying local->remote (%d bytes) for %s", bytesCopied, localConn.RemoteAddr())
pf.logf("INFO", "Finished copying local->remote (%d bytes) for %s", bytesCopied, localConn.RemoteAddr())
}()
wg.Wait()
pf.logf("INFO: Closing forwarded connection for %s", localConn.RemoteAddr())
pf.logf("INFO", "Closing forwarded connection for %s", localConn.RemoteAddr())
}
func (pf *PortForwarder) logf(format string, v ...any) {
func (pf *PortForwarder) logf(level, format string, v ...any) {
pf.logMutex.Lock()
defer pf.logMutex.Unlock()
log.Printf(format, v...)
switch level {
case "INFO":
slog.Info(format, v...)
case "WARN":
slog.Warn(format, v...)
case "ERROR":
slog.Error(format, v...)
}
}

View file

@ -1,18 +1,20 @@
package main
import (
"log"
"fmt"
"log/slog"
"os"
)
func main() {
app, err := NewApp()
if err != nil {
log.Fatalf("ERROR: Unable to setup application: %v", err)
slog.Error(fmt.Sprintf("Unable to setup application: %v", err))
os.Exit(1)
}
defer func() {
if err := app.Close(); err != nil {
log.Printf("ERROR: Failed to close application resources: %v", err)
slog.Error(fmt.Sprintf("Failed to close application resources: %v", err))
}
}()

4
ssh.go
View file

@ -1,7 +1,7 @@
package main
import (
"log"
"log/slog"
"golang.org/x/crypto/ssh"
)
@ -12,7 +12,7 @@ type SSHConnection struct {
func (s *SSHConnection) Close() error {
if s.client != nil {
log.Println("DEBUG: Closing SSH client connection.")
slog.Debug("Closing SSH client connection.")
return s.client.Close()
}
return nil

View file

@ -3,7 +3,7 @@ package main
import (
"database/sql"
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
@ -37,7 +37,7 @@ func NewTimeStore(cfg Config) (*TimeStore, error) {
return nil, fmt.Errorf("could not determine database path: %w", err)
}
log.Printf("INFO: Using database at: %s", dbPath)
slog.Info(fmt.Sprintf("Using database at: %s", dbPath))
db, err := sql.Open("sqlite", fmt.Sprintf("%s?_pragma=journal_mode(WAL)", dbPath))
if err != nil {
@ -66,7 +66,7 @@ func NewTimeStore(cfg Config) (*TimeStore, error) {
createIndexSQL := `CREATE INDEX IF NOT EXISTS idx_time_entries_start_time ON time_entries (start_time);`
if _, err = db.Exec(createIndexSQL); err != nil {
log.Printf("WARN: Failed to create index on start_time: %v", err)
slog.Warn(fmt.Sprintf("Failed to create index on start_time: %v", err))
}
return &TimeStore{db: db, dbPath: dbPath}, nil
@ -80,7 +80,7 @@ func ensureDatabasePath(_ Config) (string, error) {
workConfigDir := filepath.Join(configDir, "work")
dbPath := filepath.Join(workConfigDir, "worktime.sqlite")
if err := os.MkdirAll(workConfigDir, 0750); err != nil {
if err := os.MkdirAll(workConfigDir, 0o750); err != nil {
return "", fmt.Errorf("failed to create config directory '%s': %w", workConfigDir, err)
}
@ -89,7 +89,7 @@ func ensureDatabasePath(_ Config) (string, error) {
func (ts *TimeStore) Close() error {
if ts.db != nil {
log.Printf("INFO: Closing database connection to %s", ts.dbPath)
slog.Info(fmt.Sprintf("Closing database connection to %s", ts.dbPath))
return ts.db.Close()
}
return nil
@ -108,7 +108,7 @@ func (ts *TimeStore) stopCurrentEntry(now time.Time) (bool, error) {
}
if rowsAffected > 1 {
log.Printf("WARN: Stopped %d entries. Expected 0 or 1. Manual DB check might be needed.", rowsAffected)
slog.Warn(fmt.Sprintf("Stopped %d entries. Expected 0 or 1. Manual DB check might be needed.", rowsAffected))
}
return rowsAffected > 0, nil
}
@ -124,7 +124,7 @@ func (ts *TimeStore) StartTracking(tag string) error {
return err
}
if stopped {
log.Println("INFO: Stopped previous time entry.")
slog.Info("Stopped previous time entry.")
}
query := `INSERT INTO time_entries (tag, start_time, end_time) VALUES (?, ?, NULL);`
@ -132,7 +132,7 @@ func (ts *TimeStore) StartTracking(tag string) error {
if err != nil {
return fmt.Errorf("failed to start tracking tag '%s': %w", tag, err)
}
log.Printf("INFO: Started tracking: %s at %s", tag, now.Format(time.RFC3339))
slog.Info(fmt.Sprintf("Started tracking: %s at %s", tag, now.Format(time.RFC3339)))
return nil
}
@ -143,9 +143,9 @@ func (ts *TimeStore) StopTracking() error {
return err
}
if stopped {
log.Printf("INFO: Stopped tracking at %s", now.Format(time.RFC3339))
slog.Info(fmt.Sprintf("Stopped tracking at %s", now.Format(time.RFC3339)))
} else {
log.Println("INFO: No active time entry found to stop.")
slog.Info("No active time entry found to stop.")
}
return nil
}
@ -273,7 +273,7 @@ func getTimeRangeFromPeriod(period string) (time.Time, time.Time) {
end := start.AddDate(0, 0, 1)
return start, end
}
log.Printf("WARN: Unrecognized period string '%s'. Cannot calculate time range.", period)
slog.Warn(fmt.Sprintf("Unrecognized period string '%s'. Cannot calculate time range.", period))
return time.Time{}, time.Time{}
}
}
@ -329,14 +329,14 @@ func (ts *TimeStore) ShowSummary(period string) error {
}
func (ts *TimeStore) ExportSummary(filename string) error {
log.Printf("INFO: Starting export to '%s'...", filename)
slog.Info(fmt.Sprintf("Starting export to '%s'...", filename))
currentYear := time.Now().Year()
location := time.Local
yearStart := time.Date(currentYear, 1, 1, 0, 0, 0, 0, location)
yearEnd := yearStart.AddDate(1, 0, 0)
log.Printf("INFO: Exporting data for year %d (%s to %s)", currentYear, yearStart.Format("2006-01-02"), yearEnd.Format("2006-01-02"))
slog.Info(fmt.Sprintf("Exporting data for year %d (%s to %s)", currentYear, yearStart.Format("2006-01-02"), yearEnd.Format("2006-01-02")))
query := `
SELECT id, tag, start_time, end_time
@ -362,7 +362,7 @@ func (ts *TimeStore) ExportSummary(filename string) error {
if err = rows.Err(); err != nil {
return fmt.Errorf("error during export row iteration: %w", err)
}
log.Printf("INFO: Found %d potentially relevant time entries for year %d.", len(entries), currentYear)
slog.Info(fmt.Sprintf("Found %d potentially relevant time entries for year %d.", len(entries), currentYear))
dailySummaries, err := aggregateEntriesToDailySummaries(entries, yearStart, yearEnd)
if err != nil {
@ -372,17 +372,17 @@ func (ts *TimeStore) ExportSummary(filename string) error {
excelEntries := convertDailyToExcelEntries(dailySummaries)
if len(excelEntries) == 0 {
log.Println("WARN: No daily summaries generated for the export period.")
slog.Warn("No daily summaries generated for the export period.")
fmt.Println("No data available to generate the export for the specified period.")
return nil
}
log.Printf("INFO: Generated %d daily entries for the Excel export.", len(excelEntries))
slog.Info(fmt.Sprintf("Generated %d daily entries for the Excel export.", len(excelEntries)))
if err := writeExcelSheet(excelEntries, filename); err != nil { // Aufruf der geänderten Funktion
return fmt.Errorf("failed to write excel sheet '%s': %w", filename, err)
}
log.Printf("INFO: Successfully exported timetable to %s", filename)
slog.Info(fmt.Sprintf("Successfully exported timetable to %s", filename))
fmt.Printf("Successfully exported timetable to %s\n", filename)
return nil
}
@ -399,16 +399,16 @@ func (ts *TimeStore) LogFullDay(tag string, date time.Time) error {
dayEnd := dayStart.Add(24 * time.Hour)
dayStr := dayStart.Format("2006-01-02")
log.Printf("INFO: Attempting to log '%s' for the full day %s", tag, dayStr)
slog.Info(fmt.Sprintf("Attempting to log '%s' for the full day %s", tag, dayStr))
// 1. Stoppe den aktuell laufenden Timer (falls vorhanden)
// Wir verwenden dayStart als Zeitpunkt für das Stoppen, um Konsistenz zu wahren
stopped, err := ts.stopCurrentEntry(dayStart)
if err != nil {
// Nur loggen, weitermachen. Der Nutzer will diesen Tag ja explizit setzen.
log.Printf("WARN: Failed to stop current entry before logging full day '%s': %v", tag, err)
slog.Warn(fmt.Sprintf("Failed to stop current entry before logging full day '%s': %v", tag, err))
} else if stopped {
log.Printf("INFO: Stopped active timer before logging '%s' for %s.", tag, dayStr)
slog.Info(fmt.Sprintf("Stopped active timer before logging '%s' for %s.", tag, dayStr))
}
tx, err := ts.db.Begin()
@ -436,7 +436,7 @@ func (ts *TimeStore) LogFullDay(tag string, date time.Time) error {
}
titleCaser := cases.Title(language.English)
log.Printf("INFO: Successfully logged full day entry: Tag='%s', Start='%s', End='%s'", tag, dayStart.Format(time.RFC3339), dayEnd.Format(time.RFC3339))
slog.Info(fmt.Sprintf("Successfully logged full day entry: Tag='%s', Start='%s', End='%s'", tag, dayStart.Format(time.RFC3339), dayEnd.Format(time.RFC3339)))
fmt.Printf("Successfully logged '%s' for %s.\n", titleCaser.String(tag), dayStr) // Benutzerfeedback
return nil
}