← Back to task

Commit 1d13b506

commit 1d13b5065dd6053194386c62a97be05a7761af2a
Author: Coder Agent <coder@agents.omni>
Date:   Mon Feb 16 20:01:46 2026

    Centralize web routing under Omni.Web
    
    Use Omni.Web as the single Warp router for /tasks, /news, and /files.
    
    Task-Id: t-615

diff --git a/Omni/Ava/Core.hs b/Omni/Ava/Core.hs
index 0704e259..6c7b8e32 100755
--- a/Omni/Ava/Core.hs
+++ b/Omni/Ava/Core.hs
@@ -38,9 +38,9 @@ import qualified Data.Time as Time
 import qualified Omni.Agent.AuditLog as AuditLog
 import qualified Omni.Agent.Memory as Memory
 import qualified Omni.Ava.Telegram.Bot as Telegram
-import qualified Omni.Ava.Web as Web
 import qualified Omni.Cli as Cli
 import qualified Omni.Test as Test
+import qualified Omni.Web as Web
 import qualified System.Directory as Dir
 import qualified System.Environment as Environment
 import qualified System.IO as IO
diff --git a/Omni/Ava/Web.hs b/Omni/Ava/Web.hs
index a9aada05..7096e682 100644
--- a/Omni/Ava/Web.hs
+++ b/Omni/Ava/Web.hs
@@ -25,6 +25,7 @@
 -- : dep http-conduit
 module Omni.Ava.Web
   ( startWebServer,
+    initWebDb,
     app,
     defaultPort,
     main,
@@ -59,12 +60,16 @@ main = putText "Use Omni.Ava for the main entry point"
 defaultPort :: Int
 defaultPort = 8079
 
-startWebServer :: Int -> FilePath -> IO ()
-startWebServer port dbPath = do
-  putText <| "Starting Ava web server on port " <> tshow port
+initWebDb :: FilePath -> IO ()
+initWebDb dbPath =
   SQL.withConnection dbPath <| \conn -> do
     Trace.initTraceDb conn
     Agent.initAgentDb conn
+
+startWebServer :: Int -> FilePath -> IO ()
+startWebServer port dbPath = do
+  putText <| "Starting Ava web server on port " <> tshow port
+  initWebDb dbPath
   Warp.run port (app dbPath)
 
 -- -----------------------------------------------------------------------------
diff --git a/Omni/Dev/Beryllium/Ava.nix b/Omni/Dev/Beryllium/Ava.nix
index 4519e981..0fe2b4cd 100644
--- a/Omni/Dev/Beryllium/Ava.nix
+++ b/Omni/Dev/Beryllium/Ava.nix
@@ -53,7 +53,7 @@
 
   # Note: Tailscale Funnel for beryllium ingress is configured via:
   #   tailscale funnel --bg 80
-  # Caddy then routes /files, /news, and /tasks (via ava on 8079).
+  # Caddy then proxies all requests to Omni.Web on 8079.
   # This persists in tailscaled config and doesn't need a systemd service.
   # URL: https://beryllium.oryx-ide.ts.net/
 }
diff --git a/Omni/Dev/Beryllium/Caddy.nix b/Omni/Dev/Beryllium/Caddy.nix
index 41f7bd7c..8aa3a370 100644
--- a/Omni/Dev/Beryllium/Caddy.nix
+++ b/Omni/Dev/Beryllium/Caddy.nix
@@ -1,31 +1,16 @@
-# Unified ingress for beryllium services.
+# Unified ingress for beryllium web.
 #
