78c1d70126e3
fix nina log: parse JSON-lines log into date/time/action/outcome columns (was dumping raw JSON); write new entries as JSON with real Io.Clock.real timestamp (kills hardcoded 2026/00:00)
- time
- 2026-06-04 23:11 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- log json formatting
- parent
- 46652f694f90
↓ nina-78c1d701.tar.gzatlas at this save
files changed1 file
src/commands.zig+70 −10src/commands.zig+70 −10
@@ -368,16 +368,56 @@368368 defer ctx.gpa.free(content);369369 var lines = std.mem.splitScalar(u8, content, '\n');370370 while (lines.next()) |line| {371- if (line.len == 0) continue;372- var parts = std.mem.tokenizeScalar(u8, line, '\t');373- const date = parts.next() orelse continue;374- const time = parts.next() orelse "";375- const action = parts.next() orelse "";376- const detail = parts.next() orelse "";377- output.logRow(date, time, action, detail);371+ const t = std.mem.trim(u8, line, " \t\r");372+ if (t.len == 0) continue;373+ if (t[0] == '{') {374+ renderJsonLogLine(ctx.gpa, t);375+ } else {376+ // legacy tab-separated format377+ var parts = std.mem.tokenizeScalar(u8, t, '\t');378+ const date = parts.next() orelse continue;379+ const time = parts.next() orelse "";380+ const action = parts.next() orelse "";381+ const detail = parts.next() orelse "";382+ output.logRow(date, time, action, detail);383+ }378384 }379385 }380386 387+// The on-disk log is JSON Lines: {"ts","machine","command","outcome","gen_after",...}.388+// Render ts -> date/time columns, command -> action, outcome(+gen) -> detail.389+fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {390+ const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {391+ output.logRow(line, "", "", ""); // unparseable — show raw rather than drop392+ return;393+ };394+ defer parsed.deinit();395+ if (parsed.value != .object) return;396+ const o = parsed.value.object;397+398+ const ts = jsonStr(o, "ts");399+ const date = if (ts.len >= 10) ts[0..10] else ts;400+ const time = if (ts.len >= 16) ts[11..16] else "";401+ const action = jsonStr(o, "command");402+ const outcome = jsonStr(o, "outcome");403+404+ var buf: [160]u8 = undefined;405+ var detail: []const u8 = outcome;406+ if (o.get("gen_after")) |g| {407+ if (g == .integer) {408+ detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;409+ }410+ }411+ output.logRow(date, time, action, detail);412+}413+414+fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {415+ if (o.get(key)) |v| {416+ if (v == .string) return v.string;417+ }418+ return "";419+}420+381421 pub fn edit(ctx: Ctx) !void {382422 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");383423 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";@@ -867,9 +907,29 @@867907 // --- Log helper ---868908 869909 fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {870- var buf: [128]u8 = undefined;871- const entry = std.fmt.bufPrint(&buf, "{s}\t{s}\t{s}\tgen {d}\n", .{872- "2026", "00:00", action, gen,910+ var ts_buf: [32]u8 = undefined;911+ const ts = nowIso(ctx.io, &ts_buf);912+ var buf: [256]u8 = undefined;913+ const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{914+ ts, ctx.machine.name, action, gen,873915 }) catch return;874916 exec.appendNinaLog(ctx.io, ctx.environ, entry);917+}918+919+// Wall-clock UTC as ISO-8601. Uses Io.Clock.real (the settable Unix-epoch clock);920+// Io.Clock.awake is monotonic and unsuitable for timestamps. Falls back to epoch 0.921+fn nowIso(io: std.Io, buf: []u8) []const u8 {922+ const fallback = "1970-01-01T00:00:00Z";923+ const ns = std.Io.Clock.real.now(io).toNanoseconds();924+ if (ns <= 0) return fallback;925+ const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));926+ const es = std.time.epoch.EpochSeconds{ .secs = secs };927+ const ed = es.getEpochDay();928+ const yd = ed.calculateYearDay();929+ const md = yd.calculateMonthDay();930+ const ds = es.getDaySeconds();931+ return std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}Z", .{932+ yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,933+ ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),934+ }) catch return fallback;875935 }