Skip to content

Consolidate native apps into one monorepo - #1

Merged
zackbart merged 140 commits into
mainfrom
agent/consolidate-native-apps
Aug 3, 2026
Merged

Consolidate native apps into one monorepo#1
zackbart merged 140 commits into
mainfrom
agent/consolidate-native-apps

Conversation

@zackbart

@zackbart zackbart commented Aug 3, 2026

Copy link
Copy Markdown
Owner

What changed

  • Imported Barr, Windo, Loadout, Livewall, MrMouse, Herdr iOS, and JellyTV under app-specific directories with complete Git history.
  • Added a root catalog documenting platforms and release conventions.
  • Moved Barr CI to the repository-level workflows directory.
  • Replaced three standalone macOS release workflows with one app-aware workflow.
  • Namespaced future release tags (barr-v*, windo-v*, and loadout-v*) to prevent collisions.
  • Preserved existing standalone repositories and historical releases; none are archived by this PR.

Why

The apps were spread across standalone repositories even though they share ownership, native tooling, Apple signing credentials, and Homebrew distribution. This establishes zackbart/apps as their common source and publishing home without breaking existing release URLs during migration.

Publishing impact

The new release workflow builds, signs, notarizes, publishes the selected DMG, and updates its Homebrew cask. Before the first monorepo release, the seven existing Actions secrets must be recreated on zackbart/apps because GitHub does not expose secret values for copying.

