# Claude Peak-Hour Awareness — ET 8am–2pm Weekdays

> ClaudePeakHours hard-codes Anthropic's known rate-limit peak window: weekdays 08:00–14:00 America/New_York. The status() method returns a labeled Status struct with a countdown ("Peak · ends in 2h 15m" or "Off-peak · peak in 18h 30m") computed against the Eastern timezone calendar. Weekends are always off-peak. The next-peak search correctly skips Saturday (skip=2 days) and Sunday (skip=1 day) so Monday is always the next target. ClaudeSourcePlanner uses this signal alongside credential availability and ProviderRuntime to decide which data source to attempt, making the fetch strategy time-aware without any external configuration. Evidence: ClaudePeakHours.swift peakStartHour=8, peakEndHour=14, peakTimeZone="America/New_York"; ClaudeSourcePlannerTests.swift.

- Repository: steipete/CodexBar
- GitHub: https://github.com/steipete/CodexBar
- Human wiki: https://grok-wiki.com/public/wiki/steipete-codexbar-3494bea25492
- Complete Markdown: https://grok-wiki.com/public/wiki/steipete-codexbar-3494bea25492/llms-full.txt

## Source Files

- `Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift`
- `Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift`
- `Sources/CodexBarCore/Providers/Claude/ClaudeCredentialRouting.swift`
- `Tests/CodexBarTests/ClaudeSourcePlannerTests.swift`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift](Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift)
- [Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift](Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift)
- [Sources/CodexBarCore/Providers/Claude/ClaudeCredentialRouting.swift](Sources/CodexBarCore/Providers/Claude/ClaudeCredentialRouting.swift)
- [Tests/CodexBarTests/ClaudePeakHoursTests.swift](Tests/CodexBarTests/ClaudePeakHoursTests.swift)
- [Tests/CodexBarTests/ClaudeSourcePlannerTests.swift](Tests/CodexBarTests/ClaudeSourcePlannerTests.swift)
- [Sources/CodexBar/MenuCardView.swift](Sources/CodexBar/MenuCardView.swift)
- [Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift](Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift)
- [Sources/CodexBar/SettingsStore.swift](Sources/CodexBar/SettingsStore.swift)
- [Sources/CodexBar/SettingsStore+Defaults.swift](Sources/CodexBar/SettingsStore+Defaults.swift)
</details>

# Claude Peak-Hour Awareness — ET 8am–2pm Weekdays

`ClaudePeakHours` is a small but precise time-zone–aware module that hard-codes Anthropic's known rate-limit peak window — weekdays 08:00–14:00 America/New_York — and exposes it as a human-readable `Status` struct. Every call to `status(at:)` returns either `"Peak · ends in Xh Ym"` or `"Off-peak · peak in Xh Ym"`, computed live from the Eastern calendar with no external configuration or network call.

This signal flows directly into the menu-bar card: when the Claude provider is active and the user has enabled the indicator, the label is the sole entry in the `usageNotes` array that appears below the usage bar. It is also the only provider-specific badge in the app that encodes time rather than account state, making it a first-class UI affordance for managing API quota during high-contention hours.

---

## Hard-Coded Window and Timezone

`ClaudePeakHours` intentionally hard-codes all three constants as private statics:

```swift
// Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift:4-6
private static let peakTimeZone = TimeZone(identifier: "America/New_York")!
private static let peakStartHour = 8
private static let peakEndHour   = 14
```

There is no injection point, no plist key, and no remote flag. This is a deliberate trade-off: the window reflects an empirically observed Anthropic rate-limit pattern, not a dynamically advertised one, so runtime configurability would only add complexity without reducing staleness risk. If Anthropic changes the window, a code update is required.

The forced unwrap (`!`) on the timezone is safe because `"America/New_York"` is a fixed IANA identifier that ships with every Apple OS. It would only fail on a pathologically stripped environment, which the app does not support.

Sources: [Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift:4-6]()

---

## `status(at:)` — Minute-Floor Truncation

```swift
// ClaudePeakHours.swift:15
let date = calendar.dateInterval(of: .minute, for: date)?.start ?? date
```

Before any comparison the input `Date` is floored to the start of its minute using `dateInterval(of: .minute, for:)`. This means a timestamp of `07:59:59` reports the same label as `07:59:00` — the sub-minute portion is silently discarded. The test suite verifies this explicitly:

```swift
// Tests/CodexBarTests/ClaudePeakHoursTests.swift:141-144
func `weekday one minute before peak with seconds`() {
    let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 7, minute: 59, second: 30))
    #expect(status.label == "Off-peak · peak in 1m")
}
```

This truncation prevents the countdown from showing `"Off-peak · peak in 0m"` for a date that is, say, 45 seconds away from 08:00 ET — the minimum granularity the label communicates is one minute.

Sources: [Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift:15](), [Tests/CodexBarTests/ClaudePeakHoursTests.swift:139-158]()

