ai: restore progress in CLI/add to TUI
This commit is contained in:
parent
7b07c09f70
commit
86a764dca9
3 changed files with 156 additions and 13 deletions
|
|
@ -3,6 +3,49 @@ const zfin = @import("../root.zig");
|
||||||
const cli = @import("common.zig");
|
const cli = @import("common.zig");
|
||||||
const fmt = cli.fmt;
|
const fmt = cli.fmt;
|
||||||
|
|
||||||
|
/// CLI progress context for loadPrices callback.
|
||||||
|
const CliProgress = struct {
|
||||||
|
svc: *zfin.DataService,
|
||||||
|
color: bool,
|
||||||
|
/// Offset added to index for display (e.g. stock count when loading watch symbols).
|
||||||
|
index_offset: usize,
|
||||||
|
/// Grand total across all loadPrices calls (stocks + watch).
|
||||||
|
grand_total: usize,
|
||||||
|
|
||||||
|
fn onProgress(ctx: *anyopaque, index: usize, _: usize, symbol: []const u8, status: zfin.DataService.SymbolStatus) void {
|
||||||
|
const self: *CliProgress = @ptrCast(@alignCast(ctx));
|
||||||
|
const display_idx = self.index_offset + index + 1;
|
||||||
|
switch (status) {
|
||||||
|
.fetching => {
|
||||||
|
// Show rate-limit wait before the fetch
|
||||||
|
if (self.svc.estimateWaitSeconds()) |w| {
|
||||||
|
if (w > 0) cli.stderrRateLimitWait(w, self.color) catch {};
|
||||||
|
}
|
||||||
|
cli.stderrProgress(symbol, " (fetching)", display_idx, self.grand_total, self.color) catch {};
|
||||||
|
},
|
||||||
|
.cached => {
|
||||||
|
cli.stderrProgress(symbol, " (cached)", display_idx, self.grand_total, self.color) catch {};
|
||||||
|
},
|
||||||
|
.fetched => {
|
||||||
|
// Already showed "(fetching)" — no extra line needed
|
||||||
|
},
|
||||||
|
.failed_used_stale => {
|
||||||
|
cli.stderrProgress(symbol, " FAILED (using cached)", display_idx, self.grand_total, self.color) catch {};
|
||||||
|
},
|
||||||
|
.failed => {
|
||||||
|
cli.stderrProgress(symbol, " FAILED", display_idx, self.grand_total, self.color) catch {};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn callback(self: *CliProgress) zfin.DataService.ProgressCallback {
|
||||||
|
return .{
|
||||||
|
.context = @ptrCast(self),
|
||||||
|
.on_progress = onProgress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void {
|
pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void {
|
||||||
// Load portfolio from SRF file
|
// Load portfolio from SRF file
|
||||||
const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| {
|
const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| {
|
||||||
|
|
@ -60,12 +103,21 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
|
||||||
try cli.stderrPrint("Warning: TWELVEDATA_API_KEY not set. Cannot fetch current prices.\n");
|
try cli.stderrPrint("Warning: TWELVEDATA_API_KEY not set. Cannot fetch current prices.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Progress callback for per-symbol output
|
||||||
|
var progress_ctx = CliProgress{
|
||||||
|
.svc = svc,
|
||||||
|
.color = color,
|
||||||
|
.index_offset = 0,
|
||||||
|
.grand_total = all_syms_count,
|
||||||
|
};
|
||||||
|
|
||||||
// Load prices for stock/ETF positions
|
// Load prices for stock/ETF positions
|
||||||
const load_result = svc.loadPrices(syms, &prices, force_refresh);
|
const load_result = svc.loadPrices(syms, &prices, force_refresh, progress_ctx.callback());
|
||||||
fail_count = load_result.fail_count;
|
fail_count = load_result.fail_count;
|
||||||
|
|
||||||
// Fetch watch symbol candles (for watchlist display, not portfolio value)
|
// Fetch watch symbol candles (for watchlist display, not portfolio value)
|
||||||
_ = svc.loadPrices(watch_syms.items, &prices, force_refresh);
|
progress_ctx.index_offset = syms.len;
|
||||||
|
_ = svc.loadPrices(watch_syms.items, &prices, force_refresh, progress_ctx.callback());
|
||||||
|
|
||||||
// Summary line
|
// Summary line
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,31 @@ pub const DataService = struct {
|
||||||
latest_date: ?Date,
|
latest_date: ?Date,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Status emitted for each symbol during price loading.
|
||||||
|
pub const SymbolStatus = enum {
|
||||||
|
/// Price resolved from fresh cache.
|
||||||
|
cached,
|
||||||
|
/// About to attempt an API fetch (emitted before the network call).
|
||||||
|
fetching,
|
||||||
|
/// Price fetched successfully from API.
|
||||||
|
fetched,
|
||||||
|
/// API fetch failed but stale cached price was used.
|
||||||
|
failed_used_stale,
|
||||||
|
/// API fetch failed and no cached price exists.
|
||||||
|
failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Callback for progress reporting during price loading.
|
||||||
|
/// `context` is an opaque pointer to caller-owned state.
|
||||||
|
pub const ProgressCallback = struct {
|
||||||
|
context: *anyopaque,
|
||||||
|
on_progress: *const fn (ctx: *anyopaque, index: usize, total: usize, symbol: []const u8, status: SymbolStatus) void,
|
||||||
|
|
||||||
|
fn emit(self: ProgressCallback, index: usize, total: usize, symbol: []const u8, status: SymbolStatus) void {
|
||||||
|
self.on_progress(self.context, index, total, symbol, status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// Load current prices for a list of symbols into `prices`.
|
/// Load current prices for a list of symbols into `prices`.
|
||||||
///
|
///
|
||||||
/// For each symbol the resolution order is:
|
/// For each symbol the resolution order is:
|
||||||
|
|
@ -474,11 +499,13 @@ pub const DataService = struct {
|
||||||
/// 3. Stale cache -> use last close from expired cache entry
|
/// 3. Stale cache -> use last close from expired cache entry
|
||||||
///
|
///
|
||||||
/// If `force_refresh` is true, cache is invalidated before checking freshness.
|
/// If `force_refresh` is true, cache is invalidated before checking freshness.
|
||||||
|
/// If `progress` is provided, it is called for each symbol with the outcome.
|
||||||
pub fn loadPrices(
|
pub fn loadPrices(
|
||||||
self: *DataService,
|
self: *DataService,
|
||||||
symbols: []const []const u8,
|
symbols: []const []const u8,
|
||||||
prices: *std.StringHashMap(f64),
|
prices: *std.StringHashMap(f64),
|
||||||
force_refresh: bool,
|
force_refresh: bool,
|
||||||
|
progress: ?ProgressCallback,
|
||||||
) PriceLoadResult {
|
) PriceLoadResult {
|
||||||
var result = PriceLoadResult{
|
var result = PriceLoadResult{
|
||||||
.cached_count = 0,
|
.cached_count = 0,
|
||||||
|
|
@ -487,8 +514,9 @@ pub const DataService = struct {
|
||||||
.stale_count = 0,
|
.stale_count = 0,
|
||||||
.latest_date = null,
|
.latest_date = null,
|
||||||
};
|
};
|
||||||
|
const total = symbols.len;
|
||||||
|
|
||||||
for (symbols) |sym| {
|
for (symbols, 0..) |sym, i| {
|
||||||
if (force_refresh) {
|
if (force_refresh) {
|
||||||
self.invalidate(sym, .candles_daily);
|
self.invalidate(sym, .candles_daily);
|
||||||
}
|
}
|
||||||
|
|
@ -499,9 +527,13 @@ pub const DataService = struct {
|
||||||
prices.put(sym, close) catch {};
|
prices.put(sym, close) catch {};
|
||||||
}
|
}
|
||||||
result.cached_count += 1;
|
result.cached_count += 1;
|
||||||
|
if (progress) |p| p.emit(i, total, sym, .cached);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// About to fetch — notify caller (so it can show rate-limit waits etc.)
|
||||||
|
if (progress) |p| p.emit(i, total, sym, .fetching);
|
||||||
|
|
||||||
// 2. Try API fetch
|
// 2. Try API fetch
|
||||||
if (self.getCandles(sym)) |candle_result| {
|
if (self.getCandles(sym)) |candle_result| {
|
||||||
defer self.allocator.free(candle_result.data);
|
defer self.allocator.free(candle_result.data);
|
||||||
|
|
@ -513,6 +545,7 @@ pub const DataService = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.fetched_count += 1;
|
result.fetched_count += 1;
|
||||||
|
if (progress) |p| p.emit(i, total, sym, .fetched);
|
||||||
continue;
|
continue;
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
|
|
||||||
|
|
@ -521,6 +554,9 @@ pub const DataService = struct {
|
||||||
if (self.getCachedLastClose(sym)) |close| {
|
if (self.getCachedLastClose(sym)) |close| {
|
||||||
prices.put(sym, close) catch {};
|
prices.put(sym, close) catch {};
|
||||||
result.stale_count += 1;
|
result.stale_count += 1;
|
||||||
|
if (progress) |p| p.emit(i, total, sym, .failed_used_stale);
|
||||||
|
} else {
|
||||||
|
if (progress) |p| p.emit(i, total, sym, .failed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
75
src/tui.zig
75
src/tui.zig
|
|
@ -1067,8 +1067,40 @@ const App = struct {
|
||||||
var fail_count: usize = 0;
|
var fail_count: usize = 0;
|
||||||
var fetch_count: usize = 0;
|
var fetch_count: usize = 0;
|
||||||
var stale_count: usize = 0;
|
var stale_count: usize = 0;
|
||||||
|
var failed_syms: [8][]const u8 = undefined;
|
||||||
{
|
{
|
||||||
const load_result = self.svc.loadPrices(syms, &prices, false);
|
const TuiProgress = struct {
|
||||||
|
app: *App,
|
||||||
|
failed: *[8][]const u8,
|
||||||
|
fail_n: usize = 0,
|
||||||
|
|
||||||
|
fn onProgress(ctx: *anyopaque, _: usize, _: usize, symbol: []const u8, status: zfin.DataService.SymbolStatus) void {
|
||||||
|
const s: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
switch (status) {
|
||||||
|
.fetching => {
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
const msg = std.fmt.bufPrint(&buf, "Loading {s}...", .{symbol}) catch "Loading...";
|
||||||
|
s.app.setStatus(msg);
|
||||||
|
},
|
||||||
|
.failed, .failed_used_stale => {
|
||||||
|
if (s.fail_n < s.failed.len) {
|
||||||
|
s.failed[s.fail_n] = symbol;
|
||||||
|
s.fail_n += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn callback(s: *@This()) zfin.DataService.ProgressCallback {
|
||||||
|
return .{
|
||||||
|
.context = @ptrCast(s),
|
||||||
|
.on_progress = onProgress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var tui_progress = TuiProgress{ .app = self, .failed = &failed_syms };
|
||||||
|
const load_result = self.svc.loadPrices(syms, &prices, false, tui_progress.callback());
|
||||||
latest_date = load_result.latest_date;
|
latest_date = load_result.latest_date;
|
||||||
fail_count = load_result.fail_count;
|
fail_count = load_result.fail_count;
|
||||||
fetch_count = load_result.fetched_count;
|
fetch_count = load_result.fetched_count;
|
||||||
|
|
@ -1130,16 +1162,39 @@ const App = struct {
|
||||||
// Show warning if any securities failed to load
|
// Show warning if any securities failed to load
|
||||||
if (fail_count > 0) {
|
if (fail_count > 0) {
|
||||||
var warn_buf: [256]u8 = undefined;
|
var warn_buf: [256]u8 = undefined;
|
||||||
const stale = stale_count;
|
if (fail_count <= 3) {
|
||||||
if (stale > 0 and stale == fail_count) {
|
// Show actual symbol names for easier debugging
|
||||||
const warn_msg = std.fmt.bufPrint(&warn_buf, "{d} symbols failed to refresh (using stale cache) | r/F5 to retry", .{fail_count}) catch "Warning: some securities used stale cache";
|
var sym_buf: [128]u8 = undefined;
|
||||||
self.setStatus(warn_msg);
|
var sym_len: usize = 0;
|
||||||
} else if (stale > 0) {
|
const show = @min(fail_count, failed_syms.len);
|
||||||
const warn_msg = std.fmt.bufPrint(&warn_buf, "{d} symbols failed ({d} using stale cache) | r/F5 to retry", .{ fail_count, stale }) catch "Warning: some securities failed";
|
for (0..show) |fi| {
|
||||||
self.setStatus(warn_msg);
|
if (sym_len > 0) {
|
||||||
|
if (sym_len + 2 < sym_buf.len) {
|
||||||
|
sym_buf[sym_len] = ',';
|
||||||
|
sym_buf[sym_len + 1] = ' ';
|
||||||
|
sym_len += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const s = failed_syms[fi];
|
||||||
|
const copy_len = @min(s.len, sym_buf.len - sym_len);
|
||||||
|
@memcpy(sym_buf[sym_len..][0..copy_len], s[0..copy_len]);
|
||||||
|
sym_len += copy_len;
|
||||||
|
}
|
||||||
|
if (stale_count > 0) {
|
||||||
|
const warn_msg = std.fmt.bufPrint(&warn_buf, "Failed to refresh: {s} (using stale cache)", .{sym_buf[0..sym_len]}) catch "Warning: some securities failed";
|
||||||
|
self.setStatus(warn_msg);
|
||||||
|
} else {
|
||||||
|
const warn_msg = std.fmt.bufPrint(&warn_buf, "Failed to load: {s}", .{sym_buf[0..sym_len]}) catch "Warning: some securities failed";
|
||||||
|
self.setStatus(warn_msg);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const warn_msg = std.fmt.bufPrint(&warn_buf, "Warning: {d} securities failed to load prices", .{fail_count}) catch "Warning: some securities failed";
|
if (stale_count > 0 and stale_count == fail_count) {
|
||||||
self.setStatus(warn_msg);
|
const warn_msg = std.fmt.bufPrint(&warn_buf, "{d} symbols failed to refresh (using stale cache) | r/F5 to retry", .{fail_count}) catch "Warning: some securities used stale cache";
|
||||||
|
self.setStatus(warn_msg);
|
||||||
|
} else {
|
||||||
|
const warn_msg = std.fmt.bufPrint(&warn_buf, "Warning: {d} securities failed to load prices", .{fail_count}) catch "Warning: some securities failed";
|
||||||
|
self.setStatus(warn_msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (fetch_count > 0) {
|
} else if (fetch_count > 0) {
|
||||||
var info_buf: [128]u8 = undefined;
|
var info_buf: [128]u8 = undefined;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue