refactor: clean up code from comments
This commit is contained in:
parent
4ceed6f301
commit
29bdd3a2a4
8 changed files with 150 additions and 252 deletions
80
app.go
80
app.go
|
|
@ -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
112
cmd.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
147
export.go
|
|
@ -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 {
|
||||
|
|
|
|||
29
forwarder.go
29
forwarder.go
|
|
@ -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
4
go.mod
|
|
@ -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
3
ssh.go
|
|
@ -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
|
||||
|
|
|
|||
22
store.go
22
store.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue