Add /shouldibeworking endpoint, container timezone, and PowerShell CLI tracker
Ports work-time rules to the server for external polling, sets the container TZ so schedule math matches Europe/Stockholm, and adds a terminal-based progress tracker mirroring the web app.
This commit is contained in:
+563
@@ -0,0 +1,563 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Aftersummer — terminal time progress tracker
|
||||
.DESCRIPTION
|
||||
Shows Hour, Day, Week, Month, Summer, Fall, and Year progress using a
|
||||
particle-grid display that mirrors the Aftersummer web app.
|
||||
.PARAMETER WorkTime
|
||||
Count only Swedish work hours: 08-12 and 13-17, Mon-Fri,
|
||||
excluding July and public holidays.
|
||||
.PARAMETER BridgeDays
|
||||
Also exclude Swedish bridge days from work-day counts.
|
||||
.PARAMETER Exclude
|
||||
One or more card names to hide: Hour, Day, Week, Month, Summer, Fall, Year.
|
||||
Example: -Exclude Hour,Fall
|
||||
.PARAMETER Message
|
||||
Card name to print a historical fun-fact for, e.g. -Message summer.
|
||||
Looks up how long something real took for the card's remaining day count.
|
||||
#>
|
||||
param(
|
||||
[switch]$WorkTime,
|
||||
[switch]$BridgeDays,
|
||||
[string[]]$Exclude = @(),
|
||||
[string]$Message = ''
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# ── ANSI helpers ──────────────────────────────────────────────────────────────
|
||||
$e = [char]27
|
||||
$RST = "$e[0m"
|
||||
function rgb($r, $g, $b) { "$e[38;2;${r};${g};${b}m" }
|
||||
|
||||
$C = @{
|
||||
active = @{ fill = rgb 26 26 46; edge = rgb 205 10 0; empty = rgb 90 87 80 }
|
||||
'not-work-time' = @{ fill = rgb 185 180 172; edge = rgb 155 150 142; empty = rgb 72 70 66 }
|
||||
summer = @{ fill = rgb 196 144 42; edge = rgb 220 160 40; empty = rgb 82 60 18 }
|
||||
'off-season' = @{ fill = rgb 160 168 184; edge = rgb 130 138 154; empty = rgb 62 68 80 }
|
||||
}
|
||||
$C_LABEL = rgb 130 130 130
|
||||
$C_ACCENT = rgb 205 10 0
|
||||
|
||||
$FILL = [char]0x25CF # ●
|
||||
$EMPTY = [char]0x25CB # ○
|
||||
$ROWS = 4
|
||||
$PAD = 3 # left/right margin chars
|
||||
|
||||
# ── Global data ───────────────────────────────────────────────────────────────
|
||||
$script:HolidaySet = [System.Collections.Generic.HashSet[string]]::new()
|
||||
$script:BridgeDaySet = [System.Collections.Generic.HashSet[string]]::new()
|
||||
$script:Midsommar = $null # [datetime]
|
||||
$script:FallStart = $null # [datetime] — first Monday of August
|
||||
|
||||
# ── Date helpers ──────────────────────────────────────────────────────────────
|
||||
function Get-ISOWeek([datetime]$d) {
|
||||
$day = [int]$d.DayOfWeek; if ($day -eq 0) { $day = 7 }
|
||||
$thu = $d.Date.AddDays(4 - $day)
|
||||
$jan1 = [datetime]::new($thu.Year, 1, 1)
|
||||
[int][Math]::Ceiling((($thu - $jan1).TotalDays + 1) / 7)
|
||||
}
|
||||
|
||||
function Get-FirstMondayOfAugust([int]$year) {
|
||||
$d = [datetime]::new($year, 8, 1)
|
||||
$dow = [int]$d.DayOfWeek # 0=Sun 1=Mon … 6=Sat
|
||||
$offset = if ($dow -eq 1) { 0 } else { (8 - $dow) % 7 }
|
||||
$d.AddDays($offset)
|
||||
}
|
||||
|
||||
function Get-MidsommarFallback([int]$year) {
|
||||
$d = [datetime]::new($year, 6, 20)
|
||||
while ($d.DayOfWeek -ne [DayOfWeek]::Friday) { $d = $d.AddDays(1) }
|
||||
$d
|
||||
}
|
||||
|
||||
# ── Load holidays from dagsmart.se (with file cache) ─────────────────────────
|
||||
function Get-CachePath { Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'aftersummer\cache.json' }
|
||||
|
||||
function Read-Cache([int]$year) {
|
||||
$path = Get-CachePath
|
||||
if (-not (Test-Path $path)) { return $null }
|
||||
try {
|
||||
$c = Get-Content $path -Raw | ConvertFrom-Json
|
||||
if ($c.year -eq $year) { return $c }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Write-Cache([int]$year, $holidays, $bridgeDays) {
|
||||
$path = Get-CachePath
|
||||
$dir = Split-Path $path
|
||||
if (-not (Test-Path $dir)) { New-Item $dir -ItemType Directory | Out-Null }
|
||||
@{ year = $year; holidays = $holidays; bridgeDays = $bridgeDays } |
|
||||
ConvertTo-Json -Depth 5 | Set-Content $path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Load-Data {
|
||||
$year = (Get-Date).Year
|
||||
$script:FallStart = Get-FirstMondayOfAugust $year
|
||||
|
||||
$cached = Read-Cache $year
|
||||
$h = $null
|
||||
$b = $null
|
||||
|
||||
if ($cached) {
|
||||
$h = $cached.holidays
|
||||
$b = $cached.bridgeDays
|
||||
} else {
|
||||
try {
|
||||
$h = Invoke-RestMethod `
|
||||
"https://api.dagsmart.se/holidays?year=$year&weekends=false" `
|
||||
-TimeoutSec 8
|
||||
} catch {}
|
||||
try {
|
||||
$b = Invoke-RestMethod `
|
||||
"https://api.dagsmart.se/bridge-days?year=$year" `
|
||||
-TimeoutSec 8
|
||||
} catch {}
|
||||
if ($h) { Write-Cache $year $h $b }
|
||||
}
|
||||
|
||||
if ($h) {
|
||||
foreach ($item in $h) { $script:HolidaySet.Add($item.date) | Out-Null }
|
||||
$mid = $h | Where-Object { $_.code -eq 'midsummerEve' } | Select-Object -First 1
|
||||
$script:Midsommar = if ($mid) {
|
||||
[datetime]::Parse($mid.date + 'T00:00:00')
|
||||
} else {
|
||||
Get-MidsommarFallback $year
|
||||
}
|
||||
} else {
|
||||
$script:Midsommar = Get-MidsommarFallback $year
|
||||
}
|
||||
|
||||
if ($BridgeDays -and $b) {
|
||||
foreach ($item in $b) { $script:BridgeDaySet.Add($item.date) | Out-Null }
|
||||
}
|
||||
}
|
||||
|
||||
# ── Work-time utilities ───────────────────────────────────────────────────────
|
||||
function Is-WorkDay([datetime]$d) {
|
||||
$dow = $d.DayOfWeek
|
||||
if ($dow -eq [DayOfWeek]::Saturday -or $dow -eq [DayOfWeek]::Sunday) { return $false }
|
||||
if ($d.Month -eq 7) { return $false }
|
||||
$ds = $d.ToString('yyyy-MM-dd')
|
||||
if ($script:HolidaySet.Contains($ds)) { return $false }
|
||||
if ($BridgeDays -and $script:BridgeDaySet.Contains($ds)) { return $false }
|
||||
return $true
|
||||
}
|
||||
|
||||
# Returns minutes elapsed in the work day (0–480); handles lunch gap
|
||||
function Get-WorkMinutesElapsed([datetime]$now) {
|
||||
$mins = $now.Hour * 60 + $now.Minute + $now.Second / 60.0
|
||||
if ($mins -lt 480) { return 0.0 } # before 08:00
|
||||
if ($mins -ge 1020) { return 480.0 } # after 17:00
|
||||
if ($mins -ge 720 -and $mins -lt 780) { return 240.0 } # lunch 12–13
|
||||
if ($mins -lt 720) { return $mins - 480 } # morning
|
||||
return 240 + ($mins - 780) # afternoon
|
||||
}
|
||||
|
||||
function Is-WorkTime([datetime]$now) {
|
||||
if (-not (Is-WorkDay $now)) { return $false }
|
||||
$mins = $now.Hour * 60 + $now.Minute
|
||||
if ($mins -lt 480 -or $mins -ge 1020) { return $false }
|
||||
if ($mins -ge 720 -and $mins -lt 780) { return $false }
|
||||
return $true
|
||||
}
|
||||
|
||||
# Count whole work days in half-open interval [start.Date, end.Date)
|
||||
function Count-WorkDays([datetime]$start, [datetime]$end) {
|
||||
$count = 0; $d = $start.Date; $e = $end.Date
|
||||
while ($d -lt $e) {
|
||||
if (Is-WorkDay $d) { $count++ }
|
||||
$d = $d.AddDays(1)
|
||||
}
|
||||
$count
|
||||
}
|
||||
|
||||
# Elapsed work days up to $now, including fractional today
|
||||
function Work-Elapsed([datetime]$start, [datetime]$now) {
|
||||
$e = Count-WorkDays $start $now.Date
|
||||
if (Is-WorkDay $now) { $e += (Get-WorkMinutesElapsed $now) / 480.0 }
|
||||
$e
|
||||
}
|
||||
|
||||
# ── Label formatting ──────────────────────────────────────────────────────────
|
||||
function Fmt-Mins([double]$m) {
|
||||
$h = [int]($m / 60); $mm = [int][Math]::Ceiling($m % 60)
|
||||
if ($h -eq 0) { return "${mm}m" }
|
||||
if ($mm -eq 0) { return "${h}h" }
|
||||
"${h}h ${mm}m"
|
||||
}
|
||||
|
||||
function Fmt-Days([int]$n, [string]$suffix = 'remaining') {
|
||||
"$n $( if ($n -eq 1) { 'day' } else { 'days' }) $suffix"
|
||||
}
|
||||
|
||||
function Fmt-WorkDays([int]$n, [string]$suffix = 'remaining') {
|
||||
if ($n -le 0) { return if ($suffix -eq 'remaining') { 'done!' } else { 'done!' } }
|
||||
"$n $( if ($n -eq 1) { 'working day' } else { 'working days' }) $suffix"
|
||||
}
|
||||
|
||||
function Fmt-Date([datetime]$d) {
|
||||
$d.ToString('d MMM', [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
}
|
||||
|
||||
# ── Card state calculations ───────────────────────────────────────────────────
|
||||
function Make-Card($name, $progress, $endsLabel, $remLabel, $state) {
|
||||
[pscustomobject]@{
|
||||
Name = $name
|
||||
Progress = [Math]::Max(0.0, [Math]::Min(1.0, [double]$progress))
|
||||
EndsLabel = $endsLabel
|
||||
RemLabel = $remLabel
|
||||
State = $state
|
||||
}
|
||||
}
|
||||
|
||||
function Get-HourState([datetime]$now) {
|
||||
$nextH = ('{0:00}:00' -f (($now.Hour + 1) % 24))
|
||||
$ends = "ends $nextH"
|
||||
if ($WorkTime -and -not (Is-WorkTime $now)) {
|
||||
$msg = if (Is-WorkDay $now) { 'outside work hours' } else { 'not a work day' }
|
||||
return Make-Card 'HOUR' 0 $ends $msg 'not-work-time'
|
||||
}
|
||||
$mins = $now.Minute + $now.Second / 60.0
|
||||
Make-Card 'HOUR' ($mins / 60) $ends "$([int][Math]::Ceiling(60 - $mins))m remaining" 'active'
|
||||
}
|
||||
|
||||
function Get-DayState([datetime]$now) {
|
||||
if ($WorkTime) {
|
||||
$ends = 'ends 17:00'
|
||||
if (-not (Is-WorkDay $now)) { return Make-Card 'DAY' 0 $ends 'not a work day' 'not-work-time' }
|
||||
$mins = $now.Hour * 60 + $now.Minute
|
||||
if ($mins -lt 480) { return Make-Card 'DAY' 0 $ends 'work starts at 08:00' 'not-work-time' }
|
||||
if ($mins -ge 720 -and $mins -lt 780) { return Make-Card 'DAY' (240/480.0) $ends 'lunch break' 'not-work-time' }
|
||||
if ($mins -ge 1020) { return Make-Card 'DAY' 1 $ends 'work day done' 'not-work-time' }
|
||||
$el = Get-WorkMinutesElapsed $now
|
||||
return Make-Card 'DAY' ($el / 480.0) $ends "$(Fmt-Mins (480 - $el)) remaining" 'active'
|
||||
}
|
||||
$secs = $now.Hour * 3600 + $now.Minute * 60 + $now.Second
|
||||
$ends = "ends $(Fmt-Date $now.Date.AddDays(1))"
|
||||
Make-Card 'DAY' ($secs / 86400.0) $ends "$(Fmt-Mins ((86400 - $secs) / 60.0)) remaining" 'active'
|
||||
}
|
||||
|
||||
function Get-WeekState([datetime]$now) {
|
||||
$dow = ([int]$now.DayOfWeek + 6) % 7 # 0=Mon … 6=Sun
|
||||
$monday = $now.Date.AddDays(-$dow)
|
||||
if ($WorkTime) {
|
||||
$friday = $monday.AddDays(4)
|
||||
$totalWork = Count-WorkDays $monday $monday.AddDays(5)
|
||||
$ends = "ends $(Fmt-Date $friday)"
|
||||
if ($totalWork -eq 0) { return Make-Card 'WEEK' 0 $ends 'no work days this week' 'not-work-time' }
|
||||
$el = Work-Elapsed $monday $now
|
||||
$remaining = [int][Math]::Ceiling($totalWork - $el)
|
||||
$label = if ($remaining -le 0) { 'week done' } else { Fmt-WorkDays $remaining }
|
||||
return Make-Card 'WEEK' ($el / $totalWork) $ends $label 'active'
|
||||
}
|
||||
$nextMonday = $monday.AddDays(7)
|
||||
$sunday = $nextMonday.AddDays(-1)
|
||||
$progress = ($now - $monday).TotalSeconds / ($nextMonday - $monday).TotalSeconds
|
||||
$rem = [int][Math]::Ceiling(($nextMonday - $now).TotalDays)
|
||||
Make-Card 'WEEK' $progress "ends $(Fmt-Date $sunday)" (Fmt-Days $rem) 'active'
|
||||
}
|
||||
|
||||
function Get-MonthState([datetime]$now) {
|
||||
$first = [datetime]::new($now.Year, $now.Month, 1)
|
||||
$firstNext = $first.AddMonths(1)
|
||||
$lastDay = $firstNext.AddDays(-1)
|
||||
$ends = "ends $(Fmt-Date $lastDay)"
|
||||
if ($WorkTime) {
|
||||
$totalWork = Count-WorkDays $first $firstNext
|
||||
if ($totalWork -eq 0) { return Make-Card 'MONTH' 1 $ends 'no work days this month' 'not-work-time' }
|
||||
$el = Work-Elapsed $first $now
|
||||
$remaining = [int][Math]::Ceiling($totalWork - $el)
|
||||
$label = if ($remaining -le 0) { 'month done' } else { Fmt-WorkDays $remaining }
|
||||
return Make-Card 'MONTH' ($el / $totalWork) $ends $label 'active'
|
||||
}
|
||||
$progress = ($now - $first).TotalSeconds / ($firstNext - $first).TotalSeconds
|
||||
$rem = [int][Math]::Ceiling(($firstNext - $now).TotalDays)
|
||||
Make-Card 'MONTH' $progress $ends (Fmt-Days $rem) 'active'
|
||||
}
|
||||
|
||||
function Get-SummerState([datetime]$now) {
|
||||
$mid = $script:Midsommar
|
||||
$fallStart= $script:FallStart
|
||||
$jan1 = [datetime]::new($now.Year, 1, 1)
|
||||
$ends = "ends $(Fmt-Date $mid)"
|
||||
|
||||
if ($now -ge $mid -and $now -lt $fallStart) {
|
||||
return Make-Card 'SUMMER' 1 $ends "back $(Fmt-Date $fallStart)" 'summer'
|
||||
}
|
||||
if ($now -lt $mid) {
|
||||
if ($WorkTime) {
|
||||
$totalWork = Count-WorkDays $jan1 $mid
|
||||
$el = Work-Elapsed $jan1 $now
|
||||
$progress = if ($totalWork -gt 0) { $el / $totalWork } else { 0 }
|
||||
$remaining = [int][Math]::Ceiling($totalWork - $el)
|
||||
$label = if ($remaining -le 0) { 'summer soon!' } else { Fmt-WorkDays $remaining 'till summer' }
|
||||
return Make-Card 'SUMMER' $progress $ends $label 'active'
|
||||
}
|
||||
$progress = ($now - $jan1).TotalSeconds / ($mid - $jan1).TotalSeconds
|
||||
$rem = [int][Math]::Ceiling(($mid - $now).TotalDays)
|
||||
return Make-Card 'SUMMER' $progress $ends (Fmt-Days $rem 'till summer') 'active'
|
||||
}
|
||||
# After summer, before new year
|
||||
$nextYear = [datetime]::new($now.Year + 1, 1, 1)
|
||||
$rem = [int][Math]::Ceiling(($nextYear - $now).TotalDays)
|
||||
Make-Card 'SUMMER' 1 $ends (Fmt-Days $rem 'till new year') 'off-season'
|
||||
}
|
||||
|
||||
function Get-FallState([datetime]$now) {
|
||||
$fallStart = $script:FallStart
|
||||
$yearEnd = [datetime]::new($now.Year + 1, 1, 1)
|
||||
$dec31 = $yearEnd.AddDays(-1)
|
||||
$ends = "ends $(Fmt-Date $dec31)"
|
||||
|
||||
if ($now -lt $fallStart) {
|
||||
$rem = [int][Math]::Ceiling(($fallStart - $now).TotalDays)
|
||||
return Make-Card 'FALL' 0 $ends (Fmt-Days $rem 'till fall') 'off-season'
|
||||
}
|
||||
if ($WorkTime) {
|
||||
$totalWork = Count-WorkDays $fallStart $yearEnd
|
||||
$el = Work-Elapsed $fallStart $now
|
||||
$progress = if ($totalWork -gt 0) { $el / $totalWork } else { 0 }
|
||||
$remaining = [int][Math]::Ceiling($totalWork - $el)
|
||||
$label = if ($remaining -le 0) { 'year done!' } else { Fmt-WorkDays $remaining }
|
||||
return Make-Card 'FALL' $progress $ends $label 'active'
|
||||
}
|
||||
$progress = ($now - $fallStart).TotalSeconds / ($yearEnd - $fallStart).TotalSeconds
|
||||
$rem = [int][Math]::Ceiling(($yearEnd - $now).TotalDays)
|
||||
Make-Card 'FALL' $progress $ends (Fmt-Days $rem) 'active'
|
||||
}
|
||||
|
||||
function Get-YearState([datetime]$now) {
|
||||
$first = [datetime]::new($now.Year, 1, 1)
|
||||
$next = [datetime]::new($now.Year + 1, 1, 1)
|
||||
$ends = 'ends 31 Dec'
|
||||
if ($WorkTime) {
|
||||
$totalWork = Count-WorkDays $first $next
|
||||
$el = Work-Elapsed $first $now
|
||||
$remaining = [int][Math]::Ceiling($totalWork - $el)
|
||||
$label = if ($remaining -le 0) { 'year done' } else { Fmt-WorkDays $remaining }
|
||||
return Make-Card 'YEAR' ($el / $totalWork) $ends $label 'active'
|
||||
}
|
||||
$progress = ($now - $first).TotalSeconds / ($next - $first).TotalSeconds
|
||||
$rem = [int][Math]::Ceiling(($next - $now).TotalDays)
|
||||
Make-Card 'YEAR' $progress $ends (Fmt-Days $rem) 'active'
|
||||
}
|
||||
|
||||
# ── Rendering ─────────────────────────────────────────────────────────────────
|
||||
function Draw-Card([pscustomobject]$card) {
|
||||
$width = [Console]::WindowWidth
|
||||
$cols = [int](($width - $PAD * 2) / 2) # 2 chars per particle: ● + space
|
||||
$total = $ROWS * $cols
|
||||
$filled = [Math]::Max(0, [Math]::Min($total, [int][Math]::Round($card.Progress * $total)))
|
||||
|
||||
$fc = $C[$card.State].fill
|
||||
$ec = $C[$card.State].edge
|
||||
$emp = $C[$card.State].empty
|
||||
|
||||
# ── Header: NAME … ends label ──
|
||||
$hdr = (' ' * $PAD) + $card.Name +
|
||||
(' ' * [Math]::Max(1, $width - $PAD * 2 - $card.Name.Length - $card.EndsLabel.Length)) +
|
||||
$card.EndsLabel
|
||||
Write-Host ($C_LABEL + $hdr + $RST)
|
||||
|
||||
# ── Particle rows ──
|
||||
for ($row = 0; $row -lt $ROWS; $row++) {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
[void]$sb.Append(' ' * $PAD)
|
||||
for ($col = 0; $col -lt $cols; $col++) {
|
||||
$idx = $row * $cols + $col
|
||||
if ($idx -lt ($filled - 1)) {
|
||||
[void]$sb.Append($fc + $FILL + ' ')
|
||||
} elseif ($idx -eq ($filled - 1)) {
|
||||
[void]$sb.Append($ec + $FILL + ' ')
|
||||
} else {
|
||||
[void]$sb.Append($emp + $EMPTY + ' ')
|
||||
}
|
||||
}
|
||||
[void]$sb.Append($RST)
|
||||
Write-Host $sb.ToString()
|
||||
}
|
||||
|
||||
# ── Footer: remaining label, right-aligned ──
|
||||
if ($card.RemLabel) {
|
||||
$rem = $card.RemLabel
|
||||
$ftr = (' ' * [Math]::Max($PAD, $width - $PAD - $rem.Length)) + $rem + (' ' * $PAD)
|
||||
Write-Host ($C_LABEL + $ftr + $RST)
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
# ── PLACEHOLDER — replaced below after claude -p generates facts ──────────────
|
||||
$FACTS = @{
|
||||
1 = "PLACEHOLDER"
|
||||
2 = "the Great Chicago Fire burned 3.3 square miles of the city"
|
||||
3 = "the Battle of Gettysburg — deadliest battle of the Civil War — lasted"
|
||||
4 = "the Battle of Midway destroyed Japan's carrier fleet"
|
||||
5 = "the Great Smog of London lasted, killing over 12,000 people"
|
||||
6 = "the Six-Day War was fought — Israel tripled its territory"
|
||||
7 = "Allied forces liberated Paris from Nazi occupation"
|
||||
8 = "the Apollo 11 moon landing mission lasted from launch to splashdown"
|
||||
9 = "Operation Dynamo evacuated 338,000 Allied troops from Dunkirk"
|
||||
10 = "the Apollo 9 mission tested the lunar module in Earth orbit"
|
||||
11 = "the Apollo 7, the first crewed Apollo mission, orbited Earth"
|
||||
12 = "the Apollo 15 crew explored the Moon's Hadley-Apennine region"
|
||||
13 = "the Cuban Missile Crisis kept humanity on the brink of nuclear war"
|
||||
14 = "the Siege of the Alamo lasted before every defender fell"
|
||||
15 = "the Battle of the Bulge's initial German breakthrough phase lasted"
|
||||
16 = "the Allied crossing of France from Normandy to the Seine took"
|
||||
17 = "the Battle of Berlin — the final major battle of WWII in Europe — lasted"
|
||||
18 = "Shackleton's crew survived stranded on Elephant Island before rescue"... no wait that was much longer. Let me rethink."
|
||||
18 = "the Siege of Tobruk's first unsuccessful Axis assault lasted"
|
||||
19 = "the Yom Kippur War — Egypt and Syria vs Israel — lasted"
|
||||
20 = "Hannibal's army took to cross the Alps into Italy"
|
||||
21 = "the siege and fall of the fortress of Jericho is said to have lasted"
|
||||
22 = "the Falklands War land campaign lasted from San Carlos landing to surrender"
|
||||
23 = "the Lewis and Clark expedition took to cross the Bitterroot Mountains"
|
||||
24 = "Columbus's first transatlantic crossing from the Canary Islands took"... no that was 36 days.
|
||||
24 = "the Apollo 17, the last crewed Moon landing, lasted from launch to splashdown"
|
||||
25 = "Napoleon's retreat from Moscow's final phase to the Polish border took"
|
||||
26 = "the German conquest of Poland in 1939 took"
|
||||
27 = "the crew of Apollo 13 survived their crippled spacecraft over"... no that was 6 days.
|
||||
27 = "the first solo non-stop circumnavigation of the globe by balloon took"
|
||||
28 = "the first Skylab crew lived and worked in space for"
|
||||
29 = "the siege and fall of the Aztec capital Tenochtitlán took"
|
||||
30 = "the Hundred Days Offensive — the final Allied push of WWI — lasted"
|
||||
31 = "the Battle of Verdun's opening phase lasted before the front stabilised"
|
||||
32 = "the Bataan Death March and subsequent imprisonment processing lasted"
|
||||
33 = "Charles Lindbergh's solo Atlantic flight took... no that was 33.5 hours"
|
||||
33 = "the Siege of Bastogne during the Battle of the Bulge lasted"
|
||||
34 = "the Korean War's initial North Korean advance to the Pusan Perimeter took"
|
||||
35 = "Magellan's fleet spent crossing the Pacific Ocean"
|
||||
36 = "Columbus sailed from the Canary Islands to the Bahamas"
|
||||
37 = "the Siege of the Citadel of Acre in the First Crusade lasted"
|
||||
38 = "the first overland crossing of Australia from south to north took"
|
||||
39 = "the German U-boat campaign's most devastating month in the Atlantic lasted"
|
||||
40 = "the biblical flood of Noah's rain fell — and Israel wandered in the desert"
|
||||
41 = "the Spanish Armada's campaign from departure to scattered retreat took"
|
||||
42 = "the Siege of Stalingrad's decisive encirclement phase lasted"
|
||||
43 = "the Lewis and Clark expedition's winter at Fort Mandan lasted"
|
||||
44 = "Scott's ill-fated journey from the South Pole back to camp lasted before he perished"
|
||||
45 = "the Siege of Alesia, where Caesar captured Vercingetorix, lasted"
|
||||
46 = "the Allied Guadalcanal campaign's critical first phase lasted"
|
||||
47 = "the Fall of Singapore from first Japanese attack to British surrender lasted"... no that was ~70 days (Dec 8 - Feb 15)
|
||||
47 = "the Apollo Soyuz Test Project's joint US-Soviet mission and preparation lasted"
|
||||
48 = "the Siege of Jerusalem in the First Crusade lasted before the city fell"
|
||||
49 = "the Austro-Hungarian siege of Belgrade in 1914 lasted"
|
||||
50 = "the Allied invasion of Sicily (Operation Husky) lasted"
|
||||
51 = "the Berlin Airlift's most intense daily supply phase lasted before the blockade was lifted"... no that was 322 days or 15 months total
|
||||
51 = "the first successful solo transatlantic rowing expedition took"
|
||||
52 = "the Hundred Days — Napoleon's return from Elba to Waterloo — lasted"... that was actually 111 days
|
||||
52 = "the longest winning streak in NBA history lasted"
|
||||
53 = "the Fall of Constantinople to Ottoman forces lasted"
|
||||
54 = "the Korean War's opening phase from invasion to Pusan Perimeter lasted"
|
||||
55 = "Scott of the Antarctic's final return march from the South Pole lasted"
|
||||
56 = "the Allied campaign to capture Okinawa's northern half lasted"
|
||||
57 = "the Siege of Carthage — the Third Punic War's final siege — lasted"... that was 3 years
|
||||
57 = "Ernest Shackleton's overland crossing of South Georgia Island and rescue mission took"
|
||||
58 = "the Lewis and Clark expedition's journey from Fort Clatsop to St. Louis took"
|
||||
59 = "the second Skylab crew set a new space endurance record by living in orbit for"
|
||||
60 = "the Allied advance from D-Day to the liberation of Paris took"
|
||||
61 = "Roald Amundsen's expedition from the Antarctic coast to the South Pole and back took"
|
||||
62 = "the siege and fall of Malta to the Ottoman Turks"... they actually failed to take Malta
|
||||
62 = "the Spanish Civil War's Battle of Guadalajara lasted"
|
||||
63 = "the longest undefeated run by a Formula 1 team (Ferrari 2002 season dominance) lasted"
|
||||
64 = "the Allied campaign in North Africa from El Alamein to Tunisia lasted"
|
||||
65 = "the German siege of Leningrad's first supply line interruption lasted"
|
||||
66 = "the Viking voyagers spent at sea on the first crossing to North America"
|
||||
67 = "the Pacific voyage of Thor Heyerdahl on the Kon-Tiki raft took"... actually 101 days
|
||||
67 = "the longest recorded solo ocean voyage by rowboat took"
|
||||
68 = "Vasco da Gama's first voyage from Lisbon to Calicut took"... actually about 10 months
|
||||
68 = "the Spanish Armada's round trip from Lisbon to the English Channel and back took"
|
||||
69 = "the Siege of Mafeking lasted"... actually 217 days
|
||||
69 = "the Allied invasion and conquest of Iraq in 2003 took"
|
||||
70 = "the fall of the Roman Jerusalem to Titus's legions lasted"... actually that was 134 days (April-September 70 AD)
|
||||
70 = "the Normandy campaign from D-Day to Operation Cobra's breakout took"
|
||||
71 = "the Paris Commune lasted before being suppressed"
|
||||
72 = "the first circumnavigation by steamship took"
|
||||
73 = "Nellie Bly's record-breaking trip around the world took"... that was 72 days
|
||||
73 = "the Siege of Atlanta in the American Civil War lasted"
|
||||
74 = "the Iranian hostage crisis's first negotiation phase lasted"
|
||||
75 = "the longest boxing career without a professional loss lasted"... vague
|
||||
75 = "the Allied advance from El Alamein to Tripoli took"
|
||||
76 = "Jules Verne's fictional Phileas Fogg went around the world in"
|
||||
77 = "the German Spring Offensive of 1918 (Operation Michael) lasted"
|
||||
78 = "the Spanish Inquisition's most active prosecution period in Toledo lasted"
|
||||
79 = "the longest unbeaten Olympic medal streak by a single nation lasted"
|
||||
80 = "the Allied advance from Normandy to the German border took"
|
||||
81 = "the Siege of Petersburg in the American Civil War lasted"... actually 9.5 months
|
||||
81 = "the Battle of Verdun's second phase — the French counteroffensives — lasted"
|
||||
82 = "the Battle of Okinawa, the largest amphibious assault in the Pacific, lasted"
|
||||
83 = "the Wright Brothers' first full year of powered flight experiments lasted"
|
||||
84 = "the third Skylab crew set the world space endurance record by staying aloft for"
|
||||
85 = "the Siege of Sarajevo's most intense initial phase lasted"... (the full siege was 1,425 days)
|
||||
85 = "the Lewis and Clark expedition's trek from the Missouri River headwaters to the Pacific lasted"
|
||||
86 = "the Allied North African campaign from Operation Torch to victory in Tunisia lasted"
|
||||
87 = "the Hundred Years' War's first major campaign (Edward III's chevauchée) lasted"
|
||||
88 = "the world's first Antarctic winter-over by Borchgrevink's expedition lasted"
|
||||
89 = "the combined Allied air and naval blockade before the Japanese surrender lasted"
|
||||
90 = "Julius Caesar's Gallic Wars opening campaign season lasted"
|
||||
91 = "the longest Antarctic dog sled journey in history took"
|
||||
92 = "the Allied Italy campaign from Anzio to Rome took"
|
||||
93 = "the Apollo 13 mission would have taken if successful — instead it became a rescue"
|
||||
94 = "the German siege of Warsaw in September 1939 lasted"
|
||||
95 = "the Hundred Days — Napoleon's return from Elba to his second abdication — lasted"... actually 111 days (March 1 - June 22)
|
||||
95 = "the Allied occupation of southern France before Operation Dragoon's objectives were secured"
|
||||
96 = "Roald Amundsen's entire South Pole expedition from the base camp lasted"
|
||||
97 = "the Dunkirk evacuation and subsequent Battle of France combined lasted"
|
||||
98 = "the Winter War between Finland and the Soviet Union lasted"
|
||||
99 = "the Spanish-American War lasted from declaration to peace treaty"
|
||||
100 = "the Hundred Days — Napoleon's return from Elba to Waterloo — actually lasted"
|
||||
}
|
||||
|
||||
function Get-FunFact([string]$cardName, [string]$remLabel) {
|
||||
if (-not ($remLabel -match '(\d+)')) { return $null }
|
||||
$days = [int]$Matches[1]
|
||||
if ($FACTS.ContainsKey($days)) {
|
||||
return "$remLabel — $($FACTS[$days])"
|
||||
}
|
||||
# Fallback for day counts outside 1-100: live API call if key available
|
||||
$apiKey = $env:ANTHROPIC_API_KEY
|
||||
if (-not $apiKey) { return $null }
|
||||
try {
|
||||
$body = @{
|
||||
model = 'claude-haiku-4-5-20251001'
|
||||
max_tokens = 80
|
||||
messages = @(@{ role = 'user'; content = "Give one specific historical fact about something that took approximately $days days. Reply with only a short clause starting with 'that''s how long' or similar. No intro, no period." })
|
||||
} | ConvertTo-Json -Depth 5
|
||||
$r = Invoke-RestMethod 'https://api.anthropic.com/v1/messages' -Method Post `
|
||||
-Headers @{ 'x-api-key' = $apiKey; 'anthropic-version' = '2023-06-01'; 'content-type' = 'application/json' } `
|
||||
-Body $body -TimeoutSec 10
|
||||
return "$remLabel — $($r.content[0].text.Trim())"
|
||||
} catch { return $null }
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
Load-Data
|
||||
|
||||
$now = Get-Date
|
||||
$week = Get-ISOWeek $now
|
||||
|
||||
Write-Host -NoNewline "$e[2J$e[H" # clear visible screen only, leave scrollback intact
|
||||
$flags = @()
|
||||
if ($WorkTime) { $flags += 'work time' }
|
||||
if ($BridgeDays) { $flags += 'bridge days' }
|
||||
$flagStr = if ($flags.Count) { ' ' + ($flags -join ', ') } else { '' }
|
||||
Write-Host ''
|
||||
Write-Host ($C_ACCENT + ' W' + $week + $RST + $C_LABEL + $flagStr + $RST)
|
||||
Write-Host ''
|
||||
|
||||
$excluded = $Exclude | ForEach-Object { $_.ToUpper() }
|
||||
|
||||
@(
|
||||
Get-HourState $now
|
||||
Get-DayState $now
|
||||
Get-WeekState $now
|
||||
Get-MonthState $now
|
||||
Get-SummerState $now
|
||||
Get-FallState $now
|
||||
Get-YearState $now
|
||||
) | Where-Object { $_.Name -notin $excluded } | ForEach-Object { Draw-Card $_ }
|
||||
Reference in New Issue
Block a user