From 737203d5cdd0941e0b19f85eadfcd33f54bacdd5 Mon Sep 17 00:00:00 2001 From: Nick Domako Date: Fri, 27 Mar 2026 14:36:12 -0400 Subject: [PATCH] feat(logging): structured console logging with Start-Transcript and -Verbose - Resolve Logs path from settings.json Paths.Logs (fallback: ../logs) - Start-Transcript writes timestamped .log file to Logs dir at run start - Stop-Transcript called in finally block to guarantee cleanup on all exits - Write-Host replaced with Write-Information -InformationAction Continue - Write-Verbose added for EXIF values, GPS matching detail, OCR reading/confidence, odometer validation, entry parsing, dedup decisions, and pairing distance checks - Run start/end info lines added to both scripts closes #8 --- scripts/Build-Trips.ps1 | 44 ++++++++++++++++++++++++++++---------- scripts/Rename-Photos.ps1 | 45 +++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/scripts/Build-Trips.ps1 b/scripts/Build-Trips.ps1 index 77d9c1c..99bc819 100644 --- a/scripts/Build-Trips.ps1 +++ b/scripts/Build-Trips.ps1 @@ -143,6 +143,17 @@ $reportsDir = if ($paths -and $paths.PSObject.Properties['Reports'] -and $paths. $logFile = Join-Path $reportsDir "rename-log.csv" $tripsOut = Join-Path $reportsDir "trips.csv" +$logsDir = if ($paths -and $paths.PSObject.Properties['Logs'] -and $paths.Logs) { + $paths.Logs +} else { Join-Path $PSScriptRoot ".." "logs" } +if (-not (Test-Path $logsDir)) { New-Item -ItemType Directory -Path $logsDir | Out-Null } +$transcriptPath = Join-Path $logsDir "build-trips-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" +Start-Transcript -Path $transcriptPath -Append | Out-Null + +Write-Information "[Build-Trips] Starting — reports: $reportsDir" -InformationAction Continue + +try { + $configErrors = @() if (-not $Folder) { $configErrors += "settings.json: 'Paths.Source' (or 'Folder') is required" } if (-not (Test-Path $logFile)) { $configErrors += "rename-log.csv not found (run Rename-Photos.ps1 first): $logFile" } @@ -186,6 +197,7 @@ foreach ($row in $rawLog) { $odom = 0 [int]::TryParse($row.Odometer, [ref]$odom) | Out-Null + Write-Verbose " Parsed entry: $($row.NewFile) | $dt | $($row.Location) | odom=$odom | conf=$($row.OdometerConfidence)" $entries += [PSCustomObject]@{ File = $row.NewFile @@ -199,7 +211,7 @@ foreach ($row in $rawLog) { # Sort by timestamp $entries = $entries | Sort-Object DateTime -Write-Host "Loaded $($entries.Count) log entries. Building trips..." +Write-Information "Loaded $($entries.Count) log entries. Building trips..." -InformationAction Continue # --------------------------------------------------------------------------- # Deduplication pre-pass @@ -217,7 +229,8 @@ foreach ($entry in $entries) { $sameOdom = $entry.Odometer -eq $prevEntry.Odometer if ($sameLocation -and $sameOdom -and $gap -lt $DuplicateWindowSeconds) { - Write-Host " Deduplicating $($entry.File) (duplicate of $($prevEntry.File), $([int]$gap)s gap)" + Write-Verbose " Dedup check: same=$sameLocation/$sameOdom gap=$([int]$gap)s — removing $($entry.File)" + Write-Information " Deduplicating $($entry.File) (duplicate of $($prevEntry.File), $([int]$gap)s gap)" -InformationAction Continue $dupCount++ continue } @@ -227,7 +240,7 @@ foreach ($entry in $entries) { } if ($dupCount -gt 0) { - Write-Host " Removed $dupCount duplicate(s). Proceeding with $($deduped.Count) entries." + Write-Information " Removed $dupCount duplicate(s). Proceeding with $($deduped.Count) entries." -InformationAction Continue } # --------------------------------------------------------------------------- @@ -283,6 +296,7 @@ foreach ($entry in $deduped) { # --- Different locations: this is a trip -------------------------------- $odomDelta = $entry.Odometer - $pending.Odometer $expectedDist = Get-ExpectedDistance $pending.Location $entry.Location $locationMap $RoadFactor + Write-Verbose " Pairing: '$($pending.Location)' -> '$($entry.Location)' | odomDelta=$odomDelta | expectedDist=$expectedDist" $ocrBad = ($pending.OdometerConfidence -ne "ok") -or ($entry.OdometerConfidence -ne "ok") $unknownLoc = ($pending.Location -eq "Unknown") -or ($entry.Location -eq "Unknown") @@ -318,6 +332,7 @@ foreach ($entry in $deduped) { if ($expectedDist -gt 0 -and -not $ocrBad -and -not $unknownLoc) { $deviation = [Math]::Abs($odomDelta - $expectedDist) / $expectedDist + Write-Verbose " Distance check: deviation=$([Math]::Round($deviation*100,1))% tolerance=$([Math]::Round($TolerancePct*100,0))%" if ($deviation -le $TolerancePct) { $status = "auto" } @@ -328,6 +343,7 @@ foreach ($entry in $deduped) { elseif ($expectedDist -lt 0) { $notes += "no route data for this location pair" } + Write-Verbose " Trip status: $status$(if ($notes) { ' — ' + ($notes -join '; ') })" $trips += [PSCustomObject]@{ Date = Format-TripDate $pending.DateTime @@ -376,18 +392,24 @@ $review = ($trips | Where-Object { $_.Status -eq "review" }).Count $unpaired = ($trips | Where-Object { $_.Status -eq "unpaired" }).Count $stops = ($trips | Where-Object { $_.Status -eq "stop" }).Count -Write-Host " Auto-matched : $auto" -Write-Host " Needs review : $review" -Write-Host " Unpaired : $unpaired" -Write-Host " Stops (ref) : $stops" +Write-Information " Auto-matched : $auto" -InformationAction Continue +Write-Information " Needs review : $review" -InformationAction Continue +Write-Information " Unpaired : $unpaired" -InformationAction Continue +Write-Information " Stops (ref) : $stops" -InformationAction Continue # Export CSV if ($PSCmdlet.ShouldProcess($tripsOut, "Write trips CSV")) { $trips | Export-Csv -Path $tripsOut -NoTypeInformation -Encoding utf8 - Write-Host "" - Write-Host "Trips written to: $tripsOut" + Write-Information "" -InformationAction Continue + Write-Information "Trips written to: $tripsOut" -InformationAction Continue if ($review -gt 0 -or $unpaired -gt 0) { - Write-Host "" - Write-Host "Open trips.csv and manually complete rows where Status = 'review' or 'unpaired'." + Write-Information "" -InformationAction Continue + Write-Information "Open trips.csv and manually complete rows where Status = 'review' or 'unpaired'." -InformationAction Continue } } + +Write-Information "[Build-Trips] Done." -InformationAction Continue + +} finally { + Stop-Transcript | Out-Null +} diff --git a/scripts/Rename-Photos.ps1 b/scripts/Rename-Photos.ps1 index 6333b77..e4c2d89 100644 --- a/scripts/Rename-Photos.ps1 +++ b/scripts/Rename-Photos.ps1 @@ -232,6 +232,17 @@ $reportsDir = if ($paths -and $paths.PSObject.Properties['Reports'] -and $paths. } else { Join-Path $PSScriptRoot ".." "logs" } $logFile = Join-Path $reportsDir "rename-log.json" +$logsDir = if ($paths -and $paths.PSObject.Properties['Logs'] -and $paths.Logs) { + $paths.Logs +} else { Join-Path $PSScriptRoot ".." "logs" } +if (-not (Test-Path $logsDir)) { New-Item -ItemType Directory -Path $logsDir | Out-Null } +$transcriptPath = Join-Path $logsDir "rename-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" +Start-Transcript -Path $transcriptPath -Append | Out-Null + +Write-Information "[Rename-Photos] Starting — source: $Folder" -InformationAction Continue + +try { + $logEntries = @() $lastOdometer = $null $gapSinceLastGood = 0 @@ -254,7 +265,7 @@ if (Test-Path $logFile) { $photos = Get-ChildItem -Path $Folder -Filter "IMG_*.jpg" if ($photos.Count -eq 0) { - Write-Host "No IMG_*.jpg files found in $Folder" + Write-Information "No IMG_*.jpg files found in $Folder" -InformationAction Continue exit 0 } @@ -262,15 +273,16 @@ foreach ($file in $photos) { # Skip already-renamed files (pattern: yyMMdd-hhmm ...) if ($file.Name -match '^\d{6}-\d{4} ') { - Write-Host "Skipping already renamed: $($file.Name)" + Write-Warning " Already renamed, skipping: $($file.Name)" continue } - Write-Host "Processing $($file.Name)..." + Write-Information "Processing $($file.Name)..." -InformationAction Continue # --- EXIF: date/time and GPS ---------------------------------------- $exifOut = & $ExifToolPath -s3 -DateTimeOriginal -GPSLatitude# -GPSLongitude# "$($file.FullName)" 2>&1 $exifLines = @($exifOut | Where-Object { $_ -match '\S' }) + Write-Verbose " EXIF raw output ($($exifLines.Count) lines): $($exifLines -join ' | ')" if ($exifLines.Count -lt 1 -or $exifLines[0] -notmatch '^\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}') { Write-Warning " No DateTimeOriginal - skipping $($file.Name)" @@ -279,6 +291,7 @@ foreach ($file in $photos) { } $dateTimeRaw = $exifLines[0].Trim() # e.g. "2026:03:01 14:32:15" + Write-Verbose " EXIF DateTimeOriginal: $dateTimeRaw" if ($dateTimeRaw -match '^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2})') { $yy = $matches[1].Substring(2, 2) $mm = $matches[2] @@ -301,12 +314,15 @@ foreach ($file in $photos) { if ($exifLines.Count -ge 3) { $latStr = $exifLines[1].Trim() $lonStr = $exifLines[2].Trim() + Write-Verbose " GPS raw: lat=$latStr lon=$lonStr" if ($latStr -match '^-?\d+(\.\d+)?$' -and $lonStr -match '^-?\d+(\.\d+)?$') { $gpsLat = $latStr $gpsLon = $lonStr $match = Get-NearestLocation -Lat ([double]$latStr) -Lon ([double]$lonStr) ` -Locations $locations -ThresholdMiles $ProximityThresholdMiles if ($match) { + $matchDist = Get-HaversineDistance ([double]$latStr) ([double]$lonStr) $match.lat $match.lon + Write-Verbose " GPS matched '$($match.name)' at $([Math]::Round($matchDist,3)) mi" $locationName = $match.name } else { @@ -327,9 +343,11 @@ foreach ($file in $photos) { # --- OCR: odometer reading ------------------------------------------ $ocr = Get-OdometerReading -ImagePath $file.FullName + Write-Verbose " OCR result: reading=$($ocr.Reading) confidence=$($ocr.Confidence)" if ($ocr.Confidence -eq 'ok') { $reading = [int]$ocr.Reading $maxDelta = ($gapSinceLastGood + 1) * $MaxTripMiles + Write-Verbose " Odometer check: read=$reading prev=$lastOdometer maxDelta=$maxDelta" if ($null -ne $lastOdometer -and ($reading -lt $lastOdometer -or $reading -gt ($lastOdometer + $maxDelta))) { Write-Warning " Odometer suspect: read $reading, previous was $lastOdometer (max delta $maxDelta mi)" $ocr = @{ Reading = "00000"; Confidence = "suspect:got=$reading,prev=$lastOdometer" } @@ -362,7 +380,7 @@ foreach ($file in $photos) { # --- Rename and log ------------------------------------------------- if ($PSCmdlet.ShouldProcess($file.FullName, "Rename to $newName")) { Rename-Item -Path $file.FullName -NewName $newName - Write-Host " Renamed -> $newName" + Write-Information " Renamed -> $newName" -InformationAction Continue $renamedCount++ $logEntries += [PSCustomObject]@{ @@ -380,14 +398,19 @@ foreach ($file in $photos) { } $processedCount = @($photos | Where-Object { $_.Name -notmatch '^\d{6}-\d{4} ' }).Count -Write-Host "" -Write-Host "--- Summary ---" -Write-Host " Processed : $processedCount" -Write-Host " Renamed : $renamedCount" -Write-Host " Skipped : $($skipped.Count)" +Write-Information "" -InformationAction Continue +Write-Information "--- Summary ---" -InformationAction Continue +Write-Information " Processed : $processedCount" -InformationAction Continue +Write-Information " Renamed : $renamedCount" -InformationAction Continue +Write-Information " Skipped : $($skipped.Count)" -InformationAction Continue if ($skipped.Count -gt 0) { foreach ($s in $skipped) { - Write-Host " - $($s.File): $($s.Reason)" + Write-Information " - $($s.File): $($s.Reason)" -InformationAction Continue } } -Write-Host " Log : $logFile" +Write-Information " Log : $logFile" -InformationAction Continue +Write-Information "[Rename-Photos] Done." -InformationAction Continue + +} finally { + Stop-Transcript | Out-Null +}