diff --git a/src/main.zig b/src/main.zig index f8ad78b..f68506e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -501,7 +501,7 @@ fn runCli(init: std.process.Init) !u8 { // loader resolve + union-merge the same way the CLI does. // This is the load-bearing fix for "CLI and TUI report // different totals" - there's exactly one code path now. - tui.run(io, gpa_alloc, tui_config, globals.portfolio_patterns, globals.watchlist_path, resolved_theme, cmd_args, today) catch |err| switch (err) { + tui.run(io, gpa_alloc, tui_config, globals.portfolio_patterns, globals.watchlist_path, resolved_theme, cmd_args, today, globals.refresh_policy) catch |err| switch (err) { // tui.run already printed an actionable stderr message // for invalid CLI args; surface as exit 1 without a // panic / stack trace. diff --git a/src/tui.zig b/src/tui.zig index a5d8006..4fce302 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -502,6 +502,12 @@ pub const App = struct { local_tz: zfin.market.TimeZone, config: zfin.Config, svc: *zfin.DataService, + /// Cache-vs-network policy from the global `--refresh-data` flag, + /// mapped once at init. Threaded into every automatic data fetch + /// (the initial `portfolio.load` and each tab's `loadData`) so the + /// TUI honors `--refresh-data=never` (offline) and `=force` the same + /// way the CLI commands do. Default `.{}` is auto (respect TTL). + fetch_options: zfin.FetchOptions = .{}, keymap: keybinds.KeyMap, theme: theme.Theme, active_tab: Tab = .portfolio, @@ -2510,6 +2516,7 @@ pub fn run( app_theme: theme.Theme, args: []const []const u8, today: zfin.Date, + refresh_policy: framework.RefreshPolicy, ) !void { const watchlist_path: ?[]const u8 = global_watchlist_path; @@ -2593,6 +2600,7 @@ pub fn run( .local_tz = local_tz, .config = config, .svc = svc, + .fetch_options = cli.fetchOptionsFromPolicy(refresh_policy), .keymap = keymap, .theme = app_theme, .symbol = symbol, @@ -2684,6 +2692,8 @@ pub fn run( .progress = symbol_progress.callback(), .aggregate_progress = aggregate_progress.callback(), .watchlist_syms = watch_syms.items, + .force_refresh = app_inst.fetch_options.force_refresh, + .skip_network = app_inst.fetch_options.skip_network, }) catch |err| blk: { std.log.scoped(.tui).warn("portfolio load failed: {t}", .{err}); break :blk null; diff --git a/src/tui/earnings_tab.zig b/src/tui/earnings_tab.zig index 15b498d..c85e894 100644 --- a/src/tui/earnings_tab.zig +++ b/src/tui/earnings_tab.zig @@ -87,7 +87,10 @@ pub const tab = struct { /// frees current payload, clears flags, and re-runs the /// fetch path. pub fn reload(state: *State, app: *App) !void { - if (app.symbol.len > 0) { + // Offline mode (--refresh-data=never): skip cache invalidation. + // loadData can't re-fetch, so dropping the entry would just blank + // the tab; instead re-render from the cached entry. + if (app.symbol.len > 0 and !app.fetch_options.skip_network) { app.svc.invalidate(app.symbol, .earnings); } // Clear every flag so loadData has the same starting @@ -135,7 +138,7 @@ fn loadData(state: *State, app: *App) void { state.loaded = true; state.error_msg = null; - const result = app.svc.getEarnings(app.symbol, .{}) catch |err| { + const result = app.svc.getEarnings(app.symbol, app.fetch_options) catch |err| { switch (err) { zfin.DataError.NoApiKey => { state.error_msg = "No API key. Set FMP_API_KEY (free at financialmodelingprep.com)"; diff --git a/src/tui/options_tab.zig b/src/tui/options_tab.zig index d2ada4b..70e5012 100644 --- a/src/tui/options_tab.zig +++ b/src/tui/options_tab.zig @@ -143,7 +143,10 @@ pub const tab = struct { pub const deactivate = framework.noopDeactivate(State); pub fn reload(state: *State, app: *App) !void { - if (app.symbol.len > 0) { + // Offline mode (--refresh-data=never): skip cache invalidation. + // loadData can't re-fetch, so dropping the entry would just blank + // the tab; instead re-render from the cached entry. + if (app.symbol.len > 0 and !app.fetch_options.skip_network) { app.svc.invalidate(app.symbol, .options); } // Drop chains first so loadData starts clean. @@ -298,7 +301,7 @@ fn loadData(state: *State, app: *App) void { } state.chains = null; - const result = app.svc.getOptions(app.symbol, .{}) catch |err| { + const result = app.svc.getOptions(app.symbol, app.fetch_options) catch |err| { switch (err) { zfin.DataError.FetchFailed => app.setStatus("CBOE fetch failed (network error)"), else => app.setStatus("Error loading options"), diff --git a/src/tui/performance_tab.zig b/src/tui/performance_tab.zig index bc10ab6..6772c7b 100644 --- a/src/tui/performance_tab.zig +++ b/src/tui/performance_tab.zig @@ -119,7 +119,7 @@ fn loadData(state: *State, app: *App) void { app.symbol_data.trailing_me_price = null; app.symbol_data.trailing_me_total = null; - const result = app.svc.getTrailingReturns(app.symbol, .{}) catch |err| { + const result = app.svc.getTrailingReturns(app.symbol, app.fetch_options) catch |err| { switch (err) { zfin.DataError.NoApiKey => app.setStatus("No API key. Set TIINGO_API_KEY"), zfin.DataError.FetchFailed => app.setStatus("Fetch failed (network error or rate limit)"), @@ -155,7 +155,7 @@ fn loadData(state: *State, app: *App) void { // Try to load ETF profile (non-fatal, won't show for non-ETFs) if (!app.symbol_data.etf_loaded) { app.symbol_data.etf_loaded = true; - if (app.svc.getEtfProfile(app.symbol, .{})) |etf_result| { + if (app.svc.getEtfProfile(app.symbol, app.fetch_options)) |etf_result| { if (etf_result.data.isEtf()) { // Take ownership of the EtfProfile data. We // deliberately don't call etf_result.deinit diff --git a/src/tui/quote_tab.zig b/src/tui/quote_tab.zig index 2056473..89b7957 100644 --- a/src/tui/quote_tab.zig +++ b/src/tui/quote_tab.zig @@ -174,7 +174,7 @@ fn refreshLiveQuote(state: *State, app: *App, force: bool) void { // "refreshed Xs ago" header timestamp. const now_s = std.Io.Timestamp.now(app.io, .real).toSeconds(); if (!force and !shouldFetchLiveQuote(market.marketSession(now_s), state.live != null, now_s - state.timestamp)) return; - if (app.svc.getQuote(app.symbol, .{})) |q| { + if (app.svc.getQuote(app.symbol, app.fetch_options)) |q| { state.live = q; state.timestamp = now_s; } else |err| std.log.scoped(.quote_tab).debug("{s}: live-quote fetch failed: {t}", .{ app.symbol, err });