40 Commits
Author SHA1 Message Date
vgearen 059aef3609 fix(chart): fix top whitespace and KRW number wrap in intraday view
Two bugs in StockChartView triggered by opening a Korean stock's chart:

1. Top whitespace: DropdownView used ZStack to overlay stockListView
   (opacity 0 when chart shown) and StockChartView. ZStack sized to
   max(list, chart) — list at ~720 tall pushed the ~500-tall chart to
   vertical-center, leaving ~200 of blank above the chart. Replace
   ZStack with plain if-let so list and chart are mutually exclusive
   in the layout tree.

2. Stat-row line wrap: fmt used %.3f always, producing 'KRW 284000.000'
   which overflowed the narrow stat columns and wrapped to two lines
   ('284000.0' / '00'). Scale precision by magnitude: ≥1000 → integer,
   ≥10 → 2 decimals, else 3 decimals.
2026-05-14 11:53:11 +08:00
vgearen d8904f4acb fix(ui): match settings height to list mode + CI uses latest Xcode
UI fix (SDK-agnostic):
- SettingsView height 480 → 620 so the settings mode panel size matches
  list mode at maximum. Avoids MenuBarExtra's NSPanel not shrinking when
  switching between list and settings views.
- Drop the .id(showSettings) hack — it forced view rebuild but the panel
  is managed by AppKit and does not respond to SwiftUI view recreation.

CI hardening:
- Use maxim-lobanov/setup-xcode@v1 with latest-stable so the runner Xcode
  is as close to local (Xcode 26.5) as available, minimizing SwiftUI SDK
  behavior drift.
- Run xcodebuild test before building the DMG; release will fail-fast on
  test regressions instead of shipping broken binaries.

Root cause analysis of v1.3.4 regression:
Local Xcode 26.5 produced a 5.2MB binary linking to macOS 26 SwiftUI
symbols; GH macos-latest with default Xcode 16 produced a 5.0MB binary
linking to macOS 15 SwiftUI. The .id() hack happened to look correct in
the SDK 26 build but not in SDK 15 — same source, different runtime
behavior. The height:620 approach works in both.
2026-05-14 11:30:52 +08:00
vgearen bb00d4379f fix(ui): force panel relayout when switching between list/settings
MenuBarExtra(.window) caches the NSPanel size from list mode (~720 with
many stocks) and does not auto-shrink when switching to the 480-tall
settings view, leaving a large blank gap. Adding .id(showSettings) tells
SwiftUI to rebuild the view subtree on mode change, prompting the panel
to recompute its size from the new content.

release: 1.3.3 → 1.3.4
2026-05-14 11:12:45 +08:00
vgearen 8c424dbe59 fix(ui): use fixedSize so dropdown panel shrinks to content
The opaque background added in v1.3.2 exposed extra empty space below
the toolbar — the panel was sized larger than the VStack content.
Adding .fixedSize(horizontal: false, vertical: true) before the
background makes the panel size to its actual content.

release: 1.3.2 → 1.3.3
2026-05-14 11:00:24 +08:00
vgearen c9b6bf467c fix(ui): give dropdown an opaque background
MenuBarExtra(.window) on the new SDK defaults to a vibrant translucent
material, letting whatever is behind bleed through. Apply
Color(.windowBackgroundColor) to the root VStack so the panel reads as
a normal opaque window in both light and dark mode.

release: 1.3.1 → 1.3.2
2026-05-14 10:52:55 +08:00
vgearen 2a13e1976f feat(settings): sort '我的股票' with held stocks first
Stocks with both costPrice and holdingShares set (effective values from
real portfolio or active watchlist) appear at the top; the rest follow in
original order. Makes managing larger watchlists easier.
2026-05-14 10:46:59 +08:00
vgearen f1b6b72d8d fix(critical): isolate AppStateTests from user's real data directory
PRIOR BEHAVIOR (data-loss bug):
AppStateTests.setUp() constructed AppState() directly, which used the real
~/Library/Application Support/Stockbar/ for stocks.json. Each test method
that did 'sut.stocks = [s1, s2]' triggered didSet → saveStocks → silently
overwrote the user's actual portfolio file with 2 test entries. backupIfNeeded
then rolled all 10 backup snapshots forward, eventually erasing every
historical copy of real data.

This destroyed a user's 29-stock portfolio when xcodebuild test was run
during Korean stock feature development (2026-05-14 10:08).

FIX:
- AppState gains a static appSupportDirOverride: URL? hook
- AppStateTests.setUp() creates a unique tmpDir, sets the override, tearDown
  resets and removes the directory
- Verified: after running all 7 AppStateTests, the real stocks.json is
  byte-identical to before the run.

This must land before any further test run touches AppState.
2026-05-14 10:35:01 +08:00
vgearen 4e1afefcb8 fix(korea): parallel Korean search alongside Tencent so 6-digit codes find both
When user types a 6-digit code like '000660', Tencent returns A股 sz000660
(*ST 南华) while the same code is also a valid Korean ticker (000660.KS SK
hynix). Previous logic only fell back to Yahoo when Tencent returned empty,
hiding the Korean match.

Now: fetchSuggestions runs tencentSearch and yahooKoreanSearch concurrently
and merges results with dedup. Yahoo is queried whenever the input is
plausibly Korean: 6-digit number, ≥3 letters, Hangul, .ks/.kq suffix, kr_
prefix. Short numeric inputs skip Yahoo to save a request.
2026-05-14 10:27:46 +08:00
vgearen 87a67f9ba9 feat(korea): show KR group in dropdown + Yahoo search for Korean stocks
DropdownView: extend market iteration arrays to include .krStock so Korean
stocks appear as a 4th group after A股/港股/美股.

SettingsView search overhaul:
- looksKorean(): heuristic to route KR-like keywords (.ks/.kq suffix, kr_
  prefix, Hangul characters) directly to Yahoo
- yahooKoreanSearch(): /v1/finance/search filtered to symbols ending in
  .KS or .KQ, returns SearchResult with market=.krStock
- directResult(): explicit kr_<code>.ks and bare 005930.ks/kq forms supported
- Placeholder text updated to suggest Korean inputs

Tencent/Sina searches unchanged for A股/港股/美股.
2026-05-14 10:08:24 +08:00
vgearen 8f568195d1 feat(korea): Korean intraday chart via Yahoo v8 chart
ChartService.fetchIntraday dispatches .krStock to new fetchKoreanIntraday,
which reuses the Yahoo v8 chart endpoint. parseKoreanChartPoints (exposed
static for tests) maps KST timestamps to indices: 09:00 → 0, 15:29 → 389
(KR session is 6.5h = 390 minutes, no pre/post market).

RefreshScheduler comment updated to note KR trading window (KST 09:00-15:30
= BJ 08:00-14:30) is already covered by the always-true weekday branch.
2026-05-14 10:07:10 +08:00
vgearen 6e63064207 feat(korea): add koreanMarketSession + wire KR into AppState.refresh
AppState.koreanMarketSession (nonisolated) returns '盘中' during KST 09:00-15:30
on weekdays, nil otherwise. Marked nonisolated so it can be called from sync
non-MainActor contexts (notably tests).

AppState.refresh now has a 5th parallel branch fetching Korean quotes via
DataService.fetchKoreanQuotes. KR is non-throwing (returns [:] on failure) so
it's awaited outside the try block — any KR network failure does not abort
A股/港股fetch results.
2026-05-14 10:05:47 +08:00
vgearen bddb608b76 feat(korea): fetch Korean quotes via Yahoo v8 chart endpoint
DataService.parseKoreanChartMeta parses Yahoo v8 chart 'meta' segment into
Quote (price/change/percent/updateTime). Yahoo v7 batch quote endpoint
returns 401 since 2024, so we fall back to v8 chart per-symbol.

DataService.fetchKoreanQuotes uses withTaskGroup to issue concurrent
requests for the watchlist; single-symbol failure does not affect others.

Fixtures captured from live API for KOSPI (005930.KS Samsung) and KOSDAQ
(293490.KQ Pearl Abyss); synthetic quote_empty.json covers null price.
2026-05-14 10:03:56 +08:00
vgearen 28eb6bd98c feat(korea): add Market.krStock + KRW currency + chart x-axis
Adding Market.krStock cascades into several exhaustive switches that must all
update together: ExchangeRates.convert, DisplayCurrency, StockChartView ranges
and labels. Bundles tasks 2/3/4/5/14 into one build-passing landing.

- Market enum: add krStock case + kr_ prefix routing in Market.from
- ExchangeRates: add usdToKrw (fallback 1380), krwToCny derived, KRW path in convert
- DisplayCurrency: add .krw (₩), display name 韩元
- CurrencyService.fetchRates: include fx_susdkrw in Sina FX batch URL
- StockChartView: KR fullDayRange 0...389 (KST 09:00-15:30, 390 min); KR axis labels
  09:00/10:30/12:00/13:30/15:30

Tests: +5 Stock cases for KR, +8 CurrencyKRWTests covering all KR↔CNY/USD/HKD/KRW
conversion paths and fallback constant.
2026-05-14 10:01:57 +08:00
vgearen 36ec419416 feat(korea): add KoreanStockID helpers for kr_<code>.ks/kq ↔ Yahoo symbol 2026-05-14 09:59:41 +08:00
vgearenandClaude Opus 4.6 dccfd8bfc0 v1.2.7: Fix HK updateTime field index + hide daily PnL on non-trading days
- Fix HK quote updateTime: fields[29] was volume, changed to fields[30]
- Add Quote.isToday: compare updateTime date with today (Beijing timezone)
- effectiveDailyPnl returns nil when quote data is not from today
- Prevents stale daily PnL display on holidays/weekends

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 09:39:53 +08:00
vgearenandClaude Opus 4.6 f8bb287194 v1.2.6: Hide daily PnL on non-trading days (holidays/weekends)
- Add Quote.isToday: compare updateTime date with today in Beijing timezone
- effectiveDailyPnl returns nil when quote data is stale (not from today)
- Prevents showing yesterday's change as today's daily PnL on closed markets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:14:49 +08:00
vgearenandClaude Opus 4.6 592ad90f61 v1.2.5: Add watchlist (观察仓) feature with virtual portfolio support
- New Watchlist model: named virtual portfolios with per-stock cost/shares
- Watchlists persisted to watchlists.json, active ID in settings
- AppState: effective cost/shares/PnL methods that respect active watchlist
- Settings: create/delete/switch watchlists, edit writes to active watchlist
- ProfitSummaryView shows active watchlist name
- UI alignment fixes for toggle and text field padding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:31:59 +08:00
vgearenandClaude Opus 4.6 ac87a4417d v1.2.4: Fix scroll position: preserve on chart return, reset on panel reopen
- Use ZStack overlay for chart instead of if/else branch swap
- ScrollView stays mounted while chart is shown (opacity toggle)
- Scroll position preserved when returning from chart view
- Panel reopen naturally starts from top

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:08:50 +08:00
vgearenandClaude Opus 4.6 7e502232d3 v1.2.3: Add holdings group option to pin held stocks at top
- New setting: "持仓股单独分组置顶" toggle
- When enabled, stocks with holdings are grouped into a "持仓" section at top
- Remaining non-held stocks still grouped by A/HK/US market as usual
- StockGroupView supports custom title for holdings group

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:58:30 +08:00
vgearenandClaude Opus 4.6 d898185745 v1.2.2: Fix overnight session weekday logic (Sunday evening = Monday overnight)
- ET 20:00-24:00: allow Sunday (wd=1) as Monday overnight start
- ET 00:00-04:00: allow Saturday (wd=7) as Friday overnight continuation
- Fixes no data shown on Sunday evening when overnight API has live data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:51:15 +08:00
vgearenandClaude Opus 4.6 80c12aba58 v1.2.2: Preserve scroll position when returning from chart view
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:17:35 +08:00
vgearenandClaude Opus 4.6 c7b19e4f49 v1.2.1: Add sort settings (change% asc/desc, US session/regular price mode)
- Add SortRule enum (涨跌幅升序/降序) and USPriceMode enum (时段价格/盘中价)
- Settings page: sort rule segmented picker + US price mode picker
- Toolbar sort button applies configured rule; off = default order
- US stocks: session price mode uses extendedPrice for change% calculation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:30:41 +08:00
vgearenandClaude Opus 4.6 94b14693d5 v1.2.0: Add US overnight session real-time prices via Pyth/TradingView API
- Add PythService.swift: fetch overnight prices from us-overnight-stock-api.vercel.app
- Restore "夜盘" session in usMarketSession() (ET 20:00-04:00)
- Parallel fetch Sina + Pyth during overnight, merge once to avoid UI flicker
- Clear stale after-hours extendedPrice before applying overnight data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:52:39 +08:00
vgearenandClaude Opus 4.6 72103bd69f v1.1.9: Remove US night session display (no free data source available)
- Remove "夜盘" from usMarketSession(), only keep 盘前/盘中/盘后
- Night session (ET 20:00-04:00) has no free real-time data API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:31:04 +08:00
vgearenandClaude Opus 4.6 86201cfb69 v1.1.8: Fix US night session data, anti-aliased icon, full-day refresh
- Fallback to extended price when main price is 0 during night session
- Extend US stock refresh to cover all sessions (weekday all-day)
- Regenerate app icons with 4x supersampling for smooth rounded corners

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 21:44:18 +08:00
vgearenandClaude Opus 4.6 af1a98efc6 v1.1.7: Extend US stock refresh to cover all sessions (pre/regular/after/night)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:56:27 +08:00
vgearenandClaude Opus 4.6 2683521067 v1.1.6: Add high/low prices to chart stats row
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:07:27 +08:00
vgearenandClaude Opus 4.6 729b03eb6e v1.1.5: Add confirmation dialog before auto-update
- Show NSAlert with version info when new version found
- User must confirm before download begins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:56:03 +08:00
vgearenandClaude Opus 4.6 66b77aee04 v1.1.4: Security hardening — SHA-256 verify, fix injection, log rotation, error logging
- Add SHA-256 hash verification for downloaded DMG in auto-update
- Fix shell command injection: use positional args instead of path interpolation
- Add log rotation (truncate to 500KB when exceeding 1MB)
- Replace critical try? with do/catch + logToFile for data persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:51:23 +08:00
vgearenandClaude Opus 4.6 00f142f091 v1.1.3: Fix status bar PnL display, rounded app icon, LaunchAgent fallback
- Fix "daily + total" PnL mode only showing daily: use single Text concatenation
- Move PnL options into status bar picker (daily / total / both), hide icon in PnL mode
- Add rounded corners to app icon for macOS consistency
- Add LaunchAgent plist fallback for launch-at-login on Intel Macs
- Remove StatusBarPnL enum, use statusBarStockId special values instead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:56:19 +08:00
vgearenandClaude Opus 4.6 af7a45e027 v1.1.2: Status bar PnL display options and login item fix
- Add setting to choose status bar PnL: none / daily / total / both
- Fix launch-at-login: remove unconditional register() on every launch
- Add error logging for SMAppService register/unregister

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:27:08 +08:00
vgearenandClaude Opus 4.6 88e705fece v1.1.1: Add PnL percentage display and in-app auto-update
- Show floating PnL percentage for each stock and portfolio summary
- Show daily/total PnL percentage in profit summary view
- Replace manual GitHub download with in-app auto-update (download DMG, mount, replace, relaunch)
- Disable app sandbox for Process support (non-App Store distribution)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:16:35 +08:00
vgearenandClaude Sonnet 4.6 51b3ee7bca Update screenshot and unify stock row background
- Replace README screenshot with latest UI
- StockRowView: use Color.primary.opacity(0.04) to match ProfitSummaryView style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 11:47:16 +08:00
vgearenandClaude Sonnet 4.6 41665a6308 Add US session zones, check-for-updates, UI polish
- StockChartView: mark 盘前/盘中/盘后 with colored background zones and text labels for US stocks
- UpdateChecker: check latest release via GitHub API, prompt user to download if newer version found; semantic version comparison to avoid false positives
- SettingsView: add 关于 section with version and 检查更新 button; hide scrollbar (.never)
- DropdownView: hide scrollbar (.never)
- ProfitSummaryView: replace opaque white background with subtle transparent tint
- project.pbxproj: normalize MARKETING_VERSION to 1.0.0 (three-part semver)
- README/README_CN: add main screenshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 11:43:27 +08:00
vgearenandClaude Sonnet 4.6 8c60d39024 Migrate all settings from UserDefaults to settings.json
- AppSettings: fully Codable with statusBarStockId, refreshInterval,
  colorScheme, displayCurrency; uses decodeIfPresent for forward compat
- AppState: replace @AppStorage with @Published var config: AppSettings,
  persisted to ~/Library/Application Support/Stockbar/settings.json
- Computed accessors (statusBarStockId, refreshInterval, colorScheme,
  displayCurrency) maintain same API so views require no changes
- Fix views: appState.settings.* → appState.config.*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 10:12:59 +08:00
vgearenandClaude Sonnet 4.6 28cf10480b Rename app from StockMonitor to Stockbar
- Bundle ID: com.vgearen.StockMonitor → com.vgearen.Stockbar
- Display name: Stock Monitor → Stockbar
- App entry struct: StockMonitorApp → StockbarApp
- App Support path: ~/Library/Application Support/Stockbar
- Log path: ~/Library/Logs/Stockbar
- OSLog subsystem: com.vgearen.Stockbar
- Test module import: @testable import Stockbar
- GitHub Actions: scheme, DMG name updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 09:34:19 +08:00
vgearenandClaude Sonnet 4.6 a119f89624 Fix config data loss on startup and Codable forward-compatibility
- AppState.init: use _stocks = Published(wrappedValue:) to bypass didSet,
  preventing empty array from overwriting disk on decode failure
- AppState.saveStocks: guard against overwriting non-empty file with []
- Stock: explicit Codable extension using decodeIfPresent for optional fields,
  so missing fields in old JSON use defaults instead of failing the whole decode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 09:17:14 +08:00
vgearenandClaude Sonnet 4.6 3227b4fcda Add lifecycle logging to diagnose startup crashes
- AppLogger.swift: global OSLog Logger + logToFile() writing to
  ~/Library/Logs/StockMonitor/app.log
- AppDelegate: log applicationDidFinishLaunching / WillTerminate / ShouldTerminate
- AppState.init: log start, stocks-loaded count, and init-complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 09:01:41 +08:00
vgearenandClaude Sonnet 4.6 5a482ac818 Add currency conversion for portfolio summary
- CurrencyService: fetch USD/CNY and USD/HKD from Sina Finance forex
- DisplayCurrency enum (CNY/HKD/USD) with symbol and display name
- AppState: fetch rates on every refresh, convert totalPnL/totalDailyPnL
- Settings: add 持仓汇总货币 segmented picker
- ProfitSummaryView: show currency symbol in P&L display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:51:42 +08:00
vgearenandClaude Sonnet 4.6 6b1f565d7d Initial release: macOS menu bar stock monitor
- A-shares, HK, and US stock real-time quotes (Sina / Tencent)
- Intraday minute charts (Tencent / Yahoo Finance)
- US extended-hours session indicator with price and change%
- Portfolio P&L tracking (cost price × shares)
- Sort by change%, color themes, launch at login
- File-based persistence with 10-snapshot rolling backup
- Universal binary (Apple Silicon + Intel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 17:41:52 +08:00