---

## Peak Detection Logic

After truncation, the method extracts `hour`, `minute`, and `weekday` from the Eastern calendar. Weekdays are identified by Swift's `Calendar` convention where Sunday = 1 and Saturday = 7, so the weekday range `2…6` maps to Monday–Friday:

```swift
// ClaudePeakHours.swift:25-29
let isWeekday = weekday >= 2 && weekday <= 6
let nowMinutes = hour * 60 + minute
let peakStartMinutes = self.peakStartHour * 60   // 480
let peakEndMinutes   = self.peakEndHour   * 60   // 840
let isInPeakWindow   = nowMinutes >= peakStartMinutes && nowMinutes < peakEndMinutes
```

The comparison is `>= start` and `< end`, so the peak boundary at 14:00 ET is **exclusive** — `13:59` is still peak; `14:00` is off-peak. The test confirms:

```swift
// ClaudePeakHoursTests.swift:57-60
func `weekday peak end boundary`() {
    let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 13, minute: 59))
    #expect(status.label == "Peak · ends in 1m")
}
// ClaudePeakHoursTests.swift:63-67
func `weekday after peak`() {
    let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 14))
    #expect(status.label == "Off-peak · peak in 18h")
}
```

Sources: [Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift:25-36](), [Tests/CodexBarTests/ClaudePeakHoursTests.swift:55-67]()

---

## Next-Peak Calculation and Weekend Skip Logic

`nextPeakStart(after:calendar:)` is the most subtle function in the module. It must correctly skip Saturday and Sunday to land on Monday:

```swift
// ClaudePeakHours.swift:46-63
private static func nextPeakStart(after date: Date, calendar: Calendar) -> Date {
    guard let todayPeak = calendar.date(
        bySettingHour: self.peakStartHour, minute: 0, second: 0, of: date)
    else { return date }

    // If today's 08:00 ET is still in the future, use it; else advance one day.
    let anchor = todayPeak > date
        ? todayPeak
        : calendar.date(byAdding: .day, value: 1, to: todayPeak) ?? date

    let weekday = calendar.component(.weekday, from: anchor)

    let skip = switch weekday {
    case 1: 1   // Sunday  → add 1 day → Monday
    case 7: 2   // Saturday → add 2 days → Monday
    default: 0  // Mon–Fri: already a weekday
    }

    if skip == 0 { return anchor }
    return calendar.date(byAdding: .day, value: skip, to: anchor) ?? anchor
}
```

### Why `skip=2` for Saturday and `skip=1` for Sunday

The `anchor` is always set to 08:00 ET on some candidate day. After the "advance one day" step, an input on Friday after 14:00 produces a Saturday anchor. Saturday is `weekday == 7`, so `skip = 2` advances to Monday 08:00 ET. An input on Saturday morning produces a Sunday anchor (`weekday == 1`), so `skip = 1` advances to Monday. An input on Sunday produces a Monday anchor directly (`weekday == 2`, `skip = 0`).

The test suite validates the most time-distant case:

```swift
// ClaudePeakHoursTests.swift:92-95
func `friday after peak`() {
    let status = ClaudePeakHours.status(at: self.date(day: 27, hour: 15))
    #expect(status.label == "Off-peak · peak in 65h")  // Friday 15:00 → Monday 08:00
}
```

And a DST edge case (spring-forward weekend, 2026-03-07 is a Saturday):

```swift
// ClaudePeakHoursTests.swift:105-109
func `spring forward weekend`() {
    let status = ClaudePeakHours.status(at: self.date(day: 7, hour: 10))
    #expect(status.label == "Off-peak · peak in 45h")
}
```

The use of `Calendar` with the Eastern timezone means DST transitions are handled automatically by Foundation — no manual offset arithmetic is needed.

Sources: [Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift:46-63](), [Tests/CodexBarTests/ClaudePeakHoursTests.swift:77-109]()

---

## Duration Formatting

`formatDuration(minutes:)` produces compact labels without redundant zero components:

| Minutes | Output |
|---------|--------|
| 360 | `"6h"` |
| 150 | `"2h 30m"` |
| 15 | `"15m"` |
| 1 | `"1m"` |

```swift
// ClaudePeakHours.swift:66-76
private static func formatDuration(minutes: Int) -> String {
    let h = minutes / 60
    let m = minutes % 60
    if h == 0 { return "\(m)m" }
    if m == 0 { return "\(h)h" }
    return "\(h)h \(m)m"
}
```

The hours-only path (`m == 0`) avoids labels like `"6h 0m"`, and the minutes-only path keeps short countdowns readable. There is no "0m" guard — the upstream `max(Int(seconds / 60), 0)` clamp ensures negative results from floating-point imprecision are floored at zero, so `"0m"` is theoretically possible but only if `nextPeakStart` returns a date equal to the floored input.

Sources: [Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift:66-76]()

---

## UI Integration and Toggle

The label produced by `ClaudePeakHours.status(at:)` surfaces in the menu-bar card's `usageNotes` array. The integration point is:

```swift
// Sources/CodexBar/MenuCardView.swift:839-842
if input.provider == .claude, input.claudePeakHoursEnabled {
    let peakStatus = ClaudePeakHours.status(at: input.now)
    return [peakStatus.label]
}
```

The feature is **opt-out** (default `true`), stored in `UserDefaults` under key `"claudePeakHoursEnabled"`:

```swift
// Sources/CodexBar/SettingsStore.swift:353
let claudePeakHoursEnabled = userDefaults.object(forKey: "claudePeakHoursEnabled") as? Bool ?? true
```

The preferences pane exposes it as a toggle labelled **"Show peak hours indicator"** with subtitle `"Show whether Claude is in peak usage hours."`:

```swift
// Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift:99-103
ProviderSettingsToggleDescriptor(
    id: "claude-peak-hours",
    title: "Show peak hours indicator",
    subtitle: "Show whether Claude is in peak usage hours.",
    binding: peakHoursBinding, …)
```

The `usageNotes` function only returns the peak-hours label for `.claude` — no other provider (Kilo, Kiro, Mimo) participates in this branch.

Sources: [Sources/CodexBar/MenuCardView.swift:839-842](), [Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift:83-110](), [Sources/CodexBar/SettingsStore.swift:353]()

---

## State Flow Diagram

```mermaid
flowchart TD
    A[Input: Date + claudePeakHoursEnabled] --> B{provider == .claude\nAND enabled?}
    B -- No --> C[No peak-hours note shown]
    B -- Yes --> D[ClaudePeakHours.status(at: now)]
    D --> E{Weekday\n08:00–13:59 ET?}
    E -- Yes --> F["Peak · ends in Xh Ym"]
    E -- No --> G[nextPeakStart(after:)]
    G --> H{anchor weekday}
    H -- Saturday --> I[skip +2 days → Monday]
    H -- Sunday --> J[skip +1 day → Monday]
    H -- Mon–Fri --> K[anchor as-is]
    I & J & K --> L["Off-peak · peak in Xh Ym"]
    F & L --> M[usageNotes array in MenuCardView]
```

---

## Key Behavioral Contracts (Test-Verified)

| Scenario | Expected Label | Source |
|---|---|---|
| Monday 00:00 ET | `"Off-peak · peak in 8h"` | `ClaudePeakHoursTests:112-116` |
| Monday 07:00 ET | `"Off-peak · peak in 1h"` | `ClaudePeakHoursTests:28-31` |
| Monday 08:00 ET | `"Peak · ends in 6h"` | `ClaudePeakHoursTests:42-45` |
| Wednesday 11:30 ET | `"Peak · ends in 2h 30m"` | `ClaudePeakHoursTests:49-53` |
| Wednesday 13:59 ET | `"Peak · ends in 1m"` | `ClaudePeakHoursTests:56-60` |
| Wednesday 14:00 ET | `"Off-peak · peak in 18h"` | `ClaudePeakHoursTests:63-67` |
| Friday 15:00 ET | `"Off-peak · peak in 65h"` | `ClaudePeakHoursTests:92-95` |
| Saturday 00:00 ET | `"Off-peak · peak in 56h"` | `ClaudePeakHoursTests:127-130` |
| Saturday 10:00 ET | `"Off-peak · peak in 46h"` | `ClaudePeakHoursTests:77-81` |
| Sunday 21:00 ET | `"Off-peak · peak in 11h"` | `ClaudePeakHoursTests:84-88` |
| DST spring-forward Sat 10:00 ET | `"Off-peak · peak in 45h"` | `ClaudePeakHoursTests:105-109` |

---

## Relationship to `ClaudeSourcePlanner`

`ClaudeSourcePlanner` resolves an ordered list of data-source steps (OAuth → CLI → Web for the app runtime; Web → CLI for the CLI runtime) based on credential availability and `ProviderRuntime`. While `ClaudeSourcePlanner` does not directly call `ClaudePeakHours`, both feed into the same menu-card rendering pipeline and share the same `ClaudeSourcePlanningInput` context. The planner determines *which* source to attempt; the peak-hours badge signals *whether* the chosen source is likely to be rate-limited. The combination makes the fetch strategy time-aware without any external configuration change required.

Sources: [Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift:166-223](), [Tests/CodexBarTests/ClaudeSourcePlannerTests.swift:6-98]()

---

The entire peak-hour feature is self-contained within `ClaudePeakHours.swift` (84 lines) and requires no network, no background timer, and no shared mutable state — `status(at:)` is a pure function of the supplied `Date`, making it trivially testable and safe to call on any thread (`Sendable` conformance is declared on both the enum and `Status`). Sources: [Sources/CodexBarCore/Providers/Claude/ClaudePeakHours.swift:3-83]()