Validation

  • actionlint .github/workflows/*.yml
  • git diff --check
  • XcodeGen project generation for Barr, Windo, Loadout, and Herdr iOS
  • swift build for HerdrKit, JellyfinAPI, LiveTV, and Loadout's HerdrKit

Full Xcode tests were not available locally because the machine currently has Command Line Tools, not the full Xcode application. Barr CI will exercise the native test suite on GitHub's macos-26 runner.

zackbart and others added 30 commits April 3, 2026 12:15
Lightweight replacement for Logi Options+ with HID++ protocol support,
gesture recognition via CGEventTap, and macOS Tahoe-compatible action dispatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove archive/ directory (dead SPM-based code, superseded by Xcode project)
- Fix hardcoded log path — now writes to ~/Library/Logs/MrMouse/
- Add MIT license
- Add CONTRIBUTING.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fresh SwiftUI tvOS app scaffold ("Jelly TV") plus softplan.md capturing
the research synthesis and phased plan for a native tvOS Jellyfin client
targeting Apple TV 4K on tvOS 18. Plan covers the architecture decisions
(AVPlayerViewController, hand-rolled JellyfinClient actor, SwiftData,
Nuke, Keychain), the DeviceProfile shape, tvOS-specific gotchas, and a
"must not regress" list drawn from current SwiftFin tvOS 1.0.1 bugs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Outer dir, project bundle, inner source dir, and the @main struct all
move off the literal "Jelly TV" name to eliminate shell-quoting friction
on every xcodebuild/grep/find command. Display name "Jelly TV" is
preserved via INFOPLIST_KEY_CFBundleDisplayName so the on-screen label
doesn't change. PRODUCT_BUNDLE_IDENTIFIER stays com.cursorkittens.Jelly-TV
to avoid signing/provisioning churn.

Also bumps SWIFT_VERSION 5.0 → 6.0 in both Debug and Release so the app
target compiles under the same Swift 6 strict-concurrency mode the
upcoming SPM packages will use, avoiding cross-boundary Sendable
warnings at the import site. Adds .motif/ to .gitignore for the active
workflow state directory.

Verified with xcodebuild against Apple TV 4K (3rd generation) Simulator,
tvOS 26.2 — BUILD SUCCEEDED.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds JellyTV/Info.plist with NSLocalNetworkUsageDescription,
NSAppTransportSecurity.NSAllowsLocalNetworking=YES (allows plain HTTP to
RFC1918/.local addresses, sufficient for v1 LAN-only scope), and
NSBonjourServices=[_jellyfin._tcp] for future mDNS discovery.

Kept GENERATE_INFOPLIST_FILE=YES so Xcode still synthesizes
CFBundleDisplayName, CFBundleIdentifier, etc. from build settings —
INFOPLIST_FILE merges as the base layer. Plist lives at JellyTV/Info.plist
(next to .xcodeproj, outside the synchronized source group) so the
PBXFileSystemSynchronizedRootGroup doesn't double-process it as a
resource and an info plist.

Verified the built app's merged Info.plist contains all expected keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Creates Packages/{JellyfinAPI,Persistence,DesignSystem,Library,Player,Settings}
as sibling local packages. Each is swift-tools-version 6.2 with
.tvOS(.v26) platform, a single library target, and a placeholder
namespace enum so the module compiles. Library depends on
JellyfinAPI+DesignSystem; Settings depends on JellyfinAPI+Persistence+
DesignSystem. Other dependencies will be wired in their phases.

All 6 packages compile independently via swift build with cross-package
local path resolution working correctly. Not yet linked to the Xcode app
target — that's the human-in-the-loop step in Task 0.4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User added all 6 packages via Xcode (File → Add Package Dependencies →
Add Local). Resulting pbxproj has the canonical objectVersion 77 form:
XCLocalSwiftPackageReference entries with relativePath = ../Packages/<Name>
(SRCROOT-relative since the project lives at JellyTV/JellyTV.xcodeproj),
XCSwiftPackageProductDependency entries, and Frameworks build phase
references.

Verified by adding temporary import statements for JellyfinAPI,
Persistence, Settings, DesignSystem to JellyTVApp.swift — full xcodebuild
succeeded — then reverted. Real wiring lands in Phase 1.5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Raw Security framework (SecItemAdd/Update/CopyMatching/Delete) with
kSecAttrAccessibleAfterFirstUnlock (correct for tvOS — no lock screen).
~120 LOC, zero external dependencies. Replaces the KeychainAccess SPM
dep softplan.md originally proposed (last released March 2021,
maintenance concern).

CredentialsStore exposes serverURL/accessToken/deviceId as throwing
funcs (since deviceId lazily generates a UUID on first access and the
persist step can throw). Service name is injectable so tests use a
unique UUID-based service per test for isolation.

Tests use Swift Testing (@test). Round-trip set/get/delete, overwrite,
idempotent delete, missing-key nil, deviceId persistence, clear-wipes-all.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DTOs match the verified OpenAPI 10.11.8 spec — camelCase Swift properties
with explicit CodingKeys mapping to PascalCase wire keys (locked
decision per critic C10). UserDto includes hasPassword and
primaryImageTag from the start to avoid Phase 2 schema churn (critic
M4). QuickConnectResult.dateAdded is Date? with iso8601 decoding (M3).
AuthenticationRequest uses "Pw" not "Password" — protected by an
explicit test that asserts the encoded JSON.

JellyfinError has dedicated cases for unauthenticated, quickConnectDisabled,
and quickConnectExpired so the actor + SignInModel can do per-callsite
remapping (critics B1, C4).

JellyfinClientAPI is a Sendable protocol the actor will conform to,
enabling SignInModel to be tested with a mock (critic C6). Phase 1.3
implements the actor.

Tests use Swift Testing (@test) with golden JSON literals — protects
against silent wire-format drift.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Actor conforming to JellyfinClientAPI. Owns URLSession(.ephemeral)
(critic C11), serverURL/accessToken state, immutable deviceId/client
metadata. Per-request URLRequest with the comma-separated MediaBrowser
auth header (URL-percent-encoded values, critic C5), Token field omitted
when nil. Generic send<T> maps 401 → .unauthenticated; per-callsite
remapping in quickConnectInitiate (.unauthenticated → .quickConnectDisabled,
critic C4) and quickConnectStatus (http 404 → .quickConnectExpired,
critic B1).

Tests use StubURLProtocol to intercept every URLRequest. Headline
assertions: exact auth header format with and without token, percent
encoding of special chars, "Pw" field in AuthenticateByName body,
quickConnectInitiate 401 → quickConnectDisabled, quickConnectStatus 404
→ quickConnectExpired, currentUser 401 → unauthenticated (proves
per-callsite remap doesn't leak), bare-bool decode for /QuickConnect/Enabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full state machine for the connect → choose mode → Quick Connect or
password → signed in flow. SignInModel is @observable @mainactor and
takes any JellyfinClientAPI + CredentialsStore so tests can use a mock.

Per critic findings:
- B2: normalizeServerURL prepends http:// if no scheme, requires non-nil
  host. Bare "192.168.1.50:8096" works.
- B1: pollQuickConnectLoop catches quickConnectExpired (404 → expired
  secret), transitions to failed(.quickConnectExpired). No infinite spin.
- M5: persist credentials FIRST in finishSignIn, then transition to
  signedIn. On persist failure, transition to failed(.persistFailed).
- M6: try await Task.sleep (not try?) so CancellationError propagates;
  caught explicitly in the loop.
- C8 in spirit: network errors → failed(.serverUnreachable), not silent
  retry.

SignInView is a switch over the state with five subviews. Quick Connect
shows the 6-char code at 96pt monospaced. Focus management via
@focusstate. Pixel polish deferred to Phase 5.

Tests use a MockJellyfinClient + per-test unique-service CredentialsStore.
Cover: state initialization, URL normalization (3 cases), connect
success, connect with invalid URL, connect with network error, QC
disabled, QC expired (via direct pollQuickConnectLoop call with 10ms
poll interval), password sign-in success (verifies token + URL persist),
password sign-in wrong credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SessionStore (@observable @mainactor in Settings package) is the
canonical owner of the live JellyfinClient + CredentialsStore. Per
critic C7, it lives at the app root and is shared with SignInModel via
.client + .credentials so the same actor instance and Keychain backing
store are used end-to-end.

restore() distinguishes (per critic C8):
- no credentials → signedOut
- 401 unauthenticated → clear creds, signedOut
- network error → reconnecting (creds preserved)
- 5xx HTTP → reconnecting (creds preserved)
- success → signedIn(user)

This means a momentary network blip on launch never signs the user out.
Reconnecting state offers retry + sign-out buttons.

JellyTVApp instantiates SessionStore via a @State closure-initializer
factory that builds a fresh JellyfinClient with the persistent
deviceId. RootView switches over phase, and the SignInFlowView wrapper
bridges SignInModel.state == .signedIn into sessionStore.didSignIn(user:)
via .onChange.

Tests (8 new, on top of the 11 from Phase 1.4 = 19 total in Settings):
no creds → signedOut, valid creds → signedIn (verifies client
configured), 401 → signedOut + creds cleared, network error → reconnecting
+ creds NOT cleared, 503 → reconnecting + creds NOT cleared,
didSignIn → signedIn, signOut → signedOut + creds cleared + logout called.
The "creds NOT cleared" assertion is the protection against critic C8 —
this is the regression test for #1657 persistent login behavior.

ContentView.swift deleted; JellyTVApp now hosts RootView directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds docs/manual-smoke.md covering the Phase 1 acceptance criteria a
human needs to run on a real Jellyfin server: connect with/without URL
scheme, Quick Connect happy path, Quick Connect expiry, Quick Connect
disabled (per-callsite 401 remap regression test for critic C4),
password sign-in success/failure, session persistence (#1657 regression
test), and network-blip-on-launch behavior (critic C8 regression test).

Phase 0/1 build verification: all 48 unit tests pass (8 Persistence,
21 JellyfinAPI, 19 Settings) and xcodebuild build is clean against
Apple TV 4K (3rd generation) tvOS 26.2 simulator with zero warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validator caught dead code in JellyfinClient.sendIgnoringResponse: the
body was discarded with `_` on the URLSession.data tuple, then the error
path tried to decode ProblemDetails from a fabricated empty Data() via
a meaningless `as? HTTPURLResponse == nil ? Data() : Data()` ternary
that always returned empty.

Capture the body and decode from it like send<T> does. Only affected
logout() in practice (the sole sendIgnoringResponse callsite) and only
on non-2xx, non-401 responses, but the code was clearly wrong.

Tests + xcodebuild still pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add BaseItemDto (canonical Jellyfin item type) with essential fields
- Add UserItemDataDto (playback position, played status, etc.)
- Add userViews(), resumeItems(), nextUp(), latestItems() endpoints to JellyfinClientAPI + JellyfinClient
- Add currentServerURL() to protocol for HomeModel to get server URL
- Update MockJellyfinClient with new stub fields

Implements softplan §6 Phase 2 endpoints.
- Add Nuke + NukeUI dependencies for image caching
- Add JellyfinImage helper for constructing image URLs from item metadata
- Add PosterCard component (focusable button with poster image)
- Add Shelf component (horizontal scrollable row with title + cards)
- Add HeroSection component (full-width backdrop with gradient overlay + play button)

All components follow softplan §4 UI patterns: .borderless button style,
.focusSection(), .scrollClipDisabled(), containerRelativeFrame.
- Add HomeModel with state machine (loading/loaded/failed)
- Uses async let for parallel fetching of libraries, resume, nextUp
- Fetches latest per library sequentially (needs parentId)
- Add HomeView with ScrollView + LazyVStack + hero + shelves
- Uses .scrollClipDisabled(), .scrollTargetBehavior(.viewAligned)
- Add HomeModelTests covering success, network error, no server URL, unauthorized

Wires into RootView.SignedInRootView replacing the Phase 1 placeholder.
- Replace SignedInRootView placeholder with HomeView(model:)
- Add Library import to RootView
- Update macOS platform versions to v15 for all packages (Nuke requirement)
- Add JellyfinAPI dependency to Settings test target for MockJellyfinClient access

These changes complete Phase 2 integration.
- HomeModel: drop type=="Folder"||"CollectionFolder" filter; /UserViews
  already returns the library list. Latest-per-library now populates.
- HomeModel: parallelize latestItems with withThrowingTaskGroup.
- HomeView: add HomeContent.isEmpty + empty-state view with Reload, so a
  user with no resume/nextup/latest content sees a focusable fallback
  instead of a blank screen.
- DesignSystem: rewrite PosterCard for tvOS — drop bad
  containerRelativeFrame/opacity, switch to .buttonStyle(.card) (gated
  on os(tvOS)) for proper focus/parallax.
- DesignSystem: add FocusedHomeItemKey + .focusedHomeItem(...) plumbing
  through Shelf/PosterCard so the hero backdrop can crossfade to the
  currently-focused item.
- Tests: add HomeModelScrollRegressionTests guarding against duplicate
  ids and large libraries (Swiftfin#1906 class).
Pivots JellyTV from a generic Jellyfin home/library client to a
Live-TV-first tvOS app. The user's server has only Live TV (HDHomeRun
emulator + XMLTV with real EPG).

JellyfinAPI
- New DTOs: LiveTvChannel, LiveTvProgram, plus query-result wrappers.
  Per-endpoint narrow DTOs (not extending BaseItemDto).
- New methods on JellyfinClientAPI + JellyfinClient:
    liveTvChannels()
    liveTvPrograms(channelIds:minStartDate:maxStartDate:)
  /LiveTv/Programs uses repeated channelIds query items, ISO8601 date
  params, enableTotalRecordCount=false, limit=2000.
- Tests: 7 LiveTV decoding tests (incl. .NET 7-digit fractional second
  StartDate verification — confirms .iso8601 strategy already handles
  this format) + 8 URLProtocol stub tests for the new client methods.
- Dedicated LiveTvStubURLProtocol class to avoid the shared static
  handler racing across parallel suites.

LiveTV (new package)
- GuideModel: @mainactor @observable, .loading | .loaded | .failed,
  fetches channels then programs in single window call. Drops past
  programs and orphan channelIds. Widens minStartDate by 4h to catch
  in-progress programs at the window edge. Injectable now() for tests.
- GuideContent: value type with helper accessors.
- GuideLayout: pixelsPerMinute=8, channel column 240pt, row 100pt.
- GuideView: vertical ScrollView -> HStack of (channel column outside
  horizontal scroll, sticky horizontally; horizontal ScrollView with
  TimeHeader + LazyHStack rows). NOW line as a TimelineView overlay
  sibling to the grid so timeline ticks don't rebuild program cells
  (focus survives ticks).
- ProgramCell + ChannelRow + TimeHeader + ChannelLabel.
- 7 GuideModel tests covering load success, empty channels, network
  error, unauthorized, not configured, past programs dropped, orphan
  channels ignored.

App target
- RootView signed-in path now uses GuideView(model: GuideModel(...)).
- import Library + unused import DesignSystem removed.
- LiveTV package added to JellyTV target.

Existing fakes (HomeModelTests, HomeModelScrollRegressionTests,
Settings/MockJellyfinClient) updated with stub implementations of the
new protocol methods. All 76 existing tests still pass.

Plan / critic / validation went through /motif:dev. Two critics flagged:
- Date decoder rewrite is unnecessary churn (.iso8601 handles 7-frac
  seconds) — dropped.
- maxEndDate vs maxStartDate naming — fixed.
- onScrollGeometryChange sticky overlay risky on tvOS — replaced with
  nested-scroll layout.
- LazyVStack + eager HStack would materialize ~8000 buttons —
  switched per-row HStack to LazyHStack.
- TimelineView wrapping the whole grid would drop focus every minute
  — restructured to wrap NOW line only.
Wires the EPG guide to actually play channels via AVPlayer. ChannelLabel
becomes the focusable Button (per UX choice — only channels are
selectable, not programs); ProgramCell loses its Button wrapper and is
now a non-focusable visual rectangle with a RoundedRectangle background.
fullScreenCover (tvOS) / sheet (macOS) presents LiveTVPlayerView when a
channel is selected.

JellyfinAPI
- New types: LiveStreamPlayback (URL + liveStreamId), MediaSourceInfo +
  LiveStreamResponse (decodes both singular MediaSource and plural
  MediaSources shapes), LiveStreamOpenRequest (kept but unused),
  PlaybackInfoBody, DeviceProfileBody + DirectPlay/Transcoding nested
  body types.
- New JellyfinClientAPI method liveTvOpenStream(channelId:) implemented
  by JellyfinClient using POST /Items/{itemId}/PlaybackInfo with
  autoOpenLiveStream=true (the canonical Swiftfin/web-client path —
  /LiveStreams/Open returned 500 from this server).
- Lazy userId caching on the actor (cleared on setAccessToken). Required
  by PlaybackInfo's userId query param.
- Safe playback URL construction in makePlaybackURL: prefers the
  server-supplied transcodingUrl resolved via URL(string:relativeTo:)
  (never appendingPathComponent — that percent-encodes ?), falls back
  to a URLComponents-built /Videos/{id}/stream.{container} URL with
  api_key baked in. For live streams, rewrites /videos/{id}/stream →
  /videos/{id}/master.m3u8 (HLS) since AVPlayer can't consume Jellyfin's
  progressive-download endpoint for live media. Also strips empty
  leading query items that Jellyfin sometimes emits (?&...).
- 7 new URLProtocol stub tests covering path, body shape, transcoding
  URL rewrite + empty-param cleanup, direct-stream fallback, plural
  MediaSources fallback, missing token, 401.

LiveTV package
- New LiveTVPlayerView wrapping AVPlayerViewController via
  UIViewControllerRepresentable with strict #if os(tvOS) guards on
  AVKit/UIKit imports + the representable conformance. Coordinator is
  @mainactor; cleanup happens in dismantleUIViewController. KVO on
  AVPlayerItem.status + observer on AVPlayerItemFailedToPlayToEndTime
  surface playback errors via os.Logger.
- macOS fallback shows a "tvOS only" stub so the package compiles for
  both platforms.
- ChannelPlayerPresentation ViewModifier picks fullScreenCover on tvOS,
  sheet on macOS (fullScreenCover is unavailable on macOS).
- ProgramCell now non-focusable; added RoundedRectangle background to
  preserve visual cell boundary that .buttonStyle(.card) used to give.
- ChannelLabel wraps content in a Button with .buttonStyle(.card) on
  tvOS / .plain on macOS, calls onSelect(channel) closure.
- ChannelRow lost its onSelectProgram parameter; GuideView programArea
  call site updated.
- GuideView gets @State var selectedChannel and passes the selection
  through ChannelLabel's onSelect.
- GuideModel.openStream(channelId:) — public delegating method so the
  player can resolve a stream URL without GuideView needing access to
  the underlying client (token + actor stay encapsulated).

Logging
- New JellytvLog enum exposing per-category os.Logger instances under
  subsystem tv.jelly.JellyTV (api / livetv / player / session).
- JellyfinClient.send now logs every request/response, decode failures
  with a body snippet, HTTP errors with the ProblemDetails title/detail,
  401 mapping, and network errors with URLError code.
- liveTvOpenStream logs channel id, resolved MediaSource fields, and
  the final playback URL.
- GuideModel.load logs begin / loaded / each error path.
- LiveTVPlayerView logs stream-open lifecycle and AVPlayerItem status
  changes (incl. .failed with the underlying AVError).
- JellyfinAPI Package.swift now declares .macOS(.v15) — was tvOS-only,
  which broke os.Logger availability when SPM built the package for
  swift test on the Mac.

Info.plist
- Added NSAllowsArbitraryLoadsForMedia. URLSession honors
  NSAllowsLocalNetworking but AVFoundation does not — without this
  exception AVPlayer would fail with the misleading
  -11850 "Operation Stopped" / -12939 chain on cleartext LAN streams.

Existing fakes (HomeModelTests, HomeModelScrollRegressionTests,
Settings/MockJellyfinClient, LiveTV/FakeJellyfinClient) updated with
stub liveTvOpenStream implementations. FakeJellyfinClient additionally
exposes a Result<LiveStreamPlayback, Error> stub property and captures
lastOpenStreamChannelId for assertions.

All 83 tests across all packages pass (43 JellyfinAPI, 6 Library, 19
Settings, 7 LiveTV, 8 Persistence).

Plan / 2 critics / validation went through /motif:dev. Critics caught:
- LiveStreamResponse needs to handle both singular MediaSource and
  plural MediaSources shapes (different Jellyfin versions)
- transcodingUrl resolution must use URL(string:relativeTo:), never
  appendingPathComponent (would percent-encode the ?)
- Don't double-inject api_key when Jellyfin already baked it into
  transcodingUrl
- accessToken nil must throw .unauthenticated, not silently produce
  a URL with empty api_key=
- GuideModel.client should stay private; expose openStream delegating
  method instead of leaking the client to the view layer
- import UIKit/AVKit must be inside #if os(tvOS), not just usage —
  UIKit doesn't exist on macOS
- Tests must explicitly assert no double api_key and no percent-encoded ?

Known v1 limitations (deferred):
- /LiveStreams/Close not wired on dismiss; relies on Jellyfin's idle
  timeout for cleanup. Acceptable since the HDHomeRun emulator allows
  multiple concurrent streams.
- DeviceProfile is minimum-viable; if a channel won't play the player
  view shows a Retry/Dismiss state and we'd iterate the profile.
- ProgramCell is now visually-only — the user explicitly chose
  channel-only selection over program selection.
Native macOS live wallpaper app built for macOS 26 (Tahoe) with Liquid Glass.

Features:
- Multi-display video wallpaper playback via AVPlayerLayer-backed NSWindow
- Curated catalog + local imports (MP4/MOV)
- Liquid Glass chrome (buttons, tag chips, badges, row backgrounds)
- Video preview on card hover (cached files only, pooled for perf)
- Live video playback in detail view
- First-launch welcome sheet
- Now Playing section with per-display stop controls
- Drag-and-drop import
- Battery-aware auto-pause (via SettingsManager)
- Menu bar extra for quick access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Features:
- Unapply wallpapers per-display (engine.stop(forDisplay:)) with UI in
  Now Playing cards and detail-view Remove button
- Card grid spacing bumped 16→22 and hover shadow/scale softened to
  prevent visual overlap under hover
- Opaque tag-filter strip background to prevent scroll bleed-through

Error handling & robustness:
- New AppLogger (os.Logger namespace per subsystem) replacing every
  print() in Services with filterable, privacy-aware logging
- New AppErrorPresenter: shared @mainactor presenter with title-dedup,
  single .appErrorAlert() modifier attached once per WindowGroup root,
  nonisolated static report() for service call sites
- AVPlayerItem.status KVO in WallpaperPlayerView with stored observation
  token and cleanup in stop() — surfaces playback failures
- Stale-file detection in WallpaperCatalog (staleLocalWallpaperIDs);
  WallpaperCardView shows subtle grayscale + "Missing" badge;
  WallpaperDetailView offers Remove Missing Entry
- WallpaperEngine.ensureAvailable() stale precheck runs once in apply()
  and once in setWallpaper() so Apply-to-All surfaces a single error
- WallpaperCatalog.removeLocalWallpaper() prunes array + deletes file
- loadSeedCatalog split into explicit missing-file / read / decode steps
- SettingsManager reverts launchAtLogin toggle on SMAppService failure
- DownloadManager @mainactor (fixes background @published warning)
- Import failures route through the global presenter; local
  importError state removed from ImportView
- Drop-to-import error paths fully wired in GalleryView.handleDrop

Docs:
- README.md with features, requirements, architecture, log filters
- CLAUDE.md with project conventions and gotchas (Liquid Glass rules,
  @ObservedObject vs @StateObject, os import requirement,
  NSResponder.presentError name clash, stale-file design, non-goals)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Six independent improvements landed in one pass:

- DownloadManager: rewritten to use URLSessionDownloadDelegate so the
  download progress fraction is real, not indeterminate. WallpaperDetailView
  now binds ProgressView(value:) to the live fraction.
- WallpaperPlayerView: KVO on AVPlayer.timeControlStatus surfaces silent
  stalls via AppErrorPresenter, gated on hasPlayedAtLeastOnce to suppress
  initial-buffer false positives, with a 60s rate-limit between reports.
- AppErrorPresenter: NSAlert fallback when no titled host window is
  available, so errors fired while only the menu bar is visible aren't
  silently dropped (SwiftUI .alert can't propagate through MenuBarExtra).
- AppDelegate: refreshStaleStatus now runs at launch and on every
  didBecomeActive notification, so returning to the app re-checks
  imported files.
- Tools/seed-catalog.swift: standalone Swift script that hits the Pexels
  Videos API and writes catalog.generated.json (does not clobber the
  curated catalog.json). PEXELS_API_KEY via env var.
- livewallTests target: first test target in the repo, using Swift Testing
  and PBXFileSystemSynchronizedRootGroup. 8 test cases covering
  AppErrorPresenter dedup, WallpaperCatalog stale detection, and
  DownloadManager progress publishing. ENABLE_TESTABILITY also added to
  the app's Release config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…technique

Based on InstantSpaceSwitcher by jurplel (MIT license).
This fixes desktop switching on macOS 26 Tahoe where CGS APIs are broken
and CGEvent modifier flags are stripped by the WindowServer.

The technique synthesizes a trackpad swipe gesture with high velocity (400.0)
which triggers instant space switching bypassing broken APIs.

- Added ISS.c/ISS.h with C implementation of gesture synthesis
- Updated DesktopSwitcher.swift to use ISS functions
- Added bridging header configuration in project.pbxproj
- Updated README.md with the fix and credits
Introduce DesignSystem.swift (Metrics, Palette, StatusPill, ActiveBadge)
so spacing, radii, the "active" green, and chrome pills are one source of
truth. Share DisplayManager via .shared so Settings, Detail, Gallery, and
the menu bar all stay reactive to the same instance. Fix per-view
discrepancies: card preview letterbox, card metadata pill vs. glass rule,
detail glass-on-content, unlabeled download progress, non-reactive settings
displays, orphaned import tip, unstable menu bar ordering, welcome CTA
floating outside its card, hero CTA duplication, pinned tag-strip backdrop,
and the drop overlay's invisible accent tint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebuilds the Live TV experience around a TabView shell (On Now / Guide /
Recordings) backed by the full Jellyfin Live TV API. Existing GuideModel
and program/channel DTOs stay backward-compatible; everything new is
purely additive.

JellyfinAPI
- Extend `LiveTvChannel` with `userData` (favorites) and inline
  `currentProgram` (`addCurrentProgram=true` shape).
- Extend `LiveTvProgram` with channel context, genres, ratings, image tags,
  runtime — fields the UI needs for hero/detail/guide rendering.
- New DTOs: `TimerInfoDto`, `SeriesTimerInfoDto`, `LiveTvChannelFilters`,
  `LiveTvProgramFilters`.
- New `JellyfinClient` endpoints (with `JellyfinClientAPI` defaults so
  existing mocks keep compiling):
  - `liveTvChannels(filters:addCurrentProgram:)` (full filter surface)
  - `liveTvPrograms(channelIds:minStartDate:maxStartDate:filters:)`
  - `liveTvRecommendedPrograms`
  - `liveTvProgram(programId:)`
  - `liveTvRecordings(isInProgress:seriesTimerId:limit:)`
  - `deleteLiveTvRecording`
  - `liveTvTimers`, `liveTvSeriesTimers`, `liveTvTimerDefaults`
  - `createLiveTvTimer`, `createLiveTvSeriesTimer`
  - `cancelLiveTvTimer`, `cancelLiveTvSeriesTimer`
  - `setFavorite(itemId:isFavorite:)`

DesignSystem
- New `ChannelLogoView` with letter-bug fallback + `LiveBadge` pulser.
- `LiveTvFormat` (time-range / progress) for shared EPG formatting.
- Image URL helpers on `LiveTvChannel` (logo) and `LiveTvProgram`
  (tile, backdrop) with sensible tag fallbacks.

LiveTV
- `LiveTVRootView`: TabView shell holding On Now / Guide / Recordings,
  with centralized full-screen player + program-detail presentation.
- `OnNowView`: Plex-style hero (focused channel/program drives backdrop)
  plus curated shelves — Favorites, On Now, Sports, Movies, News, Kids,
  Up Next, Recordings.
- `GuideView` rebuilt: filter pill bar (All / Favorites / Movies / Sports
  / News / Kids), channel logos in the row header, focusable program
  cells with live-progress bar, and a sticky focused-program detail strip.
- `ProgramDetailView`: backdrop, badges, channel chip, time/year/rating,
  overview, Watch / Record (toggle) / Close action row.
- `RecordingsView`: Recording Now / Scheduled / Series / Recorded
  sections with timer cancel + recording delete.
- `LiveTVPlayerView`: enhanced loading state (channel logo + name) and
  AVPlayerItem.externalMetadata injection (title, channel chip, overview,
  genre, year) so the tvOS press-up info panel reads beautifully.

App
- `RootView` now lands on a TabView with Home (Library) + Live TV.
- Per-tab models held in `@State` so loaded content survives re-renders.

Tests
- New: `LiveTvTimerDecodingTests`, `JellyfinClientLiveTvExtendedTests`
  (channels-with-filters / recommended / recordings / timers / favorites
  paths and methods), `OnNowModelTests`, `RecordingsModelTests`.
- Extended: `FakeJellyfinClient` (filtered channel + recommended +
  recordings + timer stubs), `GuideModelTests` (filter routing).

https://claude.ai/code/session_01QyJHH7BXTF4WHagXNihV2k
…rewrite)

The Phase-C playback path silently rewrote progressive `/videos/{id}/stream`
URLs to `/videos/{id}/master.m3u8`, but the query params Jellyfin attached
were progressive-style (no SegmentContainer/MinSegments/BreakOnNonKeyFrames)
so the master.m3u8 endpoint couldn't serve a valid manifest — AVPlayer
errored with CoreMediaErrorDomain -16847 / NSURLErrorResourceUnavailable
-1008 every time.

Fix the upstream cause: send a `container=ts`, `protocol=hls`,
`BreakOnNonKeyFrames=true` transcoding profile (matching the Jellyfin web
client's live-TV profile) so the server returns a real HLS URL natively.
Then drop the client-side path rewrite — silent rewrites mask
profile/server mismatches, which is exactly how this bug shipped.

Validated end-to-end against a live Jellyfin server: master playlist,
variant playlist, and segment all return HTTP 200; segment is valid
MPEG-TS (sync byte 0x47); live TV plays on the simulator.

JellyfinAPI
- TranscodingProfileBody gains `breakOnNonKeyFrames: Bool` (PascalCase
  CodingKey, no init default — consistent with the struct's all-required
  style; only caller is `liveTvDefault`).
- `DeviceProfileBody.liveTvDefault` HLS transcode profile changed:
  container `mp4`→`ts`, videoCodec `h264`→`h264,hevc` (server can
  stream-copy hevc instead of re-encoding), audioCodec
  `aac`→`aac,mp3,ac3,eac3`, minSegments `1`→`2`, `breakOnNonKeyFrames=true`.
  DirectPlay profile unchanged.
- `JellyfinClient.makePlaybackURL`: removed `/stream`→`/master.m3u8`
  path rewrite. Kept the empty-name query-item cleanup (Jellyfin's
  TranscodingUrl still sometimes emits `?&...`). Docstring rewritten.

Tests
- Updated primary fixture's TranscodingUrl to the new `/master.m3u8`
  shape Jellyfin now returns.
- New `progressiveTranscodingResponseJSON` fixture for a regression test.
- `liveTvOpenStreamSendsCorrectJSONBody` now asserts the request body's
  TranscodingProfile fields explicitly: Container=ts, MinSegments=2,
  BreakOnNonKeyFrames=true (Bool, not string), AudioCodec, VideoCodec.
- Renamed `…RewritesLiveStreamToHls` → `…UsesServerSuppliedHlsTranscodingUrl`.
- New regression test `liveTvOpenStreamPreservesProgressiveTranscodingUrl`
  pins behavior: if a server (despite our HLS profile) ever returns a
  `/stream` URL, the client must NOT silently rewrite it — fail loudly
  instead of producing a broken HLS URL.

44/44 JellyfinAPI tests pass. Full tvOS build succeeds. Ran the full
/motif:dev workflow: 2 Claude critics in parallel + Codex second-opinion.
Critics caught the original "share one fixture across contradictory
tests" mistake (split into two fixtures), the docstring rot, and that
the kept `if liveStreamId != nil` block needed a comment explaining its
post-rewrite-deletion purpose. Codex additionally caught that hevc
should join the videoCodec list (free win — server stream-copies
instead of transcoding) and that both fixtures need top-level
LiveStreamId so the cleanup gate exercises the same code path as the
rename target.

Known limitation (deferred): the DirectPlay-for-live path still routes
to `/Videos/{id}/stream.{container}` (progressive). The user's failing
channel was rejecting DirectPlay (TranscodeReasons=DirectPlayError) so
this fix targets the actual bug. Channels that pass DirectPlay haven't
been validated on tvOS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two unrelated regressions from the three-tab redesign (9ded87f):

1. Build error at line 302: `isFocused ? .primary : .primary.opacity(0.9)`
   mixed `HierarchicalShapeStyle` with `some ShapeStyle` and the ternary's
   result couldn't unify. Fixed by collapsing into
   `.primary.opacity(isFocused ? 1.0 : 0.9)` — same visual, one type.

2. Visual overlap: focused channel cells in the left column extended ~24pt
   into the program grid because `.buttonStyle(.card)` scales the focused
   button by ~1.1× and the cell filled the full 240pt column width. Inset
   each cell by 12pt horizontally so the scaled card stays inside the
   column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zackbart and others added 27 commits July 22, 2026 20:03
Serialize synthetic drags and preserve the cursor after queued WindowServer events.
Reconcile foreground, Space, and display changes without selecting stale hosted status windows.
Separate initial SwiftUI and status-item layout and coalesce panel resizing.
Activate parked items directly when possible, track the live hosted proxy, and keep storage geometry stable across foreground and display transitions.
* Fix deterministic menu bar moves

* Run macOS tests on pull requests

* Fix debug test host

* Keep debug module import stable
* Keep Barr control visible after launch

* Prepare Barr 0.0.9
git-subtree-dir: barr
git-subtree-mainline: c885b33
git-subtree-split: 3924124
git-subtree-dir: windo
git-subtree-mainline: e358948
git-subtree-split: 6b64c27
git-subtree-dir: loadout
git-subtree-mainline: 0e037e2
git-subtree-split: 611adbd
git-subtree-dir: livewall
git-subtree-mainline: 752ebfd
git-subtree-split: 4a50a41
git-subtree-dir: mrmouse
git-subtree-mainline: e222b43
git-subtree-split: 7064369
git-subtree-dir: herdr-ios
git-subtree-mainline: d829479
git-subtree-split: 201447c
git-subtree-dir: jellytv
git-subtree-mainline: 1582685
git-subtree-split: ef77fef
@zackbart
zackbart marked this pull request as ready for review August 3, 2026 19:10
@zackbart
zackbart merged commit feea167 into main Aug 3, 2026
1 check passed
@zackbart
zackbart deleted the agent/consolidate-native-apps branch August 3, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants