╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │17 setHtml(html); │ │18 } │ │19 │ │20 -// the page is allocated into this arena via parseFromSliceLeaky. it has │ │21 -// to outlive `root`, because `root` holds slices into the parsed strings. │ │20 +// the snapshot is allocated into this arena via parseFromSliceLeaky. it │ │21 +// has to outlive `root`, because `root` holds slices into the parsed strings.│ │22 var page_arena: ?std.heap.ArenaAllocator = null; │ │23 -var page: ?ui.Page = null; │ │23 +var snapshot: ?ui.Snapshot = null; │ │24 var session: ui.Session = .{}; │ │25 │ │26 fn start(json: []const u8, min_height: u32, max_width: u32) !void { │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │29 │ │30 // alloc_always so the parsed strings live entirely inside the arena │ │31 // and don't borrow from the caller's json buffer. │ │32 - page = try std.json.parseFromSliceLeaky(ui.Page, (page_arena orelse unreachable).allocator(), json, .{ │ │32 + snapshot = try std.json.parseFromSliceLeaky(ui.Snapshot, (page_arena orelse unreachable).allocator(), json, .{│ │33 .allocate = .alloc_always, │ │34 }); │ │35 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │36 - // restore session.user_id from the serialized page so the first render │ │37 - // matches what the server rendered (otherwise wasm would render the │ │38 - // page in a logged-out state on top of a logged-in server-rendered HTML). │ │39 - session.user_id = switch (page orelse unreachable) { │ │40 - .home => |h| h.user_id, │ │41 - }; │ │36 + // adopt the server's session view wholesale — one assignment instead of │ │37 + // hand-restoring each field by reaching into the page's data tree. │ │38 + session.data = (snapshot orelse unreachable).session; │ │39 │ │43 - var next_root = try ui.initRoot(allocator, &(page orelse unreachable), &session); │ │40 + var next_root = try ui.initRoot(allocator, &(snapshot orelse unreachable).page, &session);│ │41 errdefer next_root.deinit(allocator); │ │42 │ │43 if (root) |*old_root| old_root.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │153 } │ │154 const ti: *wgt.TextInput(ui.Widget) = @fieldParentPtr("focus", child.focus);│ │155 try ti.setContent(allocator, bytes); │ │156 + // mirror the TTY behavior in Login.View.input: editing an input clears │ │157 + // the stale failure flag so the "(invalid)" label doesn't linger after │ │158 + // the user has typed a correction │ │159 + session.data.login_failure = null; │ │160 } │ │161 │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │20 }; │ │21 │ │22 // per-connection mutable state. each SSH session / web session / local TUI │ │23 -// run gets its own │ │23 +// run gets its own. `data` is the subset that round-trips between server │ │24 +// and wasm; the other fields are runtime context only and stay local. │ │25 pub const Session = struct { │ │25 - user_id: ?[]const u8 = null, │ │26 + data: SessionData = .{}, │ │27 arena: ?*std.heap.ArenaAllocator = null, │ │28 haxy_moment: ?DB.HashMap(.read_only) = null, │ │29 +}; │ │30 + │ │31 +// the serializable part of session │ │32 +pub const SessionData = struct { │ │33 + user_id: ?[]const u8 = null, │ │34 + // a transient outcome to surface from the last /login POST attempt │ │35 + login_failure: ?Home.Auth.Login.Failure = null, │ │36 }; │ │37 │ │38 +// what the server hands to the client (and what main_wasm parses on _start). │ │39 +// keeps Page free of any per-request session state. │ │40 +pub const Snapshot = struct { │ │41 + page: Page, │ │42 + session: SessionData = .{}, │ │43 +}; │ │44 + │ │45 pub fn run(io: std.Io, allocator: std.mem.Allocator, page: *const Page, session: *Session) !void {│ │46 var root = try initRoot(allocator, page, session); │ │47 defer root.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │450 // when only a min is set (e.g. the wasm path passes viewport rows │ │451 // as min_height with no max), we can still vertically center while │ │452 // letting taller content extend past the min. │ │438 - const width = if (constraint.max_size.width) |w| w │ │439 - else if (constraint.min_size.width) |min_w| @max(min_w, child_grid.size.width) │ │440 - else child_grid.size.width; │ │441 - const height = if (constraint.max_size.height) |h| h │ │442 - else if (constraint.min_size.height) |min_h| @max(min_h, child_grid.size.height) │ │443 - else child_grid.size.height; │ │453 + const width = if (constraint.max_size.width) |w| w else if (constraint.min_size.width) |min_w| @max(min_w, child_grid.size.width) else child_grid.size.width; │ │454 + const height = if (constraint.max_size.height) |h| h else if (constraint.min_size.height) |min_h| @max(min_h, child_grid.size.height) else child_grid.size.height;│ │455 if (width == 0 or height == 0) return; │ │456 │ │457 const offset_x: usize = if (self.direction == .horiz or self.direction == .both) │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Home.zig│ ╰───────────────╯ ╭────────────────────────────────╮ │19 users: Users, │ │20 repos: Repos, │ │21 auth: Auth, │ │22 -user_id: ?[]const u8 = null,│ │22 │ │23 const Self = @This(); │ │24 │ ╰────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │58 .users = try Users.init(repo_opts, arena, haxy_moment),│ │59 .repos = try Repos.init(repo_opts, arena, haxy_moment),│ │60 .auth = Auth.init(), │ │62 - .user_id = session.user_id, │ │61 }; │ │62 } │ │63 │ ╰───────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Home/Auth.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │53 │ │54 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ │55 self.focus.clear(); │ │56 - if (self.session.user_id != null) { │ │56 + if (self.session.data.user_id != null) { │ │57 try self.logout.build(allocator, constraint, root_focus); │ │58 if (self.logout.getGrid()) |inner_grid| { │ │59 try self.focus.addChild(self.logout.getFocus(), inner_grid.size, 0, 0); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │69 } │ │70 │ │71 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void {│ │72 - if (self.session.user_id != null) { │ │72 + if (self.session.data.user_id != null) { │ │73 try self.logout.input(allocator, key, root_focus); │ │74 } else { │ │75 try self.login.input(allocator, key, root_focus); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