-# Caddy listens on port 80 and routes by path prefix:
-#   /news/  → newsreader (8071)
-#   /files/ → serve (8070)
-#   all else (including /tasks) → ava web (8079)
-#
-# /news and /files run with --base-path so generated HTML links
-# include their prefixes. Caddy strips those prefixes before proxying.
+# Caddy stays thin and proxies everything to Omni.Web on port 8079.
+# Omni.Web performs all app routing (/tasks, /news, /files).
 {...}: {
   services.caddy = {
     enable = true;
     globalConfig = ''
       admin localhost:2019
     '';
-    # Single-host config: match all requests, route by path
+    # Single-host config: proxy all traffic to Omni.Web
     virtualHosts.":80" = {
       extraConfig = ''
-        # Redirect /news and /files (no slash) to /news/ and /files/
-        redir /news /news/ 308
-        redir /files /files/ 308
-
-        handle_path /news/* {
-          reverse_proxy localhost:8071
-        }
-        handle_path /files/* {
-          reverse_proxy localhost:8070
-        }
         handle {
           reverse_proxy localhost:8079
         }
diff --git a/Omni/Serve.hs b/Omni/Serve.hs
index 077a92d1..6c7297d8 100755
--- a/Omni/Serve.hs
+++ b/Omni/Serve.hs
@@ -26,6 +26,7 @@
 -- : dep time
 module Omni.Serve
   ( main,
+    app,
     test,
   )
 where
diff --git a/Omni/Web.hs b/Omni/Web.hs
new file mode 100644
index 00000000..476c37ab
--- /dev/null
+++ b/Omni/Web.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Unified web server for beryllium.
+--
+-- Routes all web surfaces in a single Warp process:
+--   - /tasks and Ava control plane via 'Omni.Ava.Web'
+--   - /news via 'Omni.Newsreader.Web'
+--   - /files via 'Omni.Serve'
+--
+-- This keeps routing logic centralized under Omni/Web.
+--
+-- : dep bytestring
+-- : dep sqlite-simple
+-- : dep wai
+-- : dep warp
+module Omni.Web
+  ( startWebServer,
+    defaultPort,
+    app,
+  )
+where
+
+import Alpha
+import qualified Data.ByteString as ByteString
+import qualified Data.Text.Encoding as TextEncoding
+import qualified Database.SQLite.Simple as SQL
+import qualified Network.Wai as Wai
+import qualified Network.Wai.Handler.Warp as Warp
+import qualified Omni.Ava.Web as AvaWeb
+import qualified Omni.Newsreader.Db as NewsDb
+import qualified Omni.Newsreader.Web as NewsWeb
+import qualified Omni.Serve as Serve
+import qualified System.Environment as Env
+
+defaultPort :: Int
+defaultPort = AvaWeb.defaultPort
+
+startWebServer :: Int -> FilePath -> IO ()
+startWebServer port avaDbPath = do
+  putText <| "Starting unified Omni.Web server on port " <> tshow port
+  AvaWeb.initWebDb avaDbPath
+  NewsDb.initDb
+  newsDbPath <- NewsDb.getDbPath
+  filesRoot <- Env.lookupEnv "SERVE_ROOT" /> fromMaybe "/home/ben/ava"
+  SQL.withConnection newsDbPath <| \newsConn ->
+    Warp.run port (app avaDbPath newsConn filesRoot)
+
+app :: FilePath -> SQL.Connection -> FilePath -> Wai.Application
+app avaDbPath newsConn filesRoot req respond =
+  case Wai.pathInfo req of
+    "news" : _ ->
+      NewsWeb.app "/news" newsConn (stripPrefixRequest "news" req) respond
+    "files" : _ ->
+      Serve.app "/files" filesRoot (stripPrefixRequest "files" req) respond
+    _ ->
+      AvaWeb.app avaDbPath req respond
+
+stripPrefixRequest :: Text -> Wai.Request -> Wai.Request
+stripPrefixRequest prefix req =
+  let strippedPath = case Wai.pathInfo req of
+        _ : rest -> rest
+        [] -> []
+      raw = Wai.rawPathInfo req
+      prefixBytes = "/" <> TextEncoding.encodeUtf8 prefix
+      strippedRaw
+        | raw == prefixBytes = "/"
+        | prefixBytes `ByteString.isPrefixOf` raw =
+            let rest = ByteString.drop (ByteString.length prefixBytes) raw
+             in if ByteString.null rest then "/" else rest
+        | otherwise = raw
+   in req {Wai.pathInfo = strippedPath, Wai.rawPathInfo = strippedRaw}