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 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 {
|
||||
// Load portfolio from SRF file
|
||||
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");
|
||||
}
|
||||
|
||||
// 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
|
||||
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;
|
||||
|
||||
// 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -466,6 +466,31 @@ pub const DataService = struct {
|
|||
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`.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// 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(
|
||||
self: *DataService,
|
||||
symbols: []const []const u8,
|
||||
prices: *std.StringHashMap(f64),
|
||||
force_refresh: bool,
|
||||
progress: ?ProgressCallback,
|
||||
) PriceLoadResult {
|
||||
var result = PriceLoadResult{
|
||||
.cached_count = 0,
|
||||
|
|
@ -487,8 +514,9 @@ pub const DataService = struct {
|
|||
.stale_count = 0,
|
||||
.latest_date = null,
|
||||
};
|
||||
const total = symbols.len;
|
||||
|
||||
for (symbols) |sym| {
|
||||
for (symbols, 0..) |sym, i| {
|
||||
if (force_refresh) {
|
||||
self.invalidate(sym, .candles_daily);
|
||||
}
|
||||
|
|
@ -499,9 +527,13 @@ pub const DataService = struct {
|
|||
prices.put(sym, close) catch {};
|
||||
}
|
||||
result.cached_count += 1;
|
||||
if (progress) |p| p.emit(i, total, sym, .cached);
|
||||
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
|
||||
if (self.getCandles(sym)) |candle_result| {
|
||||
defer self.allocator.free(candle_result.data);
|
||||
|
|
@ -513,6 +545,7 @@ pub const DataService = struct {
|
|||
}
|
||||
}
|
||||
result.fetched_count += 1;
|
||||
if (progress) |p| p.emit(i, total, sym, .fetched);
|
||||
continue;
|
||||
} else |_| {}
|
||||
|
||||
|
|
@ -521,6 +554,9 @@ pub const DataService = struct {
|
|||
if (self.getCachedLastClose(sym)) |close| {
|
||||
prices.put(sym, close) catch {};
|
||||
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 fetch_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;
|
||||
fail_count = load_result.fail_count;
|
||||
fetch_count = load_result.fetched_count;
|
||||
|
|
@ -1130,16 +1162,39 @@ const App = struct {
|
|||
// Show warning if any securities failed to load
|
||||
if (fail_count > 0) {
|
||||
var warn_buf: [256]u8 = undefined;
|
||||
const stale = stale_count;
|
||||
if (stale > 0 and stale == fail_count) {
|
||||
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 if (stale > 0) {
|
||||
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";
|
||||
self.setStatus(warn_msg);
|
||||
if (fail_count <= 3) {
|
||||
// Show actual symbol names for easier debugging
|
||||
var sym_buf: [128]u8 = undefined;
|
||||
var sym_len: usize = 0;
|
||||
const show = @min(fail_count, failed_syms.len);
|
||||
for (0..show) |fi| {
|
||||
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 {
|
||||
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);
|
||||
if (stale_count > 0 and stale_count == fail_count) {
|
||||
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) {
|
||||
var info_buf: [128]u8 = undefined;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue