Compare commits
21 commits
510dd5659b
...
cb99f0f7a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb99f0f7a1 | ||
| c0a83b5892 | |||
| 127018b565 | |||
| 20b4b7ba2d | |||
| 54979319ff | |||
| d8743e54c1 | |||
| fcffccc145 | |||
| 23e3d4919f | |||
| ac943acae2 | |||
| bb75925d1f | |||
| 9a67429b45 | |||
| ea906ca862 | |||
| deb15af40e | |||
| f9945a2685 | |||
| 9732a0a0ed | |||
| efe25d3f2e | |||
| 1f588c771a | |||
| 7decdf6254 | |||
| 3b4835026f | |||
| 63c0b0c953 | |||
|
|
3956ef91ab |
15 changed files with 369 additions and 273 deletions
|
|
@ -18,15 +18,12 @@ jobs:
|
|||
- name: Get dependencies
|
||||
run: go mod tidy
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v2-latest
|
||||
|
||||
# - name: Lint code
|
||||
# run: |
|
||||
# go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
# golangci-lint run
|
||||
- name: Install golangci-lint
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
- name: Run golangci-lint
|
||||
run: golangci-lint run
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
|
|
|||
|
|
@ -7,44 +7,28 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
name: GoReleaser build
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
goos: [linux, darwin, windows]
|
||||
exclude:
|
||||
- os: ubuntu-latest
|
||||
goos: darwin
|
||||
- os: ubuntu-latest
|
||||
goos: windows
|
||||
- os: macos-latest
|
||||
goos: linux
|
||||
- os: macos-latest
|
||||
goos: windows
|
||||
- os: windows-latest
|
||||
goos: linux
|
||||
- os: windows-latest
|
||||
goos: darwin
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
- name: Set up Go 1.24
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.24
|
||||
id: go
|
||||
|
||||
- name: Get dependencies
|
||||
run: go mod tidy
|
||||
|
||||
- name: Build for ${{ matrix.goos }}
|
||||
run: |
|
||||
GOOS=${{ matrix.goos }} GOARCH=amd64 go build -o workctl-${{ matrix.goos }} .
|
||||
ls -la
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: workctl-${{ matrix.goos }}
|
||||
path: workctl-${{ matrix.goos }}
|
||||
- name: Unset GITHUB_TOKEN (if present)
|
||||
run: unset GITHUB_TOKEN
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@master
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1 +1,3 @@
|
|||
work-config.toml
|
||||
# Added by goreleaser init:
|
||||
dist/
|
||||
|
|
|
|||
57
.goreleaser.yaml
Normal file
57
.goreleaser.yaml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
gitea_urls:
|
||||
api: https://git.patanix.de/api/v1
|
||||
download: https://git.patanix.de
|
||||
|
||||
env_files:
|
||||
gitlab_token: ~/nope
|
||||
github_token: ~/nope
|
||||
|
||||
force_token: "gitea"
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}"
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
Released by {{.Env.USER}}.
|
||||
107
app.go
107
app.go
|
|
@ -2,7 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
|
@ -42,9 +42,9 @@ func (a *App) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
func (a *App) connect(withoutTimew bool) (*SSHConnection, error) { // Rückgabetyp geändert
|
||||
if err := a.timeStore.StartTracking(TagWork, withoutTimew); err != nil {
|
||||
slog.Warn(fmt.Sprintf("Failed to start time tracking for '%s': %v", TagWork, err))
|
||||
}
|
||||
|
||||
a.wakeWorkstation()
|
||||
|
|
@ -54,24 +54,25 @@ 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...")
|
||||
|
||||
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)")
|
||||
if err := sshForwarder.forward(); err != nil {
|
||||
log.Printf("ERROR: 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.")
|
||||
}()
|
||||
|
||||
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)")
|
||||
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.")
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
|
@ -80,22 +81,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 +110,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 +129,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 +141,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,39 +183,39 @@ 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
|
||||
}
|
||||
|
||||
switch choice {
|
||||
case "start work":
|
||||
a.connect()
|
||||
a.connect(withoutTimew)
|
||||
case "stop work":
|
||||
if err := a.timeStore.StopTracking(); err != nil {
|
||||
log.Printf("ERROR: Failed to stop time tracking: %v", err)
|
||||
if err := a.timeStore.StopTracking(withoutTimew); err != nil {
|
||||
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)
|
||||
if err := a.timeStore.StartTracking(TagBreak, withoutTimew); err != nil {
|
||||
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)
|
||||
if err := a.timeStore.StartTracking(TagWork, withoutTimew); err != nil {
|
||||
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 +223,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 +235,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 +258,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 +266,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 +296,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 +321,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 +342,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
|
||||
|
|
|
|||
161
cmd.go
161
cmd.go
|
|
@ -2,7 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
|
@ -12,6 +12,8 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var withoutTimew bool
|
||||
|
||||
func (a *App) setupCommands() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "workctl",
|
||||
|
|
@ -44,18 +46,18 @@ Use --background (-b) to keep tunnels running in the background without auto-con
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("Starting workday procedures...")
|
||||
|
||||
sshCon, err := a.connect()
|
||||
sshCon, err := a.connect(withoutTimew)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Failed to start connections: %v\n", err)
|
||||
return fmt.Errorf("connection setup failed: %w", err)
|
||||
}
|
||||
|
||||
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,24 +72,24 @@ 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.")
|
||||
if err := a.timeStore.StopTracking(); err != nil {
|
||||
log.Printf("WARN: Failed to stop time tracking: %v", err)
|
||||
slog.Info("Received signal, cleanup via defer sshCon.Close() will run.")
|
||||
if err := a.timeStore.StopTracking(withoutTimew); err != nil {
|
||||
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.")
|
||||
|
||||
} else {
|
||||
fmt.Println("INFO: Automatically connecting to workstation via SSH tunnel...")
|
||||
fmt.Println("Automatically connecting to workstation via SSH tunnel...")
|
||||
a.connectToWorkstation()
|
||||
|
||||
fmt.Println("INFO: Workstation SSH session finished.")
|
||||
log.Println("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)
|
||||
fmt.Println("Workstation SSH session finished.")
|
||||
slog.Info("Foreground session ended, cleanup via defer sshCon.Close() will run.")
|
||||
if err := a.timeStore.StopTracking(withoutTimew); err != nil {
|
||||
slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err))
|
||||
} else {
|
||||
log.Println("INFO: Time tracking stopped.")
|
||||
slog.Info("Time tracking stopped.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,25 +98,26 @@ Use --background (-b) to keep tunnels running in the background without auto-con
|
|||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&a.flags.StartInBackground, "background", "b", false, "Run tunnels in the background instead of connecting immediately")
|
||||
cmd.Flags().BoolVarP(&withoutTimew, "timew", "t", false, "Set this flag if you dont want to use Timewarrior Timestorage as well")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *App) stopCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stop work: Stop time tracking, kill tunnels",
|
||||
Long: "Stops the current time tracking entry and attempts to kill active SSH tunnels.",
|
||||
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)
|
||||
if err := a.timeStore.StopTracking(withoutTimew); err != nil {
|
||||
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.")
|
||||
}
|
||||
|
|
@ -122,6 +125,8 @@ func (a *App) stopCommand() *cobra.Command {
|
|||
fmt.Println("Workday stop procedures finished.")
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVarP(&withoutTimew, "timew", "t", false, "Set this flag if you dont want to use Timewarrior Timestorage as well")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *App) trackCommand() *cobra.Command {
|
||||
|
|
@ -155,15 +160,15 @@ This also stops any currently running timer.`,
|
|||
if err := a.timeStore.LogFullDay(tagLower, today); err != nil {
|
||||
return fmt.Errorf("could not log '%s' for today: %w", tagLower, err)
|
||||
}
|
||||
return nil // Erfolg
|
||||
return nil
|
||||
|
||||
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)
|
||||
if err := a.timeStore.StartTracking(tag, withoutTimew); err != nil {
|
||||
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
|
||||
return nil
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -173,8 +178,8 @@ This also stops any currently running timer.`,
|
|||
Short: "Start tracking 'break'",
|
||||
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)
|
||||
if err := a.timeStore.StartTracking(TagBreak, withoutTimew); err != nil {
|
||||
slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err))
|
||||
return fmt.Errorf("could not start break: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -204,27 +209,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 +253,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 +326,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 +358,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 +369,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
16
export.go
16
export.go
|
|
@ -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))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -442,11 +441,10 @@ func writeExcelSheet(entries []ExcelEntry, name string) error {
|
|||
f.SetCellValue(sheetName, "D"+rowStr, "")
|
||||
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)
|
||||
f.SetCellStyle(sheetName, "J"+rowStr, "J"+rowStr, saldoStyle)
|
||||
|
||||
default: // Unbekannte Tags oder Tage ohne Eintrag
|
||||
default:
|
||||
f.SetCellValue(sheetName, "J"+rowStr, 0.0)
|
||||
f.SetCellStyle(sheetName, "J"+rowStr, "J"+rowStr, saldoStyle)
|
||||
}
|
||||
|
|
|
|||
37
forwarder.go
37
forwarder.go
|
|
@ -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 -> remote (via SSH)", "Local Address", localAddr, "Remote Address", 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", "Local Address", 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 closed, stopping forwarder.", "Local Address", localAddr)
|
||||
return nil
|
||||
}
|
||||
pf.logf("ERROR: Failed to accept incoming connection on %s: %v", localAddr, err)
|
||||
pf.logf("ERROR", "Failed to accept incoming connection:", "Local Address", localAddr, "Error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
pf.logf("INFO: Accepted connection from %s on %s", localConn.RemoteAddr(), localAddr)
|
||||
pf.logf("INFO", "Accepted connection:", "Remote Address", localConn.RemoteAddr(), "Local Address", 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 via SSH tunnel", "Local Address", remoteAddr, "Remote Address", 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 via SSH:", "Remote Address", remoteAddr, "Error:", err)
|
||||
return
|
||||
}
|
||||
defer remoteConn.Close()
|
||||
pf.logf("INFO: Connection to %s established. Starting data copy.", remoteAddr)
|
||||
pf.logf("INFO", "Connection established. Starting data copy.", "Remote Address", 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", "Bytes copied", bytesCopied, "Remote Address", 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", "Bytes copied", bytesCopied, "Remote Address", localConn.RemoteAddr())
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
pf.logf("INFO: Closing forwarded connection for %s", localConn.RemoteAddr())
|
||||
pf.logf("INFO", "Closing forwarded connection", "Remote Address", 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...)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
go.mod
18
go.mod
|
|
@ -6,9 +6,9 @@ require (
|
|||
github.com/charmbracelet/huh v0.6.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
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
|
||||
github.com/xuri/excelize/v2 v2.9.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/text v0.32.0
|
||||
modernc.org/sqlite v1.37.0
|
||||
)
|
||||
|
||||
|
|
@ -33,7 +33,6 @@ require (
|
|||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
||||
|
|
@ -49,14 +48,15 @@ require (
|
|||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.6.0 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.1 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
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/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
|
|
|||
54
go.sum
54
go.sum
|
|
@ -56,8 +56,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
|||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
|
|
@ -103,36 +101,48 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
||||
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
|
||||
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
18
helpers.go
Normal file
18
helpers.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func runCommand(name string, args ...string) {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
slog.Error("Command execution error", "command", name, "args", args, "error", err)
|
||||
}
|
||||
}
|
||||
23
main.go
23
main.go
|
|
@ -1,18 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
slog.Error("Cant get user config dir")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filepath.Join(configDir, "work", "workctl.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(file, nil))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
app, err := NewApp()
|
||||
if err != nil {
|
||||
log.Fatalf("ERROR: Unable to setup application: %v", err)
|
||||
slog.Error("Unable to setup application", "Error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
if err := app.Close(); err != nil {
|
||||
log.Printf("ERROR: Failed to close application resources: %v", err)
|
||||
slog.Error("Failed to close application resources", "Error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
|||
4
ssh.go
4
ssh.go
|
|
@ -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
|
||||
|
|
|
|||
68
store.go
68
store.go
|
|
@ -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("Using database at:", "Database Path", 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("Failed to create index on start_time:", "Error:", 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("Closing database connection", "Database Path", ts.dbPath)
|
||||
return ts.db.Close()
|
||||
}
|
||||
return nil
|
||||
|
|
@ -108,12 +108,12 @@ 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
|
||||
}
|
||||
|
||||
func (ts *TimeStore) StartTracking(tag string) error {
|
||||
func (ts *TimeStore) StartTracking(tag string, withoutTimew bool) error {
|
||||
if tag == "" {
|
||||
return fmt.Errorf("cannot start tracking with an empty tag")
|
||||
}
|
||||
|
|
@ -124,7 +124,10 @@ func (ts *TimeStore) StartTracking(tag string) error {
|
|||
return err
|
||||
}
|
||||
if stopped {
|
||||
log.Println("INFO: Stopped previous time entry.")
|
||||
slog.Info("Stopped previous time entry.")
|
||||
}
|
||||
if !withoutTimew {
|
||||
runCommand("timew", "start", "work")
|
||||
}
|
||||
|
||||
query := `INSERT INTO time_entries (tag, start_time, end_time) VALUES (?, ?, NULL);`
|
||||
|
|
@ -132,20 +135,23 @@ 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
|
||||
}
|
||||
|
||||
func (ts *TimeStore) StopTracking() error {
|
||||
func (ts *TimeStore) StopTracking(withoutTimew bool) error {
|
||||
now := time.Now()
|
||||
stopped, err := ts.stopCurrentEntry(now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !withoutTimew {
|
||||
runCommand("timew", "stop", "work")
|
||||
}
|
||||
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 +279,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 +335,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 +368,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 +378,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
|
||||
if err := writeExcelSheet(excelEntries, filename); err != nil {
|
||||
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
|
||||
}
|
||||
|
|
@ -391,31 +397,27 @@ func (ts *TimeStore) LogFullDay(tag string, date time.Time) error {
|
|||
if tag == "" {
|
||||
return fmt.Errorf("cannot log full day with an empty tag")
|
||||
}
|
||||
tag = strings.ToLower(tag) // Stelle sicher, dass der Tag klein geschrieben ist
|
||||
tag = strings.ToLower(tag)
|
||||
|
||||
location := date.Location() // Verwende die Zeitzone des übergebenen Datums
|
||||
// Berechne Start (00:00:00 des Tages) und Ende (00:00:00 des nächsten Tages)
|
||||
location := date.Location()
|
||||
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
|
||||
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()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not begin transaction to log full day: %w", err)
|
||||
}
|
||||
defer tx.Rollback() // Stellt sicher, dass bei Fehlern nichts gespeichert wird
|
||||
defer tx.Rollback()
|
||||
|
||||
query := `INSERT INTO time_entries (tag, start_time, end_time) VALUES (?, ?, ?);`
|
||||
stmt, err := tx.Prepare(query)
|
||||
|
|
@ -426,17 +428,15 @@ func (ts *TimeStore) LogFullDay(tag string, date time.Time) error {
|
|||
|
||||
_, err = stmt.Exec(tag, dayStart, dayEnd)
|
||||
if err != nil {
|
||||
// Spezifischere Fehlermeldung, falls es UNIQUE Constraints gäbe
|
||||
return fmt.Errorf("failed to insert full-day entry for tag '%s' on %s: %w", tag, dayStr, err)
|
||||
}
|
||||
|
||||
// Transaktion erfolgreich abschließen
|
||||
if err = tx.Commit(); err != nil {
|
||||
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", titleCaser.String(tag), dayStr) // Benutzerfeedback
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue