refactor: clean up code from comments

This commit is contained in:
Patryk Hegenberg 2025-04-02 14:21:33 +02:00
parent 4ceed6f301
commit 29bdd3a2a4
8 changed files with 150 additions and 252 deletions

80
app.go
View file

@ -15,7 +15,7 @@ import (
type App struct {
cfg Config
flags Flags
timeStore *TimeStore // Datenbank-Handler hinzugefügt
timeStore *TimeStore
}
func NewApp() (*App, error) {
@ -31,7 +31,7 @@ func NewApp() (*App, error) {
return &App{
cfg: cfg,
timeStore: ts, // TimeStore initialisiert
timeStore: ts,
}, nil
}
@ -47,7 +47,7 @@ func (a *App) connect() {
log.Printf("WARN: Failed to start time tracking for '%s': %v", TagWork, err)
}
a.wakeWorkstation() // Versuche, Workstation zu wecken
a.wakeWorkstation()
sshCon, err := a.newSSHConnection()
if err != nil {
@ -60,7 +60,6 @@ func (a *App) connect() {
go func() {
log.Println("INFO: Starting SSH forwarder (local :2048 -> remote workstation:22)")
if err := sshForwarder.forward(); err != nil {
// Logge Fehler, wenn der Listener nicht gestartet werden kann oder abbricht
log.Printf("ERROR: SSH forwarder failed: %v", err)
}
log.Println("INFO: SSH forwarder stopped.")
@ -84,7 +83,7 @@ func (a *App) runCommand(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin // Wichtig für interaktive Befehle wie ssh
cmd.Stdin = os.Stdin
err := cmd.Run()
if err != nil {
log.Printf("ERROR: Command failed: %s %s -> %v", name, strings.Join(args, " "), err)
@ -97,15 +96,15 @@ func (a *App) runCommand(name string, args ...string) error {
func (a *App) wakeWorkstation() {
log.Println("INFO: Attempting to wake workstation...")
innerSSHCmd := fmt.Sprintf("ssh -tt %s@%s \"wakeonlan %s && echo 'Wake-on-LAN packet sent.' && exit\"",
a.cfg.JumpUser, // Benutzer auf dem Jump-Host
a.cfg.JumpHost, // Jump-Host
a.cfg.WorkstationMac) // MAC-Adresse der Workstation
a.cfg.JumpUser,
a.cfg.JumpHost,
a.cfg.WorkstationMac)
outerSSHCmd := []string{
"-tt", // TTY für die äußere Verbindung
"-p", fmt.Sprintf("%d", a.cfg.SSHPort), // Port für den ersten Host
fmt.Sprintf("%s@%s", a.cfg.SSHUser, a.cfg.SSHHost), // user@host für den ersten Host
innerSSHCmd, // Der gesamte innere SSH-Befehl als einzelnes Argument
"-tt",
"-p", fmt.Sprintf("%d", a.cfg.SSHPort),
fmt.Sprintf("%s@%s", a.cfg.SSHUser, a.cfg.SSHHost),
innerSSHCmd,
}
if err := a.runCommand("ssh", outerSSHCmd...); err != nil {
@ -118,23 +117,22 @@ func (a *App) wakeWorkstation() {
func (a *App) connectToJump() {
log.Println("INFO: Connecting to Jump Host with Port Forwarding...")
sshArgs := []string{
"-tt", // TTY Allokation
"-L", fmt.Sprintf("2048:%s:22", a.cfg.WorkstationHost), // Forwarding
"-p", fmt.Sprintf("%d", a.cfg.SSHPort), // Port für den Jump-Host
fmt.Sprintf("%s@%s", a.cfg.SSHUser, a.cfg.SSHHost), // user@jumphost
"-tt",
"-L", fmt.Sprintf("2048:%s:22", a.cfg.WorkstationHost),
"-p", fmt.Sprintf("%d", a.cfg.SSHPort),
fmt.Sprintf("%s@%s", a.cfg.SSHUser, a.cfg.SSHHost),
}
if err := a.runCommand("ssh", sshArgs...); err != nil {
// Fehler wird bereits in runCommand geloggt
}
}
func (a *App) connectToWorkstation() {
log.Println("INFO: Connecting to Workstation via local tunnel (localhost:2048)...")
sshArgs := []string{
"-tt", // TTY
"-L", fmt.Sprintf("6000:%s:3389", a.cfg.WorkstationHost), // RDP Tunnel via Workstation SSH
"-p", "2048", // Verbinde zum lokalen Port des ersten Tunnels
fmt.Sprintf("%s@127.0.0.1", a.cfg.WorkstationUser), // Benutzer@localhost (geht durch den Tunnel)
"-tt",
"-L", fmt.Sprintf("6000:%s:3389", a.cfg.WorkstationHost),
"-p", "2048",
fmt.Sprintf("%s@127.0.0.1", a.cfg.WorkstationUser),
}
if err := a.runCommand("ssh", sshArgs...); err != nil {
}
@ -144,7 +142,7 @@ func (a *App) startRDPConnection() {
log.Println("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.RDPPassword, // SICHERHEITSRISIKO!
a.cfg.SSHPassword,
)
if err := a.runCommand("bash", "-c", rdpCommand); err != nil {
}
@ -158,25 +156,22 @@ func (a *App) makeChoice() {
huh.NewSelect[string]().
Title("What would you like to do?").
Options(
huh.NewOption("Start Work & Connect", "start work"), // Kombinierte Aktion
huh.NewOption("Start Work & Connect", "start work"),
huh.NewOption("Stop Work", "stop work"),
huh.NewOption("Start Break", "start break"),
huh.NewOption("Stop Break", "stop break"),
// huh.NewOption("--- Summaries ---", "").Disabled(true), // Trenner
huh.NewOption("Show Today Summary", "show day summary"),
huh.NewOption("Show Week Summary", "show week summary"),
huh.NewOption("Show Month Summary", "show month summary"),
huh.NewOption("Export Yearly Timetable", "export"),
// huh.NewOption("--- Connections ---", "").Disabled(true), // Trenner
huh.NewOption("Connect to Jump Host (Tunnel to Workstation)", "connect to jump"),
huh.NewOption("Connect to Workstation (via Tunnel)", "connect to workstation"),
huh.NewOption("Start RDP Connection (via Tunnel)", "start rdp connection"),
huh.NewOption("Wake Workstation", "wake workstation"),
// huh.NewOption("--- Maintenance ---", "").Disabled(true), // Trenner
huh.NewOption("Kill Active Tunnels (Ports 2048, 6000)", "kill tunnels"),
huh.NewOption("Exit", "exit"),
).
Value(&choice), // Speichere die Auswahl in 'choice'
Value(&choice),
),
)
@ -192,7 +187,7 @@ func (a *App) makeChoice() {
switch choice {
case "start work":
a.connect() // Startet Zeit, weckt Rechner, baut Tunnel auf
a.connect()
case "stop work":
if err := a.timeStore.StopTracking(); err != nil {
log.Printf("ERROR: Failed to stop time tracking: %v", err)
@ -222,18 +217,18 @@ func (a *App) makeChoice() {
}
case "export":
filename := "Arbeitszeiten_" + time.Now().Format("2006") + ".xlsx"
if a.flags.ExportName != "" && a.flags.ExportName != "Arbeitszeiten.xlsx" { // Check ob Flag von CLI kommt
if a.flags.ExportName != "" && a.flags.ExportName != "Arbeitszeiten.xlsx" {
filename = a.flags.ExportName
}
if err := a.timeStore.ExportSummary(filename); err != nil {
log.Printf("ERROR: Failed to export summary to '%s': %v", filename, err)
}
case "connect to jump":
a.connectToJump() // Blockiert
a.connectToJump()
case "connect to workstation":
a.connectToWorkstation() // Blockiert
a.connectToWorkstation()
case "start rdp connection":
a.startRDPConnection() // Blockiert
a.startRDPConnection()
case "wake workstation":
a.wakeWorkstation()
case "kill tunnels":
@ -244,20 +239,20 @@ func (a *App) makeChoice() {
}
case "exit":
fmt.Println("Exiting.")
return // Beendet die Funktion, was zum Programmende führt
return
default:
log.Printf("WARN: Unhandled choice '%s'", choice)
}
if choice != "exit" && choice != "connect to jump" && choice != "connect to workstation" && choice != "start rdp connection" {
fmt.Println("\nPress Enter to continue...")
fmt.Scanln() // Warte auf Enter
a.makeChoice() // Rufe Menü erneut auf
fmt.Scanln()
a.makeChoice()
}
}
func (a *App) getSSHAuth() ssh.AuthMethod {
keyPath := os.ExpandEnv("$HOME/.ssh/hegenberg") // Sicherer Standard
keyPath := os.ExpandEnv("$HOME/.ssh/hegenberg")
keyBytes, err := os.ReadFile(keyPath)
if err != nil {
@ -294,8 +289,6 @@ func (a *App) newSSHConnection() (*SSHConnection, error) {
sshConfig := &ssh.ClientConfig{
User: a.cfg.SSHUser,
Auth: []ssh.AuthMethod{authMethod},
// ACHTUNG: InsecureIgnoreHostKey ist unsicher für Produktionsumgebungen!
// Besser: Known Hosts verwenden.
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 10 * time.Second, // Etwas längerer Timeout
}
@ -318,7 +311,6 @@ func (a *App) newSSHConnection() (*SSHConnection, error) {
return &SSHConnection{
client: client,
// session: session, // Session wird für Forwarding nicht direkt gebraucht
}, nil
}
@ -330,8 +322,6 @@ func (a *App) killForwardings() error {
log.Println("INFO: Attempting to kill processes listening on ports:", strings.Join(ports, ", "))
for _, port := range ports {
// Finde Prozess-ID (PID), die auf dem TCP-Port lauscht
// lsof -i tcp:<port> -t gibt nur die PID aus
cmd := exec.Command("lsof", "-i", "tcp:"+port, "-t")
output, err := cmd.Output()
if err != nil {
@ -339,9 +329,9 @@ func (a *App) killForwardings() error {
log.Printf("INFO: No process found listening on port %s.", port)
} else {
log.Printf("WARN: 'lsof' command failed for port %s: %v", port, err)
lastErr = fmt.Errorf("lsof failed for port %s: %w", port, err) // Letzten Fehler merken
lastErr = fmt.Errorf("lsof failed for port %s: %w", port, err)
}
continue // Gehe zum nächsten Port
continue
}
pids := strings.SplitSeq(strings.TrimSpace(string(output)), "\n")
@ -351,7 +341,7 @@ func (a *App) killForwardings() error {
continue
}
log.Printf("INFO: Found process PID %s on port %s. Attempting to kill...", pid, port)
killCmd := exec.Command("kill", pid) // SIGTERM senden
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)
forceKillCmd := exec.Command("kill", "-9", pid)
@ -375,5 +365,5 @@ func (a *App) killForwardings() error {
log.Println("INFO: No forwarding processes found or killed.")
}
return lastErr // Gibt den letzten aufgetretenen Fehler zurück
return lastErr
}

112
cmd.go
View file

@ -12,22 +12,21 @@ import (
func (a *App) setupCommands() *cobra.Command {
rootCmd := &cobra.Command{
Use: "workctl", // Name des CLI-Tools
Use: "workctl",
Short: "Manage work time, connections, and tasks",
Long: `workctl is a command-line interface to streamline common work-related tasks,
including time tracking (using an internal SQLite database), remote connections (SSH, RDP),
and other utilities.`,
Version: "1.0.0-sqlite", // Beispielversion
Version: "1.0.0-sqlite",
}
rootCmd.AddCommand(a.startCommand())
rootCmd.AddCommand(a.stopCommand())
rootCmd.AddCommand(a.showCommand())
rootCmd.AddCommand(a.trackCommand()) // Neuer Befehl für Tracking
rootCmd.AddCommand(a.connectCommands()) // Befehle für Verbindungen gruppieren
rootCmd.AddCommand(a.trackCommand())
rootCmd.AddCommand(a.connectCommands())
rootCmd.AddCommand(a.wakeCommand())
rootCmd.AddCommand(a.importTimewarriorCommand())
// Verberge das Standard 'completion' Kommando, falls nicht gewünscht
rootCmd.CompletionOptions.DisableDefaultCmd = true
return rootCmd
@ -40,7 +39,7 @@ func (a *App) startCommand() *cobra.Command {
Long: "Starts time tracking for 'work', attempts to wake the workstation, and sets up SSH tunnels.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Starting workday procedures...")
a.connect() // Führt Start Tracking, Wake, Tunnel Setup aus
a.connect()
fmt.Println("Workday start initiated. Tunnels are running in the background.")
fmt.Println("Use 'workctl connect rdp' or connect manually.")
},
@ -71,54 +70,6 @@ func (a *App) stopCommand() *cobra.Command {
}
}
// func (a *App) trackCommand() *cobra.Command {
// cmd := &cobra.Command{
// Use: "track [tag]",
// Short: "Start tracking a new tag (stops current)",
// Long: `Starts a new time tracking entry with the specified tag (e.g., 'break', 'meeting', 'projectX').
// This automatically stops any currently running timer.
// If no tag is provided, it stops the current timer and starts 'work'.`,
// Args: cobra.MaximumNArgs(1), // 0 oder 1 Argument
// Run: func(cmd *cobra.Command, args []string) {
// tag := TagWork // Standard-Tag, wenn kein Argument gegeben wird
// if len(args) > 0 {
// tag = args[0]
// }
//
// if tag == "" {
// log.Println("ERROR: Tag cannot be empty.")
// fmt.Println("Usage: workctl track <tag_name>")
// os.Exit(1) // Fehler signalisieren
// }
//
// fmt.Printf("Attempting to start tracking '%s'...\n", tag)
// if err := a.timeStore.StartTracking(tag); err != nil {
// log.Printf("ERROR: Failed to start tracking '%s': %v", tag, err)
// fmt.Printf("Error: Could not start tracking '%s'.\n", tag)
// os.Exit(1)
// } else {
// fmt.Printf("Successfully started tracking '%s'.\n", tag)
// }
// },
// }
// cmd.AddCommand(&cobra.Command{
// Use: "break",
// Short: "Start tracking 'break'",
// Run: func(cmd *cobra.Command, args []string) {
// fmt.Println("Starting break...")
// if err := a.timeStore.StartTracking(TagBreak); err != nil {
// log.Printf("ERROR: Failed to start break tracking: %v", err)
// fmt.Println("Error: Could not start break.")
// os.Exit(1)
// } else {
// fmt.Println("Break started.")
// }
// },
// })
//
// return cmd
// }
func (a *App) trackCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "track [tag]",
@ -130,51 +81,43 @@ If no tag is provided, it stops the current timer and starts 'work'.
If the provided tag is a special full-day tag ('uni', 'urlaub', 'feiertag', 'krank', 'free'),
it will mark the *current day* with that tag instead of starting an interval timer.
This also stops any currently running timer.`,
Args: cobra.MaximumNArgs(1), // 0 oder 1 Argument
RunE: func(cmd *cobra.Command, args []string) error { // RunE für Fehlerbehandlung
tag := TagWork // Standard-Tag, wenn kein Argument gegeben wird
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
tag := TagWork
if len(args) > 0 {
tag = args[0]
}
if tag == "" {
// Sollte durch Default nicht passieren, aber sicher ist sicher
return fmt.Errorf("tag cannot be empty")
}
tagLower := strings.ToLower(tag)
// Prüfe, ob es ein spezieller Ganztages-Tag ist
switch tagLower {
case "uni", "urlaub", "feiertag", "krank", "free":
today := time.Now()
fmt.Printf("Logging '%s' for today (%s)...\n", tagLower, today.Format("2006-01-02"))
if err := a.timeStore.LogFullDay(tagLower, today); err != nil {
// Loggen passiert in LogFullDay oder bei Fehlern davor
// Gib den Fehler zurück, damit Cobra ihn behandelt
return fmt.Errorf("could not log '%s' for today: %w", tagLower, err)
}
// Erfolgsmeldung kommt aus LogFullDay
return nil // Erfolg
default: // Normale Intervalle wie 'work', 'break', oder unbekannt
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)
// Gib Fehler zurück
return fmt.Errorf("could not start tracking '%s': %w", tag, err)
}
// Erfolgsmeldung kommt aus StartTracking
return nil // Erfolg
}
},
}
// Alias hinzufügen (bleibt bestehen)
cmd.AddCommand(&cobra.Command{
Use: "break",
Short: "Start tracking 'break'",
RunE: func(cmd *cobra.Command, args []string) error { // RunE verwenden
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)
@ -196,16 +139,16 @@ or exports the yearly data to an Excel file.
Periods: today, day, week, month, year (or YYYY-MM-DD)
Export: Use the --export flag or the 'export' subcommand.`,
ValidArgs: []string{"day", "week", "month", "year", "today"}, // Zur Autovervollständigung
ValidArgs: []string{"day", "week", "month", "year", "today"},
Run: func(cmd *cobra.Command, args []string) {
period := "today" // Standard ist heute
period := "today"
if len(args) > 0 {
period = args[0]
}
if a.flags.ShowExport {
filename := a.flags.ExportName
if filename == "" || filename == "Arbeitszeiten.xlsx" { // Standardwert aus Flags anpassen
if filename == "" || filename == "Arbeitszeiten.xlsx" {
filename = "Arbeitszeiten_" + time.Now().Format("2006") + ".xlsx"
log.Printf("INFO: No export name specified, using default: %s", filename)
}
@ -240,7 +183,6 @@ Export: Use the --export flag or the 'export' subcommand.`,
cmd.MarkFlagsMutuallyExclusive("week", "month", "export")
// Füge einen 'export' Unterbefehl hinzu für klarere Nutzung
cmd.AddCommand(&cobra.Command{
Use: "export [filename]",
Short: "Export yearly timetable to Excel",
@ -285,7 +227,7 @@ func (a *App) connectCommands() *cobra.Command {
Short: "Connect to Jump Host (with tunnel to workstation)",
Long: "Establishes an SSH connection to the Jump Host and forwards local port 2048 to the workstation's SSH port (22). This command blocks.",
Run: func(cmd *cobra.Command, args []string) {
a.connectToJump() // Blockiert
a.connectToJump()
},
})
@ -294,7 +236,7 @@ func (a *App) connectCommands() *cobra.Command {
Short: "Connect to Workstation via SSH tunnel",
Long: "Establishes an SSH connection to the Workstation via the local tunnel on port 2048. Also sets up RDP tunnel on local port 6000. Requires the 'jump' tunnel to be active. This command blocks.",
Run: func(cmd *cobra.Command, args []string) {
a.connectToWorkstation() // Blockiert
a.connectToWorkstation()
},
})
@ -303,7 +245,7 @@ func (a *App) connectCommands() *cobra.Command {
Short: "Start RDP session via tunnel",
Long: "Starts an RDP client (xfreerdp) connecting to localhost:6000. Requires an active tunnel forwarding this port to the workstation's RDP port (3389). This command blocks.",
Run: func(cmd *cobra.Command, args []string) {
a.startRDPConnection() // Blockiert
a.startRDPConnection()
},
})
@ -349,7 +291,7 @@ func (a *App) runImport(filepath string) (int, error) {
if err != nil {
return 0, fmt.Errorf("could not begin database transaction: %w", err)
}
defer tx.Rollback() // Rollback wird ausgeführt, wenn Commit nicht erreicht wird
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO time_entries (tag, start_time, end_time) VALUES (?, ?, ?)")
if err != nil {
@ -371,13 +313,8 @@ func (a *App) runImport(filepath string) (int, error) {
var tag, start_str, end_str string
has_date := false
// Versuche, das Format zu erkennen (mit oder ohne Datum am Anfang)
// Format mit Datum: Wk Date Day Tags Start End Time [Total]
// Format ohne Datum: Tags Start End Time [Total]
// Mindestens 4 Felder für Tag, Start, End, Time erwartet
if len(fields) >= 7 && strings.Contains(fields[1], "-") && len(fields[1]) == 10 { // Prüft auf Datum YYYY-MM-DD
current_date_str = fields[1] // Datum merken
if len(fields) >= 7 && strings.Contains(fields[1], "-") && len(fields[1]) == 10 {
current_date_str = fields[1]
tag = fields[3]
start_str = fields[4]
end_str = fields[5]
@ -385,7 +322,7 @@ func (a *App) runImport(filepath string) (int, error) {
} 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)
continue // Überspringe Zeile, wenn kein Datum bekannt ist
continue
}
tag = fields[0]
start_str = fields[1]
@ -403,7 +340,7 @@ func (a *App) runImport(filepath string) (int, error) {
log.Printf("WARN: Skipping line with invalid date '%s': %v", current_date_str, err)
continue
}
end_time := start_time.Add(24 * time.Hour) // Ende ist Anfang des nächsten Tages
end_time := start_time.Add(24 * time.Hour)
_, err = stmt.Exec(tag, start_time, end_time)
if err != nil {
@ -411,7 +348,7 @@ func (a *App) runImport(filepath string) (int, error) {
} else {
imported_count++
}
continue // Gehe zur nächsten Zeile nach Behandlung des ganztägigen Eintrags
continue
}
} else {
@ -440,11 +377,6 @@ func (a *App) runImport(filepath string) (int, error) {
log.Printf("WARN: End time is before start time on the same date line, skipping: %s", line)
continue
}
// Wenn kein Datum da war, gehen wir davon aus, dass es sich um Mitternacht handelt.
// Diese Logik ist knifflig und fehleranfällig, da `timew summary` normalerweise aufteilt.
// Einfacher Ansatz: Ignoriere diesen Fall vorerst, da er im Standard-Summary selten auftritt.
// log.Printf("INFO: Detected potential midnight crossing (end %v < start %v) - adjusting end date might be needed if timew split wasn't done.", endTime, startTime)
// endTime = endTime.Add(24 * time.Hour) // Vorsicht mit dieser Annahme!
}
db_tag := strings.ToLower(tag)

View file

@ -11,6 +11,7 @@ import (
type Config struct {
SSHUser string `mapstructure:"SSH_USER"`
SSHPassword string `mapstructure:"SSH_PASSWORD"`
SSHHost string `mapstructure:"SSH_HOST"`
JumpUser string `mapstructure:"JUMP_USER"`
JumpHost string `mapstructure:"JUMP_HOST"`

147
export.go
View file

@ -11,28 +11,28 @@ import (
)
type DailySummary struct {
Date string // YYYY-MM-DD Format
Day string // Mon, Tue, etc.
WorkStart string // HH:MM:SS - Frühester Arbeitsbeginn
WorkEnd string // HH:MM:SS - Spätestes Arbeitsende
BreakDuration time.Duration // Summe aller Pausenzeiten an diesem Tag
WorkDuration time.Duration // Summe aller Arbeitszeiten an diesem Tag
Tag string // Spezielle Tags für den Tag (Urlaub, Krank, Feiertag, Uni, Free...)
Date string
Day string
WorkStart string
WorkEnd string
BreakDuration time.Duration
WorkDuration time.Duration
Tag string
}
type ExcelEntry struct {
Date string // YYYY-MM-DD
Day string // Mon, Tue, etc.
WorkStart string // HH:MM:SS
WorkEnd string // HH:MM:SS
BreakDuration string // HH:MM:SS (formatiert)
Tag string // Spezielle Tags
Date string
Day string
WorkStart string
WorkEnd string
BreakDuration string
Tag string
}
func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd time.Time) (map[string]*DailySummary, error) {
dailyMap := make(map[string]*DailySummary)
location := yearStart.Location() // Verwende die Zeitzone des Startdatums
now := time.Now().In(location) // Aktuelle Zeit für laufende Einträge
location := yearStart.Location()
now := time.Now().In(location)
currentDay := yearStart
log.Println(currentDay)
@ -46,13 +46,13 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
dailyMap[dayStr] = &DailySummary{
Date: dayStr,
Day: weekday.String()[:3], // Mon, Tue, etc.
Tag: tag, // Initialisiere mit "free" für Wochenende, sonst leer
Day: weekday.String()[:3],
Tag: tag,
}
currentDay = currentDay.Add(24 * time.Hour) // Gehe zum nächsten Tag
currentDay = currentDay.Add(24 * time.Hour)
}
fullDayTags := make(map[string]string) // Map, um ganztägige Ereignisse zu speichern (Datum -> Tag)
fullDayTags := make(map[string]string)
for _, entry := range entries {
if entry.StartTime.IsZero() {
@ -60,11 +60,11 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
continue
}
startTime := entry.StartTime.In(location) // Stelle sicher, dass Zeiten in der korrekten Zeitzone sind
startTime := entry.StartTime.In(location)
endTime := entry.EndTime.Time.In(location)
validEndTime := entry.EndTime.Valid
if !validEndTime {
endTime = now // Nimm aktuelle Zeit für laufende Einträge
endTime = now
}
if endTime.Before(yearStart) || startTime.After(yearEnd) {
@ -82,7 +82,6 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
loopTimeForTag := startTime
for loopTimeForTag.Before(endTime) || loopTimeForTag.Equal(endTime) {
dayStr := loopTimeForTag.Format("2006-01-02")
// Nur Tage innerhalb des Jahres berücksichtigen
if _, exists := dailyMap[dayStr]; exists {
existingTag := fullDayTags[dayStr]
if shouldOverwriteTag(existingTag, lowerTag) {
@ -102,7 +101,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)
loopTime = dayEnd // Gehe zum nächsten Tag
loopTime = dayEnd
continue
}
@ -113,16 +112,15 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
}
segmentDuration := segmentEnd.Sub(segmentStart)
if segmentDuration <= 0 { // Überspringe leere Segmente
if segmentDuration <= 0 {
loopTime = dayEnd
continue
}
timeStr := segmentStart.Format("15:04:05")
// endTimeStr := segmentEnd.Format("15:04:05") // Ende des Segments
switch lowerTag {
case TagWork: // Konstante verwenden
case TagWork:
summary.WorkDuration += segmentDuration
if summary.WorkStart == "" || timeStr < summary.WorkStart {
summary.WorkStart = timeStr
@ -140,7 +138,7 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
summary.Tag = TagWork
}
case TagBreak: // Konstante verwenden
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)
@ -157,17 +155,16 @@ func aggregateEntriesToDailySummaries(entries []TimeEntry, yearStart, yearEnd ti
summary.WorkEnd = entryEndTimeOnThisDayStr
}
if summary.Tag == "" || summary.Tag == "free" {
summary.Tag = TagWork // Behandle unbekannt wie Arbeit für den Tag-Typ
summary.Tag = TagWork
}
}
loopTime = dayEnd // Gehe zum nächsten Tag
loopTime = dayEnd
}
}
for dayStr, specialTag := range fullDayTags {
if summary, exists := dailyMap[dayStr]; exists {
// Nur überschreiben, wenn der neue Tag höhere oder gleiche Prio hat
if shouldOverwriteTag(summary.Tag, specialTag) {
summary.Tag = specialTag
summary.WorkStart = ""
@ -194,9 +191,9 @@ func shouldOverwriteTag(existingTag, newTag string) bool {
"feiertag": 1,
"urlaub": 1,
"uni": 2,
"work": 3, // Arbeit hat niedrigere Priorität als spezielle Tage
"break": 99, // Pause sollte nie der Haupt-Tag sein
"free": 100, // Frei hat niedrigste Priorität
"work": 3,
"break": 99,
"free": 100,
}
prioExisting, okExisting := priority[strings.ToLower(existingTag)]
if !okExisting {
@ -217,7 +214,7 @@ func convertDailyToExcelEntries(dailySummaries map[string]*DailySummary) []Excel
for d := range dailySummaries {
dates = append(dates, d)
}
sort.Strings(dates) // Sortiere die Datums-Strings (YYYY-MM-DD)
sort.Strings(dates)
for _, dateStr := range dates {
summary := dailySummaries[dateStr]
@ -226,8 +223,8 @@ func convertDailyToExcelEntries(dailySummaries map[string]*DailySummary) []Excel
Day: summary.Day,
WorkStart: summary.WorkStart,
WorkEnd: summary.WorkEnd,
BreakDuration: formatDuration(summary.BreakDuration), // Formatierte Dauer
Tag: summary.Tag, // Übernehme den finalen Tag
BreakDuration: formatDuration(summary.BreakDuration),
Tag: summary.Tag,
}
excelEntries = append(excelEntries, entry)
}
@ -236,7 +233,7 @@ func convertDailyToExcelEntries(dailySummaries map[string]*DailySummary) []Excel
func formatDuration(d time.Duration) string {
if d < 0 {
d = -d // Arbeite mit positivem Wert für die Berechnung
d = -d
sign := "-"
d = d.Round(time.Second)
h := int64(d.Hours())
@ -254,25 +251,23 @@ func formatDuration(d time.Duration) string {
func getSollExcelTime(dayOfWeek string) any {
var sollString string
switch dayOfWeek {
case "Mon", "Tue", "Thu", "Fri": // Standard-Arbeitstage
case "Mon", "Tue", "Thu", "Fri":
sollString = "08:00"
case "Wed": // Kurzer Tag
case "Wed":
sollString = "04:00"
default: // Sa, So
return nil // Kein Soll an diesen Tagen
default:
return nil
}
sollDur, err := time.Parse("15:04", sollString)
if err != nil {
log.Printf("ERROR: Could not parse hardcoded soll string '%s': %v", sollString, err)
return nil // Fehler beim Parsen
return nil
}
return float64(sollDur.Hour())/24.0 + float64(sollDur.Minute())/(24.0*60.0)
}
func writeExcelSheet(entries []ExcelEntry, name string) error {
// Sortierung erfolgt jetzt in convertDailyToExcelEntries
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
@ -291,7 +286,7 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
if err != nil {
existingIndex, _ := f.GetSheetIndex(sheetName)
if existingIndex == -1 {
sheetName = "Sheet1" // Fallback auf Default
sheetName = "Sheet1"
index, _ = f.GetSheetIndex(sheetName)
if index == -1 {
return fmt.Errorf("could not create or find sheet '%s' or 'Sheet1': %w", sheetName, err)
@ -312,9 +307,9 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
f.SetCellValue(sheetName, "B3", "Datum")
f.SetCellValue(sheetName, "C3", "Tag")
f.SetCellValue(sheetName, "D3", "Status / Zeit") // Titel angepasst
f.SetCellValue(sheetName, "D3", "Status / Zeit")
f.MergeCell(sheetName, "D3", "E3")
f.SetCellValue(sheetName, "G3", "Dauer") // Titel angepasst
f.SetCellValue(sheetName, "G3", "Dauer")
f.MergeCell(sheetName, "G3", "H3")
f.SetCellValue(sheetName, "I3", "Pause")
f.SetCellValue(sheetName, "J3", "Netto")
@ -327,28 +322,26 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
f.SetCellValue(sheetName, "E4", "bis")
f.SetCellValue(sheetName, "G4", "brutto")
f.SetCellValue(sheetName, "H4", "")
f.SetCellValue(sheetName, "J4", "Ist (Netto)") // Titel angepasst
f.SetCellValue(sheetName, "J4", "Ist (Netto)")
f.SetCellValue(sheetName, "K4", "")
f.SetCellValue(sheetName, "L4", "Tag")
f.SetCellValue(sheetName, "N4", "Total")
f.SetCellValue(sheetName, "O4", "")
timeStyleCode := "hh:mm" // Format für Zeitpunkte und Dauer < 24h
timeStyleCode := "hh:mm"
timeStyle, _ := f.NewStyle(&excelize.Style{CustomNumFmt: &timeStyleCode})
dateStyleCode := "dd.mm.yyyy" // Format für Datum
dateStyleCode := "dd.mm.yyyy"
dateStyle, _ := f.NewStyle(&excelize.Style{CustomNumFmt: &dateStyleCode})
saldoStyleCode := "[h]:mm;[RED]-[h]:mm" // Verwende [h] um Stunden > 24 zu erlauben
saldoStyleCode := "[h]:mm;[RED]-[h]:mm"
saldoStyle, _ := f.NewStyle(&excelize.Style{CustomNumFmt: &saldoStyleCode})
headerStyle, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
Alignment: &excelize.Alignment{Horizontal: "center"},
})
centerStyle, _ := f.NewStyle(&excelize.Style{Alignment: &excelize.Alignment{Horizontal: "center"}})
// Stil für Zellen, die leer bleiben sollen (optional, um z.B. 0 auszublenden)
// emptyStyle, _ := f.NewStyle(&excelize.Style{Fill: excelize.Fill{Type: "pattern", Color: []string{"#FFFFFF"}, Pattern: 1}}) // Weißer Hintergrund
f.SetCellStyle(sheetName, "B3", "O4", headerStyle)
f.SetCellStyle(sheetName, "B1", "O1", headerStyle) // Titel auch
f.SetCellStyle(sheetName, "B1", "O1", headerStyle)
startRow := 6
for i, entry := range entries {
@ -361,7 +354,7 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
f.SetCellValue(sheetName, "B"+rowStr, dateValue)
f.SetCellStyle(sheetName, "B"+rowStr, "B"+rowStr, dateStyle)
} else {
f.SetCellValue(sheetName, "B"+rowStr, entry.Date) // Fallback
f.SetCellValue(sheetName, "B"+rowStr, entry.Date)
}
f.SetCellValue(sheetName, "C"+rowStr, entry.Day)
@ -370,11 +363,11 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
f.SetCellValue(sheetName, "K"+rowStr, sollExcelTime)
f.SetCellStyle(sheetName, "K"+rowStr, "K"+rowStr, timeStyle)
} else {
f.SetCellValue(sheetName, "K"+rowStr, "") // Leer für Wochenende etc.
f.SetCellValue(sheetName, "K"+rowStr, "")
}
switch tagLower {
case TagWork, "": // Normaler Arbeitstag oder nicht speziell getaggter Tag
case TagWork, "":
if entry.WorkStart != "" && entry.WorkEnd != "" {
startTime, _ := time.Parse("15:04:05", entry.WorkStart)
endTime, _ := time.Parse("15:04:05", entry.WorkEnd)
@ -402,7 +395,7 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
f.SetCellStyle(sheetName, "I"+rowStr, "I"+rowStr, timeStyle)
f.SetCellFormula(sheetName, "J"+rowStr, fmt.Sprintf("MAX(0, G%d-I%d)", row, row))
f.SetCellStyle(sheetName, "J"+rowStr, "J"+rowStr, saldoStyle) // Saldo-Style für Dauer
f.SetCellStyle(sheetName, "J"+rowStr, "J"+rowStr, saldoStyle)
} else {
f.SetCellValue(sheetName, "J"+rowStr, 0.0)
@ -417,13 +410,13 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
text = "Hochschule"
}
f.SetCellValue(sheetName, "D"+rowStr, text)
f.MergeCell(sheetName, "D"+rowStr, "I"+rowStr) // Verbinde D bis I
f.MergeCell(sheetName, "D"+rowStr, "I"+rowStr)
f.SetCellStyle(sheetName, "D"+rowStr, "I"+rowStr, centerStyle)
if sollExcelTime != nil {
f.SetCellValue(sheetName, "J"+rowStr, sollExcelTime)
} else {
f.SetCellValue(sheetName, "J"+rowStr, 0.0) // Kein Soll (WE), Netto 0
f.SetCellValue(sheetName, "J"+rowStr, 0.0)
}
f.SetCellStyle(sheetName, "J"+rowStr, "J"+rowStr, saldoStyle)
@ -435,19 +428,19 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
text = "Krank"
}
f.SetCellValue(sheetName, "D"+rowStr, text)
f.MergeCell(sheetName, "D"+rowStr, "I"+rowStr) // Verbinde D bis I
f.MergeCell(sheetName, "D"+rowStr, "I"+rowStr)
f.SetCellStyle(sheetName, "D"+rowStr, "I"+rowStr, centerStyle)
if sollExcelTime != nil {
f.SetCellValue(sheetName, "J"+rowStr, sollExcelTime)
} else {
f.SetCellValue(sheetName, "J"+rowStr, 0.0) // Kein Soll (WE), Netto 0
f.SetCellValue(sheetName, "J"+rowStr, 0.0)
}
f.SetCellStyle(sheetName, "J"+rowStr, "J"+rowStr, saldoStyle)
case "free": // Wochenende oder explizit "free"
case "free":
f.SetCellValue(sheetName, "D"+rowStr, "")
f.MergeCell(sheetName, "D"+rowStr, "I"+rowStr) // Verbinde D bis I
f.MergeCell(sheetName, "D"+rowStr, "I"+rowStr)
f.SetCellStyle(sheetName, "D"+rowStr, "I"+rowStr, centerStyle)
// J: Netto ist 0
f.SetCellValue(sheetName, "J"+rowStr, 0.0)
@ -459,27 +452,27 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
}
f.SetCellFormula(sheetName, "L"+rowStr, fmt.Sprintf("J%d-K%d", row, row))
f.SetCellStyle(sheetName, "L"+rowStr, "M"+rowStr, saldoStyle) // Style auf L und M
f.SetCellStyle(sheetName, "L"+rowStr, "M"+rowStr, saldoStyle)
if i == 0 { // Erste Datenzeile
if i == 0 {
f.SetCellFormula(sheetName, "N"+rowStr, fmt.Sprintf("L%d", row))
} else {
prevSaldoTotalCell := fmt.Sprintf("N%d", row-1)
f.SetCellFormula(sheetName, "N"+rowStr, fmt.Sprintf("%s+L%d", prevSaldoTotalCell, row))
}
f.SetCellStyle(sheetName, "N"+rowStr, "O"+rowStr, saldoStyle) // Style auf N und O
f.SetCellStyle(sheetName, "N"+rowStr, "O"+rowStr, saldoStyle)
}
f.SetColWidth(sheetName, "B", "B", 12) // Datum
f.SetColWidth(sheetName, "C", "C", 5) // Tag
f.SetColWidth(sheetName, "D", "E", 10) // Status/Zeit von/bis
f.SetColWidth(sheetName, "F", "F", 2) // Leer
f.SetColWidth(sheetName, "G", "H", 9) // Dauer brutto
f.SetColWidth(sheetName, "I", "I", 9) // Pause
f.SetColWidth(sheetName, "J", "J", 9) // Netto
f.SetColWidth(sheetName, "K", "K", 9) // Soll
f.SetColWidth(sheetName, "L", "M", 9) // Saldo Tag
f.SetColWidth(sheetName, "N", "O", 10) // Saldo Total
f.SetColWidth(sheetName, "B", "B", 12)
f.SetColWidth(sheetName, "C", "C", 5)
f.SetColWidth(sheetName, "D", "E", 10)
f.SetColWidth(sheetName, "F", "F", 2)
f.SetColWidth(sheetName, "G", "H", 9)
f.SetColWidth(sheetName, "I", "I", 9)
f.SetColWidth(sheetName, "J", "J", 9)
f.SetColWidth(sheetName, "K", "K", 9)
f.SetColWidth(sheetName, "L", "M", 9)
f.SetColWidth(sheetName, "N", "O", 10)
f.SetActiveSheet(index)
if err := f.SaveAs(name); err != nil {

View file

@ -5,7 +5,7 @@ import (
"io"
"log"
"net"
"sync" // Mutex hinzufügen für sichereres Logging
"sync"
"golang.org/x/crypto/ssh"
)
@ -15,7 +15,7 @@ type PortForwarder struct {
localPort string
remotePort string
remoteHost string
logMutex sync.Mutex // Mutex zum Schutz von Log-Ausgaben aus Goroutinen
logMutex sync.Mutex
}
func NewPortForwarder(sshCon *ssh.Client, localPort, remotePort, remoteHost string) *PortForwarder {
@ -29,7 +29,7 @@ func NewPortForwarder(sshCon *ssh.Client, localPort, remotePort, remoteHost stri
func (pf *PortForwarder) forward() error {
localAddr := "127.0.0.1:" + pf.localPort
remoteAddr := net.JoinHostPort(pf.remoteHost, pf.remotePort) // Sicherer Host:Port kombinieren
remoteAddr := net.JoinHostPort(pf.remoteHost, pf.remotePort)
pf.logf("INFO: Starting port forwarder: local %s -> remote %s (via SSH)", localAddr, remoteAddr)
@ -44,18 +44,11 @@ func (pf *PortForwarder) forward() error {
for {
localConn, err := listener.Accept()
if err != nil {
// Fehler tritt auf, wenn der Listener geschlossen wird oder ein Netzwerkproblem vorliegt.
// Prüfe, ob der Fehler durch Schließen des Listeners verursacht wurde (erwartet).
// Fehler wie 'use of closed network connection' sind hier normal beim Beenden.
// if errors.Is(err, net.ErrClosed) { // Bessere Prüfung in neueren Go-Versionen
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)
return nil // Kein Fehler, normales Beenden
return nil
}
pf.logf("ERROR: Failed to accept incoming connection on %s: %v", localAddr, err)
// Optional: Kurze Pause vor erneutem Versuch oder Abbruch?
// Bei dauerhaften Fehlern wird die Schleife hier schnell laufen.
// Für dieses Tool ist ein Weiterlaufen bei Accept-Fehlern wahrscheinlich ok.
continue
}
@ -71,9 +64,6 @@ func (pf *PortForwarder) handleConnection(localConn net.Conn, remoteAddr string)
remoteConn, err := pf.sshCon.Dial("tcp", remoteAddr)
if err != nil {
pf.logf("ERROR: Failed to dial remote host %s via SSH: %v", remoteAddr, err)
// Schließe lokale Verbindung, wenn Remote nicht erreicht werden kann
// (defer macht das schon, aber hier explizit zur Klarheit)
// localConn.Close() // Ist durch defer abgedeckt
return
}
defer remoteConn.Close()
@ -84,21 +74,18 @@ func (pf *PortForwarder) handleConnection(localConn net.Conn, remoteAddr string)
go func() {
defer wg.Done()
defer localConn.Close() // Schließe die eine Seite, wenn die andere endet
defer localConn.Close()
bytesCopied, err := io.Copy(localConn, remoteConn)
if err != nil {
// Fehler beim Kopieren sind normal, wenn eine Seite die Verbindung schließt
// log.Printf("DEBUG: Error copying remote->local: %v", err)
}
pf.logf("INFO: Finished copying remote->local (%d bytes) for %s", bytesCopied, localConn.RemoteAddr())
}()
go func() {
defer wg.Done()
defer remoteConn.Close() // Schließe die andere Seite, wenn diese endet
defer remoteConn.Close()
bytesCopied, err := io.Copy(remoteConn, localConn)
if err != nil {
// log.Printf("DEBUG: Error copying local->remote: %v", err)
}
pf.logf("INFO: Finished copying local->remote (%d bytes) for %s", bytesCopied, localConn.RemoteAddr())
}()
@ -107,8 +94,8 @@ func (pf *PortForwarder) handleConnection(localConn net.Conn, remoteAddr string)
pf.logf("INFO: Closing forwarded connection for %s", localConn.RemoteAddr())
}
func (pf *PortForwarder) logf(format string, v ...interface{}) {
func (pf *PortForwarder) logf(format string, v ...any) {
pf.logMutex.Lock()
defer pf.logMutex.Unlock()
log.Printf(format, v...) // Verwende den Standard-Logger
log.Printf(format, v...)
}

4
go.mod
View file

@ -1,4 +1,4 @@
module work
module workctl
go 1.24.2
@ -8,6 +8,7 @@ require (
github.com/spf13/viper v1.20.1
github.com/xuri/excelize/v2 v2.9.0
golang.org/x/crypto v0.36.0
golang.org/x/text v0.23.0
modernc.org/sqlite v1.37.0
)
@ -56,7 +57,6 @@ require (
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.62.1 // indirect
modernc.org/mathutil v1.7.1 // indirect

3
ssh.go
View file

@ -8,12 +8,11 @@ import (
type SSHConnection struct {
client *ssh.Client
// session *ssh.Session // Session wird für Forwarding nicht direkt benötigt
}
func (s *SSHConnection) Close() error {
if s.client != nil {
log.Println("DEBUG: Closing SSH client connection.") // Optional Debug Log
log.Println("DEBUG: Closing SSH client connection.")
return s.client.Close()
}
return nil

View file

@ -9,6 +9,8 @@ import (
"strings"
"time"
"golang.org/x/text/cases"
"golang.org/x/text/language"
_ "modernc.org/sqlite"
)
@ -70,7 +72,7 @@ func NewTimeStore(cfg Config) (*TimeStore, error) {
return &TimeStore{db: db, dbPath: dbPath}, nil
}
func ensureDatabasePath(cfg Config) (string, error) {
func ensureDatabasePath(_ Config) (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("could not get user config dir: %w", err)
@ -201,7 +203,7 @@ func (ts *TimeStore) CalculateSummary(period string) (map[string]time.Duration,
defer rows.Close()
summary := make(map[string]time.Duration)
now := time.Now() // Aktuelle Zeit für laufende Einträge
now := time.Now()
for rows.Next() {
var entry TimeEntry
@ -215,7 +217,7 @@ func (ts *TimeStore) CalculateSummary(period string) (map[string]time.Duration,
}
effectiveEnd := entry.EndTime.Time
if !entry.EndTime.Valid { // Eintrag läuft noch
if !entry.EndTime.Valid {
effectiveEnd = now
}
@ -276,14 +278,6 @@ func getTimeRangeFromPeriod(period string) (time.Time, time.Time) {
}
}
// func formatDuration(d time.Duration) string {
// d = d.Round(time.Second)
// h := int64(d.Hours())
// m := int64(d.Minutes()) % 60
// s := int64(d.Seconds()) % 60
// return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
// }
func (ts *TimeStore) ShowSummary(period string) error {
summary, err := ts.CalculateSummary(period)
if err != nil {
@ -320,11 +314,12 @@ func (ts *TimeStore) ShowSummary(period string) error {
tags = append(tags, tag)
}
titleCaser := cases.Title(language.English)
totalDuration := time.Duration(0)
fmt.Println("------------------------------")
for _, tag := range tags {
duration := summary[tag]
fmt.Printf(" %-12s: %s\n", strings.Title(tag), formatDuration(duration))
fmt.Printf(" %-12s: %s\n", titleCaser.String(tag), formatDuration(duration))
totalDuration += duration
}
fmt.Println("------------------------------")
@ -440,7 +435,8 @@ func (ts *TimeStore) LogFullDay(tag string, date time.Time) error {
return fmt.Errorf("failed to commit transaction for full-day entry: %w", err)
}
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))
fmt.Printf("Successfully logged '%s' for %s.\n", strings.Title(tag), dayStr) // Benutzerfeedback
fmt.Printf("Successfully logged '%s' for %s.\n", titleCaser.String(tag), dayStr) // Benutzerfeedback
return nil
}