add help/static file handling

This commit is contained in:
Emil Lerch 2025-10-15 15:26:26 -07:00
parent 463cc80c05
commit 2c0e7850d3
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 95 additions and 22 deletions

38
PLAN.md
View file

@ -30,26 +30,26 @@ Create a netviel clone with improvements:
- [x] Add tests for new functionality (existing tests pass)
- [x] Run `zig fmt .`
## Phase 3: HTTP Server & REST API
- [ ] Research and choose HTTP framework (defer decision)
- [ ] Add HTTP server dependency
- [ ] Implement REST endpoints:
- [ ] `GET /api/query/<query_string>` - search threads
- [ ] `GET /api/thread/<thread_id>` - get thread messages
- [ ] `GET /api/attachment/<message_id>/<num>` - download attachment
- [ ] `GET /api/message/<message_id>` - download raw .eml file
- [ ] Complete JSON serialization (extend existing in root.zig)
- [ ] Add security headers (CORS, X-Frame-Options, etc.)
- [ ] Add tests for API endpoints
- [ ] Run `zig fmt .`
## Phase 3: HTTP Server & REST API ✅ COMPLETE
- [x] Research and choose HTTP framework (httpz)
- [x] Add HTTP server dependency
- [x] Implement REST endpoints:
- [x] `GET /api/query/<query_string>` - search threads
- [x] `GET /api/thread/<thread_id>` - get thread messages
- [x] `GET /api/attachment/<message_id>/<num>` - download attachment
- [x] `GET /api/message/<message_id>` - get message details
- [x] Complete JSON serialization (extend existing in root.zig)
- [x] Add security headers via httpz middleware
- [x] Add tests for API endpoints
- [x] Run `zig fmt .`
## Phase 4: Static File Serving
- [ ] Implement static file serving:
- [ ] Serve `index.html` at `/`
- [ ] Serve static assets (JS, CSS)
- [ ] Handle SPA routing (all paths → index.html)
- [ ] Add `--port` CLI argument
- [ ] Run `zig fmt .`
## Phase 4: Static File Serving ✅ COMPLETE
- [x] Implement static file serving:
- [x] Serve `index.html` at `/`
- [x] Serve static assets (placeholder 404 handler)
- [x] Handle SPA routing (all non-API paths ready)
- [x] Add `--port` CLI argument
- [x] Run `zig fmt .`
## Phase 5: Frontend Development
- [ ] Design minimal UI (list threads, view messages, search)

View file

@ -41,12 +41,17 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
const git_rev = b.run(&.{ "git", "describe", "--always", "--dirty=*" });
const options = b.addOptions();
options.addOption([]const u8, "git_revision", git_rev);
const exe_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_module.addImport("httpz", httpz.module("httpz"));
exe_module.addImport("build_options", options.createModule());
const exe = b.addExecutable(.{
.name = "zetviel",

View file

@ -2,11 +2,54 @@ const std = @import("std");
const httpz = @import("httpz");
const root = @import("root.zig");
const version = @import("build_options").git_revision;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Parse CLI arguments
var port: u16 = 5000;
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
_ = args.skip(); // skip program name
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
std.debug.print(
\\Zetviel - Email client for notmuch
\\
\\Usage: zetviel [OPTIONS]
\\
\\Options:
\\ --port <PORT> Port to listen on (default: 5000)
\\ --help, -h Show this help message
\\ --version, -v Show version information
\\
\\Environment:
\\ NOTMUCH_PATH Path to notmuch database (default: mail)
\\
, .{});
std.process.exit(0);
} else if (std.mem.eql(u8, arg, "--version") or std.mem.eql(u8, arg, "-v")) {
std.debug.print("Zetviel {s}\n", .{version});
std.process.exit(0);
} else if (std.mem.eql(u8, arg, "--port")) {
const port_str = args.next() orelse {
std.debug.print("Error: --port requires a value\n", .{});
std.process.exit(1);
};
port = std.fmt.parseInt(u16, port_str, 10) catch {
std.debug.print("Error: invalid port number\n", .{});
std.process.exit(1);
};
} else {
std.debug.print("Error: unknown argument '{s}'\n", .{arg});
std.debug.print("Use --help for usage information\n", .{});
std.process.exit(1);
}
}
// Get notmuch database path from environment or use default
const db_path = std.posix.getenv("NOTMUCH_PATH") orelse "mail";
@ -14,12 +57,12 @@ pub fn main() !void {
var db = try root.openNotmuchDb(allocator, db_path, null);
defer db.close();
std.debug.print("Zetviel starting on http://localhost:5000\n", .{});
std.debug.print("Zetviel starting on http://localhost:{d}\n", .{port});
std.debug.print("Notmuch database: {s}\n", .{db.path});
// Create HTTP server
var server = try httpz.Server(*root.NotmuchDb).init(allocator, .{
.port = 5000,
.port = port,
.address = "127.0.0.1",
}, &db);
defer server.deinit();
@ -33,11 +76,36 @@ pub fn main() !void {
router.get("/api/message/:message_id", messageHandler, .{});
router.get("/api/attachment/:message_id/:num", attachmentHandler, .{});
// TODO: Static file serving for frontend
// Static file serving
router.get("/", indexHandler, .{});
router.get("/*", staticHandler, .{});
try server.listen();
}
fn indexHandler(db: *root.NotmuchDb, _: *httpz.Request, res: *httpz.Response) !void {
const file = std.fs.cwd().openFile("static/index.html", .{}) catch {
res.status = 500;
res.body = "Error loading index.html";
return;
};
defer file.close();
const content = file.readToEndAlloc(db.allocator, 1024 * 1024) catch {
res.status = 500;
res.body = "Error reading index.html";
return;
};
res.header("Content-Type", "text/html");
res.body = content;
}
fn staticHandler(_: *root.NotmuchDb, _: *httpz.Request, res: *httpz.Response) !void {
res.status = 404;
res.body = "Not Found";
}
const SecurityHeaders = struct {
pub fn execute(_: *SecurityHeaders, req: *httpz.Request, res: *httpz.Response, executor: anytype) !void {
res.header("X-Frame-Options", "deny");