╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Header.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │55 const aa = session.page_arena.allocator(); │ │56 │ │57 // the user's name (local mode has no user pages to link to) │ │57 - if (session.local == null) { │ │58 + if (!session.data.is_local) { │ │59 const text = try std.fmt.allocPrint(aa, "{s}/", .{data.owner_name}); │ │60 const link = try std.fmt.allocPrint(aa, "a:/user/{s}", .{data.owner_name});│ │61 │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │71 }); │ │72 } │ │73 │ │73 - const identity = try std.fmt.allocPrint(aa, "{s}/{s}", .{ data.owner_name, data.name }); │ │74 + // local routes carry no identity, so their urls come out elided │ │75 + const identity = if (session.data.is_local) "" else try std.fmt.allocPrint(aa, "{s}/{s}", .{ data.owner_name, data.name });│ │76 │ │77 // title sits to the left of the tabs; it links to the repo's files root │ │78 // (the bare route, so it resolves to the default branch). │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │116 const files_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try files_route.urlAlloc(session.page_arena)}); │ │117 const commits_route = ui.RoutablePage.repoCommitsRoute(identity, data.ref_or_oid, data.ref_or_oid_value, 0) orelse return error.RouteTooLong;│ │118 const commits_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try commits_route.urlAlloc(session.page_arena)}); │ │117 - const refs_link = try std.fmt.allocPrint(aa, "ai:/repo/{s}/{s}/refs", .{ data.owner_name, data.name }); │ │118 - const issues_link = if (data.issues_tag.len == 0) │ │119 - try std.fmt.allocPrint(aa, "ai:/repo/{s}/{s}/issues", .{ data.owner_name, data.name }) │ │120 - else │ │121 - try std.fmt.allocPrint(aa, "ai:/repo/{s}/{s}/issues/tag/{s}", .{ data.owner_name, data.name, data.issues_tag }); │ │122 - const settings_link = try std.fmt.allocPrint(aa, "ai:/repo/{s}/{s}/settings", .{ data.owner_name, data.name }); │ │123 - const auth_link = try std.fmt.allocPrint(aa, "ai:/repo/{s}/{s}/auth", .{ data.owner_name, data.name }); │ │119 + const refs_route = ui.RoutablePage.repoRefsRoute(identity, .branch, 0) orelse return error.RouteTooLong; │ │120 + const refs_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try refs_route.urlAlloc(session.page_arena)}); │ │121 + const issues_route = ui.RoutablePage.repoIssuesRoute(identity, data.issues_tag, "") orelse return error.RouteTooLong; │ │122 + const issues_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try issues_route.urlAlloc(session.page_arena)}); │ │123 + const settings_route = ui.RoutablePage{ .repo_settings = Array.from(identity) orelse return error.RouteTooLong }; │ │124 + const settings_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try settings_route.urlAlloc(session.page_arena)}); │ │125 + const auth_route = ui.RoutablePage{ .repo_auth = Array.from(identity) orelse return error.RouteTooLong }; │ │126 + const auth_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try auth_route.urlAlloc(session.page_arena)}); │ │127 │ │128 // the tab matching the current page is focused initially; matching by │ │129 // link (rather than position) keeps this robust to tab changes. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │226 // auth tab (login / logout). AuthTab defaults to the global ai:/auth │ │227 // link; repoint its instance at this repo's auth route so it stays on│ │228 // this page. local mode has no accounts, so it has no auth tab. │ │226 - if (session.local == null) { │ │229 + if (!session.data.is_local) { │ │230 var auth_tab = try AuthTab.View.init(allocator, session); │ │231 errdefer auth_tab.deinit(allocator); │ │232 auth_tab.text_box.getFocus().kind = .{ .custom = auth_link }; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │25 .{ .path = "script.js", .content_type = "text/javascript; charset=utf-8", .body = @embedFile("embed/script.js") },│ │26 .{ .path = "term.ttf", .content_type = "font/ttf", .body = @embedFile("embed/term.ttf") }, │ │27 .{ .path = "haxy.wasm", .content_type = "application/wasm", .body = @embedFile("haxy.wasm") }, │ │28 +}; │ │29 + │ │30 +// what a web request is served from: the multi-user server (admin repo + │ │31 +// login sessions) or a single local repo. │ │32 +pub const Host = union(enum) { │ │33 + remote: struct { │ │34 + admin_repo_path: []const u8, │ │35 + session_store: SessionStore, │ │36 + }, │ │37 + local: ui.RepoSource, │ │38 }; │ │39 │ │40 pub fn handleConnection( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │41 io: std.Io, │ │42 allocator: std.mem.Allocator, │ │43 stream: std.Io.net.Stream, │ │34 - admin_repo_path: []const u8, │ │35 - session_store: SessionStore, │ │44 + host: Host, │ │45 err: *std.Io.Writer, │ │46 ) !void { │ │47 var send_buffer = [_]u8{0} ** 4096;│ ╰───────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │61 else => |e| return e, │ │62 }; │ │63 │ │55 - handleRequest(io, &request, allocator, admin_repo_path, session_store) catch |request_err| {│ │64 + handleRequest(io, &request, allocator, host) catch |request_err| { │ │65 try err.print("web ui request failed: {s}\n", .{@errorName(request_err)}); │ │66 try err.flush(); │ │67 // best-effort 500. if handleRequest already started writing a │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │79 io: std.Io, │ │80 request: *std.http.Server.Request, │ │81 allocator: std.mem.Allocator, │ │73 - admin_repo_path: []const u8, │ │74 - session_store: SessionStore, │ │82 + host: Host, │ │83 ) !void { │ │84 const method = request.head.method; │ │85 const uri = try std.Uri.parseAfterScheme("", request.head.target);│ ╰──────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 87 │ │ 88 // POST routes can be scoped by any page, so they can redirect using │ │ 89 // the base of the URL. this allows logging in to keep you on the page │ │ 82 - // you were on. │ │ 90 + // you were on. local mode has no accounts, so no POST routes. │ │ 91 if (method == .POST) { │ │ 84 - const PostRoute = enum { login, logout, ansi }; │ │ 85 - inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │ 86 - const suffix = "/" ++ field.name; │ │ 87 - if (std.mem.endsWith(u8, path, suffix)) { │ │ 88 - const base = path[0 .. path.len - suffix.len]; │ │ 89 - return switch (@field(PostRoute, field.name)) { │ │ 90 - .login => handleLogin(io, request, allocator, base, admin_repo_path, session_store), │ │ 91 - .logout => handleLogout(request, base, session_store), │ │ 92 - .ansi => handleAnsi(io, request, allocator, base, admin_repo_path, session_store), │ │ 93 - }; │ │ 94 - } │ │ 92 + switch (host) { │ │ 93 + .remote => |remote| { │ │ 94 + const PostRoute = enum { login, logout, ansi }; │ │ 95 + inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │ 96 + const suffix = "/" ++ field.name; │ │ 97 + if (std.mem.endsWith(u8, path, suffix)) { │ │ 98 + const base = path[0 .. path.len - suffix.len]; │ │ 99 + return switch (@field(PostRoute, field.name)) { │ │100 + .login => handleLogin(io, request, allocator, base, remote.admin_repo_path, remote.session_store),│ │101 + .logout => handleLogout(request, base, remote.session_store), │ │102 + .ansi => handleAnsi(io, request, allocator, base, remote.admin_repo_path, remote.session_store), │ │103 + }; │ │104 + } │ │105 + } │ │106 + }, │ │107 + .local => {}, │ │108 } │ │109 } │ │110 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │125 return; │ │126 } │ │127 │ │115 - if (ui.RoutablePage.fromUrl(path, if (uri.query) |comp| comp.percent_encoded else null)) |current_page| {│ │128 + const query = if (uri.query) |comp| comp.percent_encoded else null; │ │129 + const current_page_maybe = switch (host) { │ │130 + .remote => ui.RoutablePage.fromUrl(path, query), │ │131 + .local => ui.RoutablePage.fromUrlLocal(path, query), │ │132 + }; │ │133 + if (current_page_maybe) |current_page| { │ │134 // resolve the haxy_session cookie's token to a user_id via the │ │135 // store. user_id_buf lives on the stack for the rest of handleRequest, │ │118 - // which is plenty for renderIndexHtml to consume. │ │136 + // which is plenty for renderIndexHtml to consume. local mode has no │ │137 + // accounts, so it is always logged out. │ │138 var user_id_buf: [evt.event_id_size]u8 = undefined; │ │120 - const user_id: ?[]const u8 = blk: { │ │121 - const token = getCookieValue(request, cookie_name) orelse break :blk null; │ │122 - if (!session_store.lookup(token, &user_id_buf)) break :blk null; │ │123 - break :blk user_id_buf[0..evt.event_id_size]; │ │124 - }; │ │125 - const login_failure: ?ui.Home.Auth.Login.Failure = │ │126 - if (getCookieValue(request, login_failure_cookie)) |raw| │ │127 - if (std.mem.eql(u8, raw, "unknown_user")) │ │128 - .unknown_user │ │129 - else if (std.mem.eql(u8, raw, "wrong_password")) │ │130 - .wrong_password │ │139 + var user_id: ?[]const u8 = null; │ │140 + var login_failure: ?ui.Home.Auth.Login.Failure = null; │ │141 + switch (host) { │ │142 + .remote => |remote| { │ │143 + user_id = blk: { │ │144 + const token = getCookieValue(request, cookie_name) orelse break :blk null; │ │145 + if (!remote.session_store.lookup(token, &user_id_buf)) break :blk null; │ │146 + break :blk user_id_buf[0..evt.event_id_size]; │ │147 + }; │ │148 + login_failure = if (getCookieValue(request, login_failure_cookie)) |raw| │ │149 + if (std.mem.eql(u8, raw, "unknown_user")) │ │150 + .unknown_user │ │151 + else if (std.mem.eql(u8, raw, "wrong_password")) │ │152 + .wrong_password │ │153 + else │ │154 + null │ │155 else │ │132 - null │ │133 - else │ │134 - null; │ │156 + null; │ │157 + }, │ │158 + .local => {}, │ │159 + } │ │160 │ │136 - const html = renderIndexHtml(io, allocator, admin_repo_path, .{ │ │161 + const html = renderIndexHtml(io, allocator, host, .{ │ │162 .user_id = user_id, │ │163 .login_failure = login_failure, │ │164 .current_page = current_page, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