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
1 file src/commands.zig+70 −10
src/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 }

view file