changeset 231:09a96dcb2b4c hg-web

[merge] Join existing hg-web branch head
author MrJuneJune <me@mrjunejune.com>
date Sun, 02 Aug 2026 16:50:48 -0700
parents 7ef4c9d2a72d (current diff) 2de49ee4fdd6 (diff)
children 7edad9623498
files hg-web/deploy.sh medi/BUILD medi/README.md medi/main.c medi/src/app.tsx medi/src/index.html medi/src/main.tsx media_processor/BUILD media_processor/README.md media_processor/media_processor.c media_processor/media_processor.h media_processor/media_processor_test.c third_party/README.md third_party/ffmpeg/BUILD third_party/ffmpeg/README.md third_party/ffmpeg/ffmpeg.h third_party/ffmpeg/ffmpeg_cli.c third_party/ffmpeg/ffmpeg_cli.h third_party/ffmpeg/ffmpeg_wrapper.c
diffstat 137 files changed, 11250 insertions(+), 3840 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.env.development	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,2 @@
+AWS_MRJUNEJUNE_ACCESS_KEY=FILL
+AWS_MRJUNEJUNE_SECRET_ACCESS_KEY=FILL
--- a/.hgignore	Sun Jan 25 10:44:04 2026 -0800
+++ b/.hgignore	Sun Aug 02 16:50:48 2026 -0700
@@ -40,3 +40,10 @@
 
 # OS related
 .DS_Store
+
+# Environment var.
+.env
+# Server config
+.config
+# DB
+mrjunejune.db
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AGENT.md	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,21 @@
+# Agent instructions
+
+## Bazel is the project interface
+
+All project execution must happen through Bazel. Use existing `bazel build`,
+`bazel test`, and `bazel run` targets instead of invoking compiled binaries,
+language runtimes, package managers, generated scripts, or multiple services
+manually.
+
+When a workflow needs several processes or commands, add a Bazel target that
+owns their startup, runfiles, supervision, shutdown, and exit status. The user
+should need one `bazel run` command.
+
+For hg-web development, run the complete local stack with:
+
+```bash
+bazel run //hg-web:dev
+```
+
+Add or update the smallest relevant Bazel target whenever code cannot be built,
+tested, run, bundled, or orchestrated through Bazel.
--- a/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ b/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,7 @@
+exports_files([".env"])
+
+filegroup(
+  name = "env_file",
+  srcs = [".env"],
+  visibility = ["//visibility:public"],
+)
--- a/MODULE.bazel	Sun Jan 25 10:44:04 2026 -0800
+++ b/MODULE.bazel	Sun Aug 02 16:50:48 2026 -0700
@@ -2,6 +2,8 @@
 bazel_dep(name = "platforms", version = "1.0.0")
 bazel_dep(name = "bazel_skylib", version = "1.8.2")
 bazel_dep(name = "rules_shell", version = "0.6.1")
+bazel_dep(name = "aspect_rules_js", version = "2.9.2", dev_dependency = True)
+bazel_dep(name = "rules_nodejs", version = "6.7.3", dev_dependency = True)
 bazel_dep(name = "openssl", version = "3.3.1.bcr.7")
 bazel_dep(name = "rules_foreign_cc", version = "0.14.0")
 bazel_dep(name = "buildifier_prebuilt", version = "7.1.2", dev_dependency = True)
@@ -13,6 +15,36 @@
 )
 
 http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
+http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+  name = "playwright_chromium_linux",
+  urls = ["https://playwright.azureedge.net/builds/chromium/1140/chromium-linux.zip"],
+  sha256 = "e78cda52fa7e847abcd36a48ffabda43e71e2220a3e9a398f7c9a179529570a5",
+  strip_prefix = "chrome-linux",
+  build_file_content = """
+exports_files(["chrome"])
+
+filegroup(
+    name = "chromium",
+    srcs = glob(["**"], exclude = ["BUILD.bazel"]),
+    visibility = ["//visibility:public"],
+)
+""",
+)
+
+node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
+node.toolchain(node_version = "20.18.0")
+use_repo(node, "nodejs")
+
+npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
+npm.npm_translate_lock(
+  name = "hg_web_npm",
+  data = ["//hg-web/e2e:package.json"],
+  npm_package_lock = "//hg-web/e2e:package-lock.json",
+  pnpm_lock = "//hg-web/e2e:pnpm-lock.yaml",
+)
+use_repo(npm, "hg_web_npm")
 
 # Bun
 http_file(
--- a/MODULE.bazel.lock	Sun Jan 25 10:44:04 2026 -0800
+++ b/MODULE.bazel.lock	Sun Aug 02 16:50:48 2026 -0700
@@ -1,5 +1,5 @@
 {
-  "lockFileVersion": 26,
+  "lockFileVersion": 28,
   "registryFileHashes": {
     "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
     "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
@@ -43,8 +43,9 @@
     "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
     "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
     "https://bcr.bazel.build/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d",
-    "https://bcr.bazel.build/modules/bazel_features/1.39.0/source.json": "f63cbeb4c602098484d57001e5a07d31cb02bbccde9b5e2c9bf0b29d05283e93",
     "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
+    "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080",
+    "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309",
     "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
     "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
     "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d",
@@ -65,8 +66,8 @@
     "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
     "https://bcr.bazel.build/modules/buildifier_prebuilt/7.1.2/MODULE.bazel": "d18b017dddf219626ea5b04028ff0db2397655fbdaa9d9258a6443ddafb303ab",
     "https://bcr.bazel.build/modules/buildifier_prebuilt/7.1.2/source.json": "5fb3a2433f6508f4b23934eaa2986d604ae65aff95d7476d1273bf5cd434eedc",
-    "https://bcr.bazel.build/modules/buildozer/8.2.1/MODULE.bazel": "61e9433c574c2bd9519cad7fa66b9c1d2b8e8d5f3ae5d6528a2c2d26e68d874d",
-    "https://bcr.bazel.build/modules/buildozer/8.2.1/source.json": "7c33f6a26ee0216f85544b4bca5e9044579e0219b6898dd653f5fb449cf2e484",
+    "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f",
+    "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd",
     "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
     "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
     "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
@@ -135,7 +136,8 @@
     "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37",
     "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
     "https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4",
-    "https://bcr.bazel.build/modules/rules_cc/0.2.16/source.json": "d03d5cde49376d87e14ec14b666c56075e5e3926930327fd5d0484a1ff2ac1cc",
+    "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84",
+    "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07",
     "https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc",
     "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
     "https://bcr.bazel.build/modules/rules_foreign_cc/0.14.0/MODULE.bazel": "56fb9a239503bab4183d06ba6cabb01cd73aae296ab499085b9193624a8a66e2",
@@ -153,8 +155,8 @@
     "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
     "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
     "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
-    "https://bcr.bazel.build/modules/rules_java/9.0.3/MODULE.bazel": "1f98ed015f7e744a745e0df6e898a7c5e83562d6b759dfd475c76456dda5ccea",
-    "https://bcr.bazel.build/modules/rules_java/9.0.3/source.json": "b038c0c07e12e658135bbc32cc1a2ded6e33785105c9d41958014c592de4593e",
+    "https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d",
+    "https://bcr.bazel.build/modules/rules_java/9.1.0/source.json": "da589573c1dee2c9ac4a568b301269a2e8191110ff0345c1a959fa7ea6c4dfd6",
     "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
     "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
     "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
@@ -228,9 +230,105 @@
   },
   "selectedYankedVersions": {},
   "moduleExtensions": {
+    "@@aspect_rules_js+//npm:extensions.bzl%pnpm": {
+      "general": {
+        "bzlTransitiveDigest": "A3wObJgLD5IcmyBKFyx9nfycX4VosrUq4UtGAHdyZ8M=",
+        "usagesDigest": "UCPGCdTkoflpzf0ShZ9hsk2zN6bbQlRvpRRZJnniDDc=",
+        "recordedInputs": [
+          "REPO_MAPPING:aspect_bazel_lib+,bazel_skylib bazel_skylib+",
+          "REPO_MAPPING:aspect_bazel_lib+,bazel_tools bazel_tools",
+          "REPO_MAPPING:aspect_bazel_lib+,tar.bzl tar.bzl+",
+          "REPO_MAPPING:aspect_rules_js+,aspect_bazel_lib aspect_bazel_lib+",
+          "REPO_MAPPING:aspect_rules_js+,aspect_rules_js aspect_rules_js+",
+          "REPO_MAPPING:aspect_rules_js+,aspect_tools_telemetry_report aspect_tools_telemetry++telemetry+aspect_tools_telemetry_report",
+          "REPO_MAPPING:aspect_rules_js+,bazel_features bazel_features+",
+          "REPO_MAPPING:aspect_rules_js+,bazel_lib bazel_lib+",
+          "REPO_MAPPING:aspect_rules_js+,bazel_skylib bazel_skylib+",
+          "REPO_MAPPING:aspect_rules_js+,bazel_tools bazel_tools",
+          "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals",
+          "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version",
+          "REPO_MAPPING:bazel_lib+,bazel_tools bazel_tools",
+          "REPO_MAPPING:tar.bzl+,aspect_bazel_lib aspect_bazel_lib+",
+          "REPO_MAPPING:tar.bzl+,bazel_skylib bazel_skylib+",
+          "REPO_MAPPING:tar.bzl+,tar.bzl tar.bzl+"
+        ],
+        "generatedRepoSpecs": {
+          "pnpm": {
+            "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_rule",
+            "attributes": {
+              "package": "pnpm",
+              "version": "8.15.9",
+              "root_package": "",
+              "link_workspace": "",
+              "link_packages": {},
+              "integrity": "sha512-SZQ0ydj90aJ5Tr9FUrOyXApjOrzuW7Fee13pDzL0e1E6ypjNXP0AHDHw20VLw4BO3M1XhQHkyik6aBYWa72fgQ==",
+              "url": "",
+              "commit": "",
+              "patch_args": [
+                "-p0"
+              ],
+              "patches": [],
+              "custom_postinstall": "",
+              "npm_auth": "",
+              "npm_auth_basic": "",
+              "npm_auth_username": "",
+              "npm_auth_password": "",
+              "lifecycle_hooks": [],
+              "extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])",
+              "extract_full_archive": true,
+              "exclude_package_contents": []
+            }
+          },
+          "pnpm__links": {
+            "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_links",
+            "attributes": {
+              "package": "pnpm",
+              "version": "8.15.9",
+              "dev": false,
+              "root_package": "",
+              "link_packages": {},
+              "deps": {},
+              "transitive_closure": {},
+              "lifecycle_build_target": false,
+              "lifecycle_hooks_env": [],
+              "lifecycle_hooks_execution_requirements": [
+                "no-sandbox"
+              ],
+              "lifecycle_hooks_use_default_shell_env": false,
+              "bins": {},
+              "package_visibility": [
+                "//visibility:public"
+              ],
+              "exclude_package_contents": []
+            }
+          }
+        }
+      }
+    },
+    "@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
+      "general": {
+        "bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=",
+        "usagesDigest": "TQSuPERI87Z4Alo1eOWeUR3NGo5f3txcCOd4tpsGXmw=",
+        "recordedInputs": [
+          "REPO_MAPPING:aspect_tools_telemetry+,bazel_lib bazel_lib+",
+          "REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+"
+        ],
+        "generatedRepoSpecs": {
+          "aspect_tools_telemetry_report": {
+            "repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository",
+            "attributes": {
+              "deps": {
+                "aspect_rules_js": "2.9.2",
+                "aspect_tools_telemetry": "0.3.3"
+              }
+            }
+          }
+        }
+      }
+    },
     "@@buildifier_prebuilt+//:defs.bzl%buildifier_prebuilt_deps_extension": {
       "general": {
-        "bzlTransitiveDigest": "27R0vlu7+T5uJVRkrJWwjP+5K4Pi5H1nJbI3JBLJyKI=",
+        "bzlTransitiveDigest": "HljGbKDagP11Zj2xu7uXH4csvPiytXXlNpFDe1PeuTs=",
         "usagesDigest": "BWWW2nG29mUymMGpRgfP+nxndKWaf1fCZZ33EDPaVIc=",
         "recordedInputs": [
           "REPO_MAPPING:buildifier_prebuilt+,bazel_skylib bazel_skylib+",
@@ -374,7 +472,7 @@
     },
     "@@emsdk+//:emscripten_deps.bzl%emscripten_deps": {
       "general": {
-        "bzlTransitiveDigest": "QDGe6PSXCStGFYJd3Yj2kvRMiXLid2u8mNHBRjJoOEw=",
+        "bzlTransitiveDigest": "+1O9FmrwgJwK/ywCKhwouNuu7NWC/hfkkzjuGp/1c9Q=",
         "usagesDigest": "f1E1WkCSGt50uwBJp/heJk2R/iAm39ixgGS93wpdywA=",
         "recordedInputs": [
           "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals",
@@ -444,9 +542,31 @@
         }
       }
     },
+    "@@pybind11_bazel+//:internal_configure.bzl%internal_configure_extension": {
+      "general": {
+        "bzlTransitiveDigest": "NRXra7941UfmNUyIxnLt82V5hULluVGL2nBsijTl4j4=",
+        "usagesDigest": "D1r3lfzMuUBFxgG8V6o0bQTLMk3GkaGOaPzw53wrwyw=",
+        "recordedInputs": [
+          "REPO_MAPPING:pybind11_bazel+,bazel_tools bazel_tools",
+          "FILE:@@pybind11_bazel+//MODULE.bazel e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34"
+        ],
+        "generatedRepoSpecs": {
+          "pybind11": {
+            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+            "attributes": {
+              "build_file": "@@pybind11_bazel+//:pybind11-BUILD.bazel",
+              "strip_prefix": "pybind11-2.12.0",
+              "urls": [
+                "https://github.com/pybind/pybind11/archive/v2.12.0.zip"
+              ]
+            }
+          }
+        }
+      }
+    },
     "@@rules_foreign_cc+//foreign_cc:extensions.bzl%tools": {
       "general": {
-        "bzlTransitiveDigest": "dWWiLQWKST2+NU4SkbVS2OhS6KgYdZ4jBI5MBsDEBt0=",
+        "bzlTransitiveDigest": "G54TxUUn6vxT4n5eIpNwbycPtXtiR354MSgHTcQAnHI=",
         "usagesDigest": "Eyh4mAOi6L+Nn/lY/wQBJclQrmBnWdQM+B4lZeq6azA=",
         "recordedInputs": [
           "REPO_MAPPING:rules_foreign_cc+,bazel_tools bazel_tools",
@@ -819,7 +939,7 @@
     },
     "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
       "general": {
-        "bzlTransitiveDigest": "ABI1D/sbS1ovwaW/kHDoj8nnXjQ0oKU9fzmzEG4iT8o=",
+        "bzlTransitiveDigest": "+Kp6j204mBZ3mxlIDDR0gBoP45BZ4jYRhRAcB8sU0qc=",
         "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=",
         "recordedInputs": [
           "REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools"
@@ -877,7 +997,7 @@
     "@@rules_nodejs+//nodejs:extensions.bzl%node": {
       "general": {
         "bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=",
-        "usagesDigest": "1PdljUSmnJF0C3nTMWMdndAlDqMjOzQHlOp2TAszTxk=",
+        "usagesDigest": "aOq2mNhv/K35rllf0LkQnk575K/CZNf1RTfrNe1kDkY=",
         "recordedInputs": [],
         "generatedRepoSpecs": {
           "nodejs_linux_amd64": {
@@ -1007,7 +1127,7 @@
     },
     "@@rules_python+//python/extensions:config.bzl%config": {
       "general": {
-        "bzlTransitiveDigest": "2hLgIvNVTLgxus0ZuXtleBe70intCfo0cHs8qvt6cdM=",
+        "bzlTransitiveDigest": "dzD8Q2YmrP3fz8saWLHPmlwPLO91ImtTmP/c9JKTStM=",
         "usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=",
         "recordedInputs": [
           "REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
@@ -1275,5 +1395,6 @@
       }
     }
   },
-  "facts": {}
+  "facts": {},
+  "factsVersions": {}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/assets/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,5 @@
+filegroup(
+  name = "icons",
+  srcs = glob(["icons/*.png"]),
+  visibility = ["//visibility:public"],
+)
Binary file assets/cabinet.png has changed
Binary file assets/icons/binder.png has changed
Binary file assets/icons/bold.png has changed
Binary file assets/icons/book.png has changed
Binary file assets/icons/cabinet.png has changed
Binary file assets/icons/clipy.png has changed
Binary file assets/icons/h1.png has changed
Binary file assets/icons/h2.png has changed
Binary file assets/icons/h3.png has changed
Binary file assets/icons/link.png has changed
Binary file assets/icons/open-book.png has changed
--- a/benchmark/bun-http-framework-benchmark/results/bun/bun-web-standard.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/bun-web-standard.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    228408.02,228408.02
+Reqs/sec    228466.99,228466.99
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    153303.58,153303.58
+Reqs/sec    152460.20,152460.20
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    151110.26,151110.26
+Reqs/sec    156344.05,156344.05
--- a/benchmark/bun-http-framework-benchmark/results/bun/bun.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/bun.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    254858.08,254858.08
+Reqs/sec    258730.63,258730.63
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    205117.00,205117.00
+Reqs/sec    208220.35,208220.35
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    156658.25,156658.25
+Reqs/sec    158675.80,158675.80
--- a/benchmark/bun-http-framework-benchmark/results/bun/elysia.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/elysia.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    400960.78,400960.78
+Reqs/sec    394707.12,394707.12
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    200374.15,200374.15
+Reqs/sec    196336.94,196336.94
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    164129.36,164129.36
+Reqs/sec    161899.39,161899.39
--- a/benchmark/bun-http-framework-benchmark/results/bun/express.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/express.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec     94358.98,94358.98
+Reqs/sec     89438.11,89438.11
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     83982.98,83982.98
+Reqs/sec     80241.22,80241.22
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     84024.83,84024.83
+Reqs/sec     83023.74,83023.74
--- a/benchmark/bun-http-framework-benchmark/results/bun/h3.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/h3.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec     98530.52,98530.52
+Reqs/sec     96603.44,96603.44
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     81602.75,81602.75
+Reqs/sec     79062.78,79062.78
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec      9829.93,9829.93
+Reqs/sec      9958.75,9958.75
--- a/benchmark/bun-http-framework-benchmark/results/bun/hono.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/hono.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    202727.16,202727.16
+Reqs/sec    197638.70,197638.70
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    147760.89,147760.89
+Reqs/sec    146857.99,146857.99
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    130184.98,130184.98
+Reqs/sec    129630.67,129630.67
--- a/benchmark/bun-http-framework-benchmark/results/bun/nbit.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/nbit.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    166352.26,166352.26
+Reqs/sec    161889.19,161889.19
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    130437.58,130437.58
+Reqs/sec    128469.38,128469.38
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    109477.61,109477.61
+Reqs/sec    107344.24,107344.24
--- a/benchmark/bun-http-framework-benchmark/results/bun/nhttp.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/nhttp.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    260256.20,260256.20
+Reqs/sec    251208.25,251208.25
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    155733.48,155733.48
+Reqs/sec    146262.76,146262.76
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    164285.02,164285.02
+Reqs/sec    150412.21,150412.21
--- a/benchmark/bun-http-framework-benchmark/results/bun/oak.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/oak.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec     66974.27,66974.27
+Reqs/sec     69435.13,69435.13
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     55255.71,55255.71
+Reqs/sec     55855.49,55855.49
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     49373.25,49373.25
+Reqs/sec     48430.51,48430.51
--- a/benchmark/bun-http-framework-benchmark/results/bun/wobe.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/bun/wobe.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    177452.13,177452.13
+Reqs/sec    164622.23,164622.23
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    159185.52,159185.52
+Reqs/sec    140543.28,140543.28
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    133618.73,133618.73
+Reqs/sec    125350.31,125350.31
--- a/benchmark/bun-http-framework-benchmark/results/c/seobeo.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/c/seobeo.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec     35694.89,35694.89
+Reqs/sec    762500.23,762500.23
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     35792.17,35792.17
+Reqs/sec    664161.76,664161.76
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     35844.77,35844.77
+Reqs/sec    680603.95,680603.95
--- a/benchmark/bun-http-framework-benchmark/results/node/express.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/express.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec     16803.98,16803.98
+Reqs/sec     16270.25,16270.25
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     15956.22,15956.22
+Reqs/sec     15420.94,15420.94
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     17518.46,17518.46
+Reqs/sec     17296.20,17296.20
--- a/benchmark/bun-http-framework-benchmark/results/node/fastify.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/fastify.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    114069.22,114069.22
+Reqs/sec    108144.21,108144.21
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    106418.14,106418.14
+Reqs/sec    102523.21,102523.21
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     32150.27,32150.27
+Reqs/sec     31702.05,31702.05
--- a/benchmark/bun-http-framework-benchmark/results/node/h3.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/h3.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    102794.32,102794.32
+Reqs/sec     98213.55,98213.55
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     86779.18,86779.18
+Reqs/sec     83364.88,83364.88
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     79135.52,79135.52
+Reqs/sec     77675.20,77675.20
--- a/benchmark/bun-http-framework-benchmark/results/node/hono.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/hono.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    102406.51,102406.51
+Reqs/sec    101150.50,101150.50
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     91815.44,91815.44
+Reqs/sec     89469.69,89469.69
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     23631.62,23631.62
+Reqs/sec     22884.58,22884.58
--- a/benchmark/bun-http-framework-benchmark/results/node/hyper-express.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/hyper-express.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    270288.86,270288.86
+Reqs/sec    261221.09,261221.09
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    207667.76,207667.76
+Reqs/sec    202783.27,202783.27
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    145951.54,145951.54
+Reqs/sec    139488.27,139488.27
--- a/benchmark/bun-http-framework-benchmark/results/node/koa.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/koa.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec     57452.05,57452.05
+Reqs/sec     52681.21,52681.21
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec     48624.37,48624.37
+Reqs/sec     46472.31,46472.31
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec     35546.79,35546.79
+Reqs/sec     34781.23,34781.23
--- a/benchmark/bun-http-framework-benchmark/results/node/ultimate-express.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/ultimate-express.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    398161.89,398161.89
+Reqs/sec    390978.83,390978.83
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    377012.33,377012.33
+Reqs/sec    371893.76,371893.76
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    134859.38,134859.38
+Reqs/sec    131597.36,131597.36
--- a/benchmark/bun-http-framework-benchmark/results/node/uws.txt	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/node/uws.txt	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,6 @@
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/
-Reqs/sec    405159.40,405159.40
+Reqs/sec    395545.79,395545.79
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s http://127.0.0.1:3000/id/1?name=bun
-Reqs/sec    376621.69,376621.69
+Reqs/sec    372478.96,372478.96
 /home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d 10s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json
-Reqs/sec    216381.92,216381.92
+Reqs/sec    211797.72,211797.72
--- a/benchmark/bun-http-framework-benchmark/results/results.md	Sun Jan 25 10:44:04 2026 -0800
+++ b/benchmark/bun-http-framework-benchmark/results/results.md	Sun Aug 02 16:50:48 2026 -0700
@@ -1,22 +1,22 @@
 
 |  Framework       | Runtime | Average | Ping       | Query      | Body       |
 | ---------------- | ------- | ------- | ---------- | ---------- | ---------- |
-| uws | node | 332,721.003 | 405,159.4 | 376,621.69 | 216,381.92 |
-| ultimate-express | node | 303,344.533 | 398,161.89 | 377,012.33 | 134,859.38 |
-| elysia | bun | 255,154.763 | 400,960.78 | 200,374.15 | 164,129.36 |
-| hyper-express | node | 207,969.387 | 270,288.86 | 207,667.76 | 145,951.54 |
-| bun | bun | 205,544.443 | 254,858.08 | 205,117 | 156,658.25 |
-| nhttp | bun | 193,424.9 | 260,256.2 | 155,733.48 | 164,285.02 |
-| bun-web-standard | bun | 177,607.287 | 228,408.02 | 153,303.58 | 151,110.26 |
-| hono | bun | 160,224.343 | 202,727.16 | 147,760.89 | 130,184.98 |
-| wobe | bun | 156,752.127 | 177,452.13 | 159,185.52 | 133,618.73 |
-| nbit | bun | 135,422.483 | 166,352.26 | 130,437.58 | 109,477.61 |
-| h3 | node | 89,569.673 | 102,794.32 | 86,779.18 | 79,135.52 |
-| express | bun | 87,455.597 | 94,358.98 | 83,982.98 | 84,024.83 |
-| fastify | node | 84,212.543 | 114,069.22 | 106,418.14 | 32,150.27 |
-| hono | node | 72,617.857 | 102,406.51 | 91,815.44 | 23,631.62 |
-| h3 | bun | 63,321.067 | 98,530.52 | 81,602.75 | 9,829.93 |
-| oak | bun | 57,201.077 | 66,974.27 | 55,255.71 | 49,373.25 |
-| koa | node | 47,207.737 | 57,452.05 | 48,624.37 | 35,546.79 |
-| seobeo | c | 35,777.277 | 35,694.89 | 35,792.17 | 35,844.77 |
-| express | node | 16,759.553 | 16,803.98 | 15,956.22 | 17,518.46 |
\ No newline at end of file
+| seobeo | c | 702,421.98 | 762,500.23 | 664,161.76 | 680,603.95 |
+| uws | node | 326,607.49 | 395,545.79 | 372,478.96 | 211,797.72 |
+| ultimate-express | node | 298,156.65 | 390,978.83 | 371,893.76 | 131,597.36 |
+| elysia | bun | 250,981.15 | 394,707.12 | 196,336.94 | 161,899.39 |
+| bun | bun | 208,542.26 | 258,730.63 | 208,220.35 | 158,675.8 |
+| hyper-express | node | 201,164.21 | 261,221.09 | 202,783.27 | 139,488.27 |
+| nhttp | bun | 182,627.74 | 251,208.25 | 146,262.76 | 150,412.21 |
+| bun-web-standard | bun | 179,090.413 | 228,466.99 | 152,460.2 | 156,344.05 |
+| hono | bun | 158,042.453 | 197,638.7 | 146,857.99 | 129,630.67 |
+| wobe | bun | 143,505.273 | 164,622.23 | 140,543.28 | 125,350.31 |
+| nbit | bun | 132,567.603 | 161,889.19 | 128,469.38 | 107,344.24 |
+| h3 | node | 86,417.877 | 98,213.55 | 83,364.88 | 77,675.2 |
+| express | bun | 84,234.357 | 89,438.11 | 80,241.22 | 83,023.74 |
+| fastify | node | 80,789.823 | 108,144.21 | 102,523.21 | 31,702.05 |
+| hono | node | 71,168.257 | 101,150.5 | 89,469.69 | 22,884.58 |
+| h3 | bun | 61,874.99 | 96,603.44 | 79,062.78 | 9,958.75 |
+| oak | bun | 57,907.043 | 69,435.13 | 55,855.49 | 48,430.51 |
+| koa | node | 44,644.917 | 52,681.21 | 46,472.31 | 34,781.23 |
+| express | node | 16,329.13 | 16,270.25 | 15,420.94 | 17,296.2 |
\ No newline at end of file
--- a/gui_ze/gui_ze.bzl	Sun Jan 25 10:44:04 2026 -0800
+++ b/gui_ze/gui_ze.bzl	Sun Aug 02 16:50:48 2026 -0700
@@ -132,7 +132,7 @@
 {copy_commands}
 export NODE_PATH=./third_party/bun/node_modules
 cp ./third_party/bun/tsconfig.json .
-{bun} build {entry} --outfile {output} --target browser
+{bun} build {entry} --outfile {output} --target browser 
 """.format(
       copy_commands = "\n".join(copy_commands),
       src_package = src_package,
--- a/hg-web/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -1,4 +1,5 @@
 load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
+load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
 load("//gui_ze:gui_ze.bzl", "move_files_into_dir", "bundle", "bun_bundle")
 
 # Source files
@@ -41,9 +42,15 @@
   dest = "src/public",
 )
 
+move_files_into_dir(
+  name = "public_fonts_files",
+  srcs = ["//mrjunejune:public_fonts_files"],
+  dest = "src/public/fonts",
+)
+
 filegroup(
   name = "all_assets",
-  srcs = glob(["src/**"]) + [":compiled_js", ":public_files"],
+  srcs = glob(["src/**"]) + [":compiled_js", ":public_files", ":public_fonts_files"],
 )
 
 # Server binaries
@@ -52,6 +59,7 @@
   srcs = ["main.c"],
   deps = ["//seobeo:seobeo"],
   data = [":all_assets"],
+  visibility = ["//hg-web:__subpackages__"],
 )
 
 cc_binary(
@@ -61,6 +69,20 @@
   data = [":all_assets"],
 )
 
+sh_binary(
+  name = "dev",
+  srcs = ["dev.sh"],
+  data = [
+    ":hg_web_server",
+    "@bazel_tools//tools/bash/runfiles",
+  ],
+)
+
+test_suite(
+  name = "tests",
+  tests = ["//hg-web/e2e:app_e2e_test"],
+)
+
 bundle(
   name = "hg_web_server_bundle",
   binary = ":hg_web_server",
--- a/hg-web/README.md	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/README.md	Sun Aug 02 16:50:48 2026 -0700
@@ -1,34 +1,286 @@
 # hg-web
 
-A web-based Mercurial repository browser. Provides a GitHub-style interface for browsing files, viewing code with syntax highlighting, and reading markdown documentation.
+A custom Mercurial forge and repository browser. The application keeps the
+networking and HTTP stack in C with Seobeo, while the hand-drawn web interface
+is implemented in React and TypeScript.
+
+## Wiki tree
 
-## Features
+```text
+hg-web
+├── Runtime
+│   ├── hg_web_server :6970
+│   │   ├── React application and static assets
+│   │   ├── repository browsing API
+│   │   ├── commit graph API
+│   │   └── Mercurial wire-protocol proxy
+│   └── hg serve :4444
+│       ├── repository files and graph data
+│       └── Mercurial pull/push protocol
+├── Backend
+│   ├── main.c
+│   └── ../seobeo
+│       ├── s_network.c
+│       ├── s_web.c
+│       ├── s_http_client.c
+│       └── s_ssl.c
+├── Frontend
+│   └── src
+│       ├── main.tsx
+│       ├── components
+│       │   ├── app.tsx
+│       │   ├── directory-browser.tsx
+│       │   ├── graph.tsx
+│       │   ├── header.tsx
+│       │   ├── footer.tsx
+│       │   ├── theme.tsx
+│       │   └── repo-browser.tsx
+│       ├── index.html
+│       ├── index.css
+│       ├── base.css
+│       └── custom pencil, panda, and icon assets
+├── Build
+│   ├── BUILD
+│   ├── ../gui_ze/gui_ze.bzl
+│   ├── ../markdown_converter
+│   └── ../third_party/highlight
+└── Operations
+    └── deploy.sh
+```
+
+`repo-browser.tsx` is an older standalone browser implementation. The active
+entry point is `main.tsx`, which renders `app.tsx`; `app.tsx` embeds
+`directory-browser.tsx`.
+
+## Runtime topology
 
-- Browse repository files and directories
-- View code files with syntax highlighting (highlight.js)
-- Render markdown files with WASM-based converter
-- Dark/light theme support with system preference detection
-- Prefetch on hover for faster navigation
+```text
+Browser
+  |
+  | HTTPS
+  v
+nginx
+  |
+  | HTTP :6970
+  v
+hg_web_server (main.c + Seobeo)
+  |
+  | HTTP :4444
+  v
+hg serve (Zenbu Mercurial repository)
+```
+
+The C server owns the public routes. It serves the application shell and
+assets, translates browser API requests into `hg serve` requests, and streams
+Mercurial wire-protocol traffic without routing bundle data through the normal
+buffered HTTP response path.
+
+## Request flows
+
+### Application shell
+
+```text
+GET /, /directories, or /graph
+  -> GetReactHome
+  -> hg-web/src/index.html
+  -> /page.js
+  -> src/main.tsx
+  -> components/app.tsx
+```
+
+### Repository browser
 
-## Structure
+```text
+directory-browser.tsx
+  -> GET /api/repo/list?path=...
+  -> ApiListDirectory
+  -> GET hg-serve/file/tip/<path>?style=json
 
+file or README selection
+  -> GET /api/repo/file?path=...
+  -> ApiGetFile
+  -> GET hg-serve/raw-file/tip/<path>
+  -> highlight.js, markdown_converter WASM, or inline static preview
 ```
-hg-web/
-├── BUILD           # Bazel build configuration
-├── deploy.sh       # Deployment script
-├── main.c          # C server handling API routes
-└── src/            # Frontend source files
+
+Images, SVG, video, audio, and PDF files open in the forge preview modal.
+Unknown binary files retain a download link. The raw file API supplies explicit
+MIME types, `nosniff`, inline disposition for supported previews, and sandboxed
+SVG responses.
+
+### Commit graph
+
+```text
+graph.tsx
+  -> GET /api/graph/<revision>?graphtop=...&style=json
+  -> ApiGetGraph
+  -> GET hg-serve/graph/<revision>?...
+  -> React rows + custom pencil/panda canvas
+
+graph row selection
+  -> /changeset/<revision>
+  -> GET /api/changeset/<revision>
+  -> ApiGetChangeset
+  -> GET hg-serve/json-rev/<revision>
+  -> changeset metadata + rendered diff
+```
+
+### Mercurial clone, pull, and push
+
+```text
+Mercurial client
+  -> GET or POST /repo
+  -> StreamHgWireProtocol
+  -> hg serve /?cmd=...
+  -> binary-safe streamed response
 ```
 
-## Building
+## Route map
+
+| Method | Route | Owner | Purpose |
+| --- | --- | --- | --- |
+| `GET` | `/` | `GetReactHome` | Application shell |
+| `GET` | `/directories` | `GetReactHome` | Legacy application-shell route |
+| `GET` | `/directory` | `GetReactHome` | Repository browser application route |
+| `GET` | `/graph` | `GetReactHome` | Commit graph application route |
+| `GET` | `/changeset/:changeset_id` | `GetReactHome` | Changeset application route |
+| `GET` | `/api/repo/list` | `ApiListDirectory` | Directory listing JSON |
+| `GET` | `/api/repo/file` | `ApiGetFile` | Raw tracked file |
+| `GET` | `/api/repo/readme` | `ApiGetReadme` | Directory README content |
+| `GET` | `/api/graph/:graph_id` | `ApiGetGraph` | Mercurial graph JSON |
+| `GET` | `/api/changeset/:changeset_id` | `ApiGetChangeset` | Changeset metadata and diff JSON |
+| `GET`, `POST` | `/repo` | `StreamHgWireProtocol` | Mercurial wire protocol |
+
+## Frontend ownership
+
+| File | Responsibility |
+| --- | --- |
+| `src/components/app.tsx` | Client-side routes, landing page, tabs, and history |
+| `src/components/directory-browser.tsx` | Breadcrumbs, listings, prefetch, file modals, README rendering |
+| `src/components/graph.tsx` | Graph fetching, pagination, canvas edges, panda nodes |
+| `src/components/theme.tsx` | Stored light/dark preference and system-theme integration |
+| `src/components/header.tsx` | Forge identity and theme control |
+| `src/components/footer.tsx` | Shared footer |
+| `src/index.css`, `src/base.css` | Custom visual language and layout |
+
+Keep the custom assets and visual language in this package. New forge screens
+should reuse the existing CSS variables, typography, textures, and components
+instead of introducing a generic design system.
+
+## Build and local run
+
+Build the server and deployable bundle from the repository root:
 
 ```bash
-bazel build //hg-web:hg_web
+bazel build //hg-web:hg_web_server
+bazel build //hg-web:hg_web_server_bundle
+bazel test //hg-web:tests
+bazel test //markdown_converter/tests:markdown_to_html_test
+bazel test //seobeo/tests:all
+```
+
+`//hg-web:tests` creates a temporary two-commit Mercurial repository and runs
+the application in pinned Chromium through Playwright. It covers shell routes,
+files and README rendering, graph and changeset navigation, browser history,
+console and page errors, API validation, Mercurial wire protocol, Markdown
+script escaping, static assets, and backend outages. The test invokes the
+workspace Mercurial CLI with an isolated configuration and home directory.
+
+Build and run the complete local stack with one Bazel command:
+
+```bash
+bazel run //hg-web:dev
+```
+
+This starts `hg serve` on `127.0.0.1:4444` and `hg_web_server` on port `6970`.
+Stopping the Bazel target stops both child processes.
+
+The bundle contains `hg_web_server` and `hg-web/src/`, including generated
+`page.js`, markdown WASM, highlight.js, styles, and image assets.
+
+## Deployment contract
+
+The expected production layout is:
+
+```text
+nginx
+  -> hg_web_server.service
+     -> /opt/hg_web_server_bundle_active/hg_web_server
+     -> working directory: /opt/hg_web_server_bundle_active
+  -> hg serve service on 127.0.0.1:4444
 ```
 
-## API Endpoints
+`deploy.sh` builds an optimized bundle into a revisioned release directory,
+atomically repoints `/opt/hg_web_server_bundle_active`, restarts
+`hg_web_server.service`, and checks `http://127.0.0.1:6970/`. A failed restart
+or health check restores the previous release and restarts it. The service,
+release root, active path, health URL, user, and group can be overridden with
+environment variables.
+
+## Forge capability tree
+
+```text
+Zenbu Forge
+├── Repository
+│   ├── File and directory browsing            available
+│   ├── Syntax highlighting                    available
+│   ├── README rendering                       available
+│   ├── Commit graph                           available
+│   ├── Changeset detail and diff              available
+│   ├── Branches, bookmarks, and tags          planned
+│   ├── File history and blame                 planned
+│   └── Search                                 planned
+├── Collaboration
+│   ├── Authentication and authorization       planned
+│   ├── Changeset review                       planned
+│   ├── Issues                                 planned
+│   └── Releases and artifacts                 planned
+└── Automation
+    ├── Mercurial incoming/changegroup hook    planned
+    ├── Durable SQLite job queue               planned
+    ├── Isolated Bazel runner                  planned
+    ├── Live logs and job status API           planned
+    ├── Forge status and log screens           planned
+    ├── Artifact retention                     planned
+    └── Atomic deploy, health check, rollback  available
+```
+
+## Automation data flow
 
-The C server (`main.c`) provides:
+The Actions-like subsystem should extend the existing server rather than
+replace it:
+
+```text
+hg push
+  -> Mercurial hook records repository + revision
+  -> small enqueue command writes a SQLite job
+  -> runner service claims one queued job
+  -> isolated shared checkout updates to the exact revision
+  -> Bazel build and test steps stream logs
+  -> job result and artifacts are recorded
+  -> hg-web status API exposes the result
+  -> custom React screens render runs, steps, logs, and artifacts
+  -> successful protected jobs may call atomic deployment
+```
 
-- `GET /api/repo/list?path=` - List directory contents
-- `GET /api/repo/file?path=` - Fetch file contents
+Start with one runner on the same host. Keep the hook fast, never execute build
+steps inside the Mercurial request, and make the queue durable before adding
+parallel or remote runners.
+
+## Safe extension order
+
+1. Add branches, bookmarks, tags, file history, blame, and search.
+2. Add the hook, SQLite queue, single runner, and retained logs.
+3. Add run/status/log pages using the existing custom UI.
+4. Add authentication and authorization before accepting public pushes or
+   user-defined automation.
+
+## Invariants
+
+- Keep public application code on Seobeo APIs.
+- Decode and validate every path before forwarding it to Mercurial.
+- Keep `/repo` binary-safe; bundle data can contain null bytes.
+- Do not buffer large wire-protocol responses through the regular HTTP client.
+- Keep backend route names synchronized with frontend fetch and navigation code.
+- Preserve accurate status codes, content lengths, and content types.
+- Keep secrets in service-owned environment files, not in source or job logs.
--- a/hg-web/deploy.sh	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/deploy.sh	Sun Aug 02 16:50:48 2026 -0700
@@ -1,15 +1,82 @@
-#!/bin/bash
-# sudo groupadd zenbu_team -- already added
-# sudo useradd -r -s /usr/sbin/nologin -G zenbu_team hg_web_server
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+SERVICE_NAME="${SERVICE_NAME:-hg_web_server.service}"
+RELEASE_ROOT="${RELEASE_ROOT:-/opt/hg_web_server_releases}"
+ACTIVE_PATH="${ACTIVE_PATH:-/opt/hg_web_server_bundle_active}"
+HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:6970/}"
+SERVICE_USER="${SERVICE_USER:-hg_web_server}"
+SERVICE_GROUP="${SERVICE_GROUP:-zenbu_team}"
+
+workspace="$(hg root)"
+cd "$workspace"
+
+revision="$(hg log -r . -T '{node|short}')"
+release_name="${revision}-$(date -u +%Y%m%dT%H%M%SZ)"
+release_dir="${RELEASE_ROOT}/${release_name}"
+staging_dir="${RELEASE_ROOT}/.${release_name}.tmp"
+next_link="${ACTIVE_PATH}.next"
+bundle_dir="bazel-bin/hg-web/hg_web_server_bundle"
+previous_release=""
+promoted=0
+
+health_check() {
+  for _ in $(seq 1 20); do
+    if curl --fail --silent --max-time 3 "$HEALTH_URL" >/dev/null; then
+      return 0
+    fi
+    sleep 1
+  done
+  echo "Health check failed: $HEALTH_URL" >&2
+  return 1
+}
+
+point_active_at() {
+  local target="$1"
+  sudo rm -f "$next_link"
+  sudo ln -s "$target" "$next_link"
+  sudo mv -Tf "$next_link" "$ACTIVE_PATH"
+}
+
+rollback() {
+  trap - ERR
+  if [[ "$promoted" -eq 1 && -n "$previous_release" && -d "$previous_release" ]]; then
+    echo "Deployment failed; rolling back to $previous_release" >&2
+    point_active_at "$previous_release"
+    sudo systemctl restart "$SERVICE_NAME"
+    health_check || echo "Rollback completed, but the health check still fails." >&2
+  else
+    echo "Deployment failed and no previous release is available for rollback." >&2
+  fi
+  sudo rm -rf "$staging_dir"
+  exit 1
+}
+trap rollback ERR
+
 bazel build -c opt //hg-web:hg_web_server_bundle
 
-# Create
-sudo cp -a bazel-bin/hg-web/hg_web_server_bundle /opt/hg_web_server_bundle_new
-sudo chown -R hg_web_server:zenbu_team /opt/hg_web_server_bundle_new
+sudo install -d -o root -g "$SERVICE_GROUP" -m 0755 "$RELEASE_ROOT"
+sudo rm -rf "$staging_dir"
+sudo install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$staging_dir"
+sudo cp -a "${bundle_dir}/." "$staging_dir/"
+sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$staging_dir"
+
+sudo test -x "$staging_dir/hg_web_server"
+sudo test -f "$staging_dir/hg-web/src/index.html"
+sudo test -f "$staging_dir/hg-web/src/page.js"
+sudo mv "$staging_dir" "$release_dir"
 
-# Swap
-sudo rm -rf /opt/hg_web_server_bundle_active
-sudo mv /opt/hg_web_server_bundle_new /opt/hg_web_server_bundle_active
+if [[ -L "$ACTIVE_PATH" ]]; then
+  previous_release="$(readlink -f "$ACTIVE_PATH")"
+elif [[ -d "$ACTIVE_PATH" ]]; then
+  previous_release="${RELEASE_ROOT}/legacy-$(date -u +%Y%m%dT%H%M%SZ)"
+  sudo mv "$ACTIVE_PATH" "$previous_release"
+fi
 
-sudo systemctl restart hg_web_server.service
-echo "Deployment complete!"
+point_active_at "$release_dir"
+promoted=1
+sudo systemctl restart "$SERVICE_NAME"
+health_check
+
+trap - ERR
+echo "Deployment complete: $release_dir"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/dev.sh	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ -n "${RUNFILES_DIR:-}" ]]; then
+  source "$RUNFILES_DIR/bazel_tools/tools/bash/runfiles/runfiles.bash"
+elif [[ -n "${RUNFILES_MANIFEST_FILE:-}" ]]; then
+  runfiles_library="$(
+    grep -sm1 '^bazel_tools/tools/bash/runfiles/runfiles.bash ' \
+      "$RUNFILES_MANIFEST_FILE" | cut -d' ' -f2-
+  )"
+  source "$runfiles_library"
+elif [[ -d "$0.runfiles" ]]; then
+  RUNFILES_DIR="$0.runfiles"
+  export RUNFILES_DIR
+  source "$RUNFILES_DIR/bazel_tools/tools/bash/runfiles/runfiles.bash"
+elif [[ -f "$0.runfiles_manifest" ]]; then
+  RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
+  export RUNFILES_MANIFEST_FILE
+  runfiles_library="$(
+    grep -sm1 '^bazel_tools/tools/bash/runfiles/runfiles.bash ' \
+      "$RUNFILES_MANIFEST_FILE" | cut -d' ' -f2-
+  )"
+  source "$runfiles_library"
+else
+  echo "Bazel runfiles are unavailable." >&2
+  exit 1
+fi
+
+workspace="${BUILD_WORKSPACE_DIRECTORY:-}"
+if [[ -z "$workspace" || ! -d "$workspace/.hg" ]]; then
+  echo "Run this target from the Zenbu workspace." >&2
+  exit 1
+fi
+
+server="$(rlocation _main/hg-web/hg_web_server)"
+runfiles_workspace="$(dirname "$(dirname "$server")")"
+hg_pid=""
+server_pid=""
+
+cleanup() {
+  trap - EXIT INT TERM
+  for pid in "$server_pid" "$hg_pid"; do
+    if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
+      kill "$pid" 2>/dev/null || true
+    fi
+  done
+  for pid in "$server_pid" "$hg_pid"; do
+    if [[ -n "$pid" ]]; then
+      wait "$pid" 2>/dev/null || true
+    fi
+  done
+}
+
+trap cleanup EXIT
+trap 'exit 130' INT TERM
+
+hg --repository "$workspace" serve \
+  --address 127.0.0.1 \
+  --port 4444 \
+  --accesslog - \
+  --errorlog - &
+hg_pid=$!
+
+(
+  cd "$runfiles_workspace"
+  exec "$server"
+) &
+server_pid=$!
+
+echo "hg-web: http://127.0.0.1:6970"
+echo "Mercurial wire endpoint: http://127.0.0.1:6970/repo"
+echo "Press Ctrl-C to stop both servers."
+
+set +e
+wait -n "$hg_pid" "$server_pid"
+status=$?
+set -e
+exit "$status"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/e2e/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,31 @@
+load("@aspect_rules_js//js:defs.bzl", "js_test")
+load("@hg_web_npm//:defs.bzl", "npm_link_all_packages")
+
+exports_files([
+    "package-lock.json",
+    "package.json",
+    "pnpm-lock.yaml",
+])
+
+npm_link_all_packages(name = "node_modules")
+
+js_test(
+    name = "app_e2e_test",
+    entry_point = "app_e2e_test.js",
+    data = [
+        ":node_modules/playwright-core",
+        "//hg-web:hg_web_server",
+        "@playwright_chromium_linux//:chromium",
+        "@playwright_chromium_linux//:chrome",
+    ],
+    env = {
+        "CHROMIUM_PATH": "$(rootpath @playwright_chromium_linux//:chrome)",
+    },
+    no_copy_to_bin = ["@playwright_chromium_linux//:chromium"],
+    size = "large",
+    target_compatible_with = [
+        "@platforms//cpu:x86_64",
+        "@platforms//os:linux",
+    ],
+    timeout = "long",
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/e2e/app_e2e_test.js	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,417 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { spawn, spawnSync } = require('node:child_process');
+const { chromium } = require('playwright-core');
+
+const BASE_URL = 'http://127.0.0.1:6970';
+const RUNFILES = process.env.JS_BINARY__RUNFILES;
+const WORKSPACE = process.env.JS_BINARY__WORKSPACE;
+
+function run(command, args, options = {}) {
+  const result = spawnSync(command, args, {
+    encoding: 'utf8',
+    ...options,
+  });
+  if (result.status !== 0) {
+    throw new Error(
+      `${command} ${args.join(' ')} failed\n${result.stdout || ''}${result.stderr || ''}`,
+    );
+  }
+  return result.stdout.trim();
+}
+
+function mercurialEnvironment(home) {
+  return {
+    ...process.env,
+    HGPLAIN: '1',
+    HGRCPATH: '',
+    HOME: home,
+  };
+}
+
+function stopProcess(child) {
+  if (!child || child.exitCode !== null) return Promise.resolve();
+  child.kill('SIGTERM');
+  return new Promise(resolve => {
+    const timer = setTimeout(() => {
+      if (child.exitCode === null) child.kill('SIGKILL');
+    }, 3000);
+    child.once('exit', () => {
+      clearTimeout(timer);
+      resolve();
+    });
+  });
+}
+
+async function waitForHttp(url, child, logs) {
+  const deadline = Date.now() + 15000;
+  while (Date.now() < deadline) {
+    if (child.exitCode !== null) {
+      throw new Error(`Server exited early with ${child.exitCode}\n${logs.join('')}`);
+    }
+    try {
+      const response = await fetch(url);
+      if (response.ok) return;
+    } catch {
+      // Keep waiting for startup.
+    }
+    await new Promise(resolve => setTimeout(resolve, 100));
+  }
+  throw new Error(`Timed out waiting for ${url}\n${logs.join('')}`);
+}
+
+function createFixtureRepository(root) {
+  const options = { env: mercurialEnvironment(root) };
+  run('hg', ['init', root], options);
+  fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
+  fs.mkdirSync(path.join(root, 'src'), { recursive: true });
+  fs.writeFileSync(
+    path.join(root, 'README.md'),
+    '# Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n',
+  );
+  fs.writeFileSync(path.join(root, 'docs', 'README.md'), '# Documentation\n\nNested README.\n');
+  fs.writeFileSync(path.join(root, 'src', 'main.c'), 'int main(void) { return 0; }\n');
+  fs.writeFileSync(
+    path.join(root, 'BUILD'),
+    'cc_library(\n    name = "fixture",\n    srcs = ["src/main.c"],\n)\n',
+  );
+  fs.writeFileSync(
+    path.join(root, 'pixel.png'),
+    Buffer.from(
+      'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2nWQAAAAASUVORK5CYII=',
+      'base64',
+    ),
+  );
+  fs.writeFileSync(path.join(root, 'movie.mp4'), Buffer.from('00000018667479706d703432', 'hex'));
+  fs.writeFileSync(path.join(root, 'sound.mp3'), Buffer.from('ID3'));
+  fs.writeFileSync(path.join(root, 'document.pdf'), Buffer.from('%PDF-1.4\n%%EOF\n'));
+  fs.writeFileSync(path.join(root, 'archive.bin'), Buffer.from([0, 1, 2, 3]));
+  run('hg', ['--repository', root, 'add'], options);
+  run('hg', ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Initial fixture'], options);
+  const firstNode = run('hg', ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options);
+
+  fs.writeFileSync(
+    path.join(root, 'README.md'),
+    '# Updated Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n\nSecond revision.\n',
+  );
+  fs.writeFileSync(path.join(root, 'new-file.txt'), 'new file\n');
+  run('hg', ['add', 'new-file.txt'], { ...options, cwd: root });
+  run('hg', ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Update fixture'], options);
+  const tipNode = run('hg', ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options);
+  return { firstNode, tipNode };
+}
+
+async function assertJson(pathname, status = 200) {
+  const response = await fetch(`${BASE_URL}${pathname}`);
+  const body = await response.text();
+  assert.equal(response.status, status, `${pathname} status: ${body}`);
+  return JSON.parse(body);
+}
+
+async function assertPageHasNoBrowserErrors(browser, pathname, assertion) {
+  const page = await browser.newPage();
+  const errors = [];
+  page.on('pageerror', error => errors.push(`pageerror: ${error.stack || error.message}`));
+  page.on('console', message => {
+    if (message.type() === 'error') errors.push(`console: ${message.text()}`);
+  });
+  page.on('requestfailed', request => {
+    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
+  });
+  page.on('response', response => {
+    if (response.status() >= 400) {
+      errors.push(`response: ${response.status()} ${response.url()}`);
+    }
+  });
+
+  const response = await page.goto(`${BASE_URL}${pathname}`, { waitUntil: 'networkidle' });
+  assert.equal(response.status(), 200, `${pathname} document status`);
+  await assertion(page);
+  if (errors.length > 0) {
+    throw new Error(`${pathname} browser errors\n${errors.join('\n')}`);
+  }
+  await page.close();
+}
+
+async function main() {
+  assert.ok(RUNFILES, 'rules_js runfiles path is required');
+  assert.ok(WORKSPACE, 'rules_js workspace name is required');
+
+  const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hg-web-e2e-'));
+  const appLogs = [];
+  const hgLogs = [];
+  let hgServer;
+  let appServer;
+  let browser;
+
+  try {
+    const { firstNode, tipNode } = createFixtureRepository(fixtureRoot);
+    const runfilesWorkspace = path.join(RUNFILES, WORKSPACE);
+    const serverBinary = path.join(runfilesWorkspace, 'hg-web', 'hg_web_server');
+    const chromiumPath = path.resolve(process.env.CHROMIUM_PATH);
+
+    assert.ok(fs.existsSync(serverBinary), `missing server binary: ${serverBinary}`);
+    assert.ok(fs.existsSync(chromiumPath), `missing Chromium binary: ${chromiumPath}`);
+
+    hgServer = spawn(
+      'hg',
+      [
+        '--repository', fixtureRoot,
+        'serve',
+        '--address', '127.0.0.1',
+        '--port', '4444',
+        '--accesslog', '-',
+        '--errorlog', '-',
+      ],
+      {
+        env: mercurialEnvironment(fixtureRoot),
+        stdio: ['ignore', 'pipe', 'pipe'],
+      },
+    );
+    hgServer.stdout.on('data', chunk => hgLogs.push(chunk.toString()));
+    hgServer.stderr.on('data', chunk => hgLogs.push(chunk.toString()));
+
+    appServer = spawn(serverBinary, [], {
+      cwd: runfilesWorkspace,
+      stdio: ['ignore', 'pipe', 'pipe'],
+    });
+    appServer.stdout.on('data', chunk => appLogs.push(chunk.toString()));
+    appServer.stderr.on('data', chunk => appLogs.push(chunk.toString()));
+
+    await waitForHttp(`${BASE_URL}/`, appServer, appLogs);
+    if (hgServer.exitCode !== null) {
+      throw new Error(`Mercurial server exited early with ${hgServer.exitCode}\n${hgLogs.join('')}`);
+    }
+    await waitForHttp(`${BASE_URL}/api/repo/list`, hgServer, hgLogs);
+
+    for (const pathname of ['/', '/directory', '/directory?path=docs', '/graph', `/changeset/${tipNode}`]) {
+      const response = await fetch(`${BASE_URL}${pathname}`);
+      assert.equal(response.status, 200, `${pathname} shell route`);
+      assert.match(await response.text(), /<title>Zenbu Repository<\/title>/);
+    }
+    for (const pathname of ['/page.js', '/index.css', '/base.css', '/pencil_lines.png', '/panda.png']) {
+      const response = await fetch(`${BASE_URL}${pathname}`);
+      assert.equal(response.status, 200, `${pathname} static asset`);
+      assert.ok((await response.arrayBuffer()).byteLength > 0, `${pathname} is non-empty`);
+    }
+    for (const pathname of ['/hg-web-background.jpg', '/pencil_texture.png']) {
+      assert.equal((await fetch(`${BASE_URL}${pathname}`)).status, 404);
+    }
+
+    const rootList = await assertJson('/api/repo/list');
+    assert.ok(rootList.directories.some(entry => entry.basename === 'docs'));
+    assert.ok(rootList.files.some(entry => entry.basename === 'README.md'));
+
+    const nestedList = await assertJson('/api/repo/list?path=docs');
+    assert.ok(nestedList.files.some(entry => entry.basename === 'README.md'));
+
+    const fileResponse = await fetch(`${BASE_URL}/api/repo/file?path=src%2Fmain.c`);
+    assert.equal(fileResponse.status, 200);
+    assert.match(await fileResponse.text(), /int main/);
+
+    for (const [filename, contentType, disposition] of [
+      ['pixel.png', 'image/png', 'inline'],
+      ['movie.mp4', 'video/mp4', 'inline'],
+      ['sound.mp3', 'audio/mpeg', 'inline'],
+      ['document.pdf', 'application/pdf', 'inline'],
+      ['archive.bin', 'application/octet-stream', 'attachment'],
+    ]) {
+      const response = await fetch(
+        `${BASE_URL}/api/repo/file?path=${encodeURIComponent(filename)}`,
+      );
+      assert.equal(response.status, 200, `${filename} response`);
+      assert.match(response.headers.get('content-type') || '', new RegExp(`^${contentType}`));
+      assert.equal(response.headers.get('content-disposition'), disposition);
+      assert.equal(response.headers.get('x-content-type-options'), 'nosniff');
+      assert.ok((await response.arrayBuffer()).byteLength > 0);
+    }
+    const binaryResponse = await fetch(`${BASE_URL}/api/repo/file?path=archive.bin`);
+    assert.deepEqual(
+      Buffer.from(await binaryResponse.arrayBuffer()),
+      Buffer.from([0, 1, 2, 3]),
+    );
+
+    const readmeResponse = await fetch(`${BASE_URL}/api/repo/readme?path=docs`);
+    assert.equal(readmeResponse.status, 200);
+    assert.match(await readmeResponse.text(), /Nested README/);
+    assert.equal((await fetch(`${BASE_URL}/api/repo/readme?path=src`)).status, 204);
+
+    const graph = await assertJson('/api/graph/tip?style=json');
+    assert.equal(graph.node, tipNode);
+    assert.ok(graph.changesets.length >= 2);
+
+    const changeset = await assertJson(`/api/changeset/${tipNode}`);
+    assert.equal(changeset.node, tipNode);
+    assert.equal(changeset.desc, 'Update fixture');
+    assert.ok(changeset.files.some(entry => entry.file === 'new-file.txt' && entry.status === 'added'));
+    assert.ok(changeset.diff.length > 0);
+
+    for (const pathname of [
+      '/api/repo/list?path=..%2Fetc',
+      '/api/repo/file?path=..%2FREADME.md',
+      '/api/graph/not-a-node?style=json',
+      '/api/changeset/not-a-node',
+    ]) {
+      const response = await fetch(`${BASE_URL}${pathname}`);
+      assert.equal(response.status, 400, `${pathname} rejects invalid input`);
+    }
+    assert.equal((await fetch(`${BASE_URL}/missing-route`)).status, 404);
+
+    const identify = run(
+      'hg',
+      ['identify', `${BASE_URL}/repo`],
+      { env: mercurialEnvironment(fixtureRoot) },
+    );
+    assert.match(identify, new RegExp(`^${tipNode.slice(0, 12)}`));
+
+    browser = await chromium.launch({
+      executablePath: chromiumPath,
+      headless: true,
+      args: ['--no-sandbox'],
+    });
+
+    await assertPageHasNoBrowserErrors(browser, '/', async page => {
+      await page.getByRole('heading', { name: 'Zenbu Repository' }).waitFor();
+      await page.getByText('Recent Commits').waitFor();
+      await page.getByText('Repository Files').waitFor();
+      assert.equal(await page.evaluate(() => window.__hgWebXss), undefined);
+      assert.equal(
+        await page.locator('.graph-container').evaluate(
+          element => getComputedStyle(element).backgroundImage,
+        ),
+        'none',
+      );
+      await page.locator('.theme-toggle').click();
+    });
+
+    await assertPageHasNoBrowserErrors(browser, '/directory', async page => {
+      await page.getByText('Repository Files').waitFor();
+      await page.getByRole('link', { name: 'README.md' }).first().click();
+      await page.getByText('Updated Fixture Repository').waitFor();
+      assert.equal(await page.evaluate(() => window.__hgWebXss), undefined);
+      await page.keyboard.press('Escape');
+
+      let delayedReadmeRequested = false;
+      let markReadmeRequested;
+      const readmeRequested = new Promise(resolve => {
+        markReadmeRequested = resolve;
+      });
+      await page.route(/\/api\/repo\/readme\?path=/, async route => {
+        delayedReadmeRequested = true;
+        markReadmeRequested();
+        await new Promise(resolve => setTimeout(resolve, 500));
+        await route.continue();
+      });
+      await page.getByRole('link', { name: 'docs' }).click();
+      await Promise.race([
+        readmeRequested,
+        new Promise((_, reject) => {
+          setTimeout(() => reject(new Error('Timed out waiting for delayed README request')), 5000);
+        }),
+      ]);
+      await page.getByRole('link', { name: 'root' }).click();
+      await page.getByText('Updated Fixture Repository').waitFor();
+      await page.waitForTimeout(600);
+      assert.equal(await page.getByText('Documentation').count(), 0);
+      assert.equal(delayedReadmeRequested, true);
+
+      await page.getByRole('link', { name: 'pixel.png' }).click();
+      const image = page.locator('.static-file-image');
+      await image.waitFor();
+      await image.evaluate(element => {
+        const imageElement = element;
+        if (imageElement.complete) return;
+        return new Promise((resolve, reject) => {
+          imageElement.addEventListener('load', resolve, { once: true });
+          imageElement.addEventListener('error', reject, { once: true });
+        });
+      });
+      assert.equal(await image.evaluate(element => element.naturalWidth), 1);
+      await page.getByRole('dialog', { name: 'Preview pixel.png' }).waitFor();
+      await page.keyboard.press('Escape');
+
+      await page.getByRole('link', { name: 'BUILD' }).click();
+      const buildCode = page.locator('code.language-python');
+      await buildCode.waitFor();
+      await page.getByText('cc_library').waitFor();
+      assert.match(await buildCode.textContent(), /name = "fixture"/);
+      await page.keyboard.press('Escape');
+
+      await page.getByRole('link', { name: 'src' }).click();
+      await page.getByRole('link', { name: 'main.c' }).click();
+      await page.getByText('int main(void)').waitFor();
+      await page.keyboard.press('Escape');
+    });
+
+    await assertPageHasNoBrowserErrors(browser, '/directory?path=docs', async page => {
+      await page.getByText('Documentation').waitFor();
+      await page.getByRole('link', { name: 'README.md' }).click();
+      await page.getByText('Nested README.').waitFor();
+    });
+
+    await assertPageHasNoBrowserErrors(browser, '/graph', async page => {
+      await page.getByText('Commit Graph').waitFor();
+      await page.waitForFunction(() => new URL(window.location.href).searchParams.has('tip'));
+      const graphUrl = page.url();
+      const rows = page.locator('.graph-row');
+      await rows.first().click();
+      await page.waitForURL(/\/changeset\/[0-9a-f]+$/);
+      await page.getByRole('heading', { name: 'Update fixture' }).waitFor();
+      await page.getByRole('button', { name: 'Back', exact: true }).click();
+      await page.waitForFunction(expected => window.location.href === expected, graphUrl);
+      await page.getByText('Commit Graph').waitFor();
+    });
+
+    await assertPageHasNoBrowserErrors(browser, `/changeset/${tipNode}`, async page => {
+      await page.getByRole('heading', { name: 'Update fixture' }).waitFor();
+      await page.locator('.changeset-files code').filter({ hasText: 'new-file.txt' }).waitFor();
+      await page.getByText('added', { exact: true }).waitFor();
+      await page.getByRole('region', { name: 'Changeset diff' }).waitFor();
+      await page.locator('.diff-column-headings').getByText('Before').first().waitFor();
+      await page.locator('.diff-column-headings').getByText('After').first().waitFor();
+      await page.locator('.diff-left.diff-remove').filter({ hasText: '# Fixture Repository' }).waitFor();
+      await page.locator('.diff-right.diff-add').filter({ hasText: '# Updated Fixture Repository' }).waitFor();
+      assert.equal(
+        await page.locator('.diff-left.diff-context').filter({ hasText: '<script>' }).first().textContent(),
+        '<script>window.__hgWebXss = true</script>',
+      );
+      assert.equal(
+        await page.locator('.changeset-paper').evaluate(
+          element => getComputedStyle(element).backgroundImage,
+        ),
+        'none',
+      );
+      await page.getByRole('button', { name: firstNode.slice(0, 12) }).click();
+      await page.waitForFunction(
+        expectedPath => window.location.pathname === expectedPath,
+        `/changeset/${firstNode}`,
+      );
+      await page.getByRole('heading', { name: 'Initial fixture' }).waitFor();
+      await page.getByRole('button', { name: 'Back', exact: true }).click();
+      await page.getByRole('heading', { name: 'Update fixture' }).waitFor();
+      await page.getByRole('button', { name: 'Back', exact: true }).click();
+      await page.getByText('Commit Graph').waitFor();
+    });
+
+    await assertPageHasNoBrowserErrors(browser, '/changeset/not-a-node', async page => {
+      await page.getByText('Recent Commits').waitFor();
+    });
+
+    await stopProcess(hgServer);
+    const unavailable = await fetch(`${BASE_URL}/api/repo/list`);
+    assert.equal(unavailable.status, 502);
+    assert.equal((await fetch(`${BASE_URL}/`)).status, 200);
+  } finally {
+    if (browser) await browser.close();
+    await stopProcess(appServer);
+    await stopProcess(hgServer);
+    fs.rmSync(fixtureRoot, { recursive: true, force: true });
+  }
+}
+
+main().catch(error => {
+  console.error(error.stack || error);
+  process.exitCode = 1;
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/e2e/package-lock.json	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,24 @@
+{
+  "name": "e2e",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "playwright-core": "1.48.2"
+      }
+    },
+    "node_modules/playwright-core": {
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
+      "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
+      "license": "Apache-2.0",
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/e2e/package.json	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,6 @@
+{
+  "private": true,
+  "dependencies": {
+    "playwright-core": "1.48.2"
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/e2e/pnpm-lock.yaml	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,18 @@
+lockfileVersion: '6.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+dependencies:
+  playwright-core:
+    specifier: 1.48.2
+    version: 1.48.2
+
+packages:
+
+  /[email protected]:
+    resolution: {integrity: sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==}
+    engines: {node: '>=18'}
+    hasBin: true
+    dev: false
--- a/hg-web/main.c	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/main.c	Sun Aug 02 16:50:48 2026 -0700
@@ -1,366 +1,780 @@
 #include "seobeo/seobeo.h"
 #include "dowa/dowa.h"
+
+#include <ctype.h>
+#include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <ctype.h>
+#include <strings.h>
+#include <time.h>
 #include <unistd.h>
-#include <sys/socket.h>
-#include <netinet/in.h>
-#include <arpa/inet.h>
-#include <netdb.h>
 
 #define HG_SERVE_HOST "127.0.0.1"
 #define HG_SERVE_PORT "4444"
+#define HG_API_TIMEOUT_MS 15000
+#define HG_STREAM_IDLE_TIMEOUT_MS 60000
+#define MAX_PATH_LENGTH 4096
+#define MAX_WIRE_QUERY_LENGTH 8192
+#define MAX_WIRE_HEADER_LENGTH 8192
 
-#define MAX_PATH 4096
+static const char *map_value_case_insensitive(Seobeo_Request_Entry *map, const char *key)
+{
+  if (!map || !key)
+    return NULL;
+
+  for (size_t i = 0; i < Dowa_Array_Length(map); i++)
+  {
+    if (map[i].key && strcasecmp(map[i].key, key) == 0)
+      return map[i].value;
+  }
+  return NULL;
+}
 
-static char* sanitize_path(const char *input_path, Dowa_Arena *arena)
+static char *arena_string(Dowa_Arena *arena, const char *value)
+{
+  size_t length = strlen(value);
+  char *copy = Dowa_Arena_Allocate(arena, length + 1);
+  memcpy(copy, value, length + 1);
+  return copy;
+}
+
+static Seobeo_Request_Entry *text_response(
+    Dowa_Arena *arena,
+    const char *status,
+    const char *content_type,
+    const char *body)
+{
+  Seobeo_Request_Entry *response = NULL;
+  Dowa_HashMap_Push_Arena(response, "status", arena_string(arena, status), arena);
+  Dowa_HashMap_Push_Arena(
+      response, "content-type", arena_string(arena, content_type), arena);
+  Dowa_HashMap_Push_Arena(response, "body", arena_string(arena, body), arena);
+  return response;
+}
+
+static boolean decode_url_component(
+    const char *encoded,
+    Dowa_Arena *arena,
+    char **decoded_out,
+    size_t *decoded_length_out)
 {
-  if (!input_path || strlen(input_path) == 0)
+  if (!encoded || !decoded_out)
+    return FALSE;
+
+  size_t encoded_length = strlen(encoded);
+  if (encoded_length >= MAX_PATH_LENGTH)
+    return FALSE;
+
+  char *decoded = Dowa_Arena_Allocate(arena, encoded_length + 1);
+  size_t output_length = 0;
+  for (size_t i = 0; i < encoded_length; i++)
   {
-    char *empty = Dowa_Arena_Allocate(arena, 1);
-    empty[0] = '\0';
-    return empty;
+    unsigned char value = (unsigned char)encoded[i];
+    if (encoded[i] == '%')
+    {
+      if (i + 2 >= encoded_length ||
+          !isxdigit((unsigned char)encoded[i + 1]) ||
+          !isxdigit((unsigned char)encoded[i + 2]))
+        return FALSE;
+
+      char hex[3] = {encoded[i + 1], encoded[i + 2], '\0'};
+      value = (unsigned char)strtoul(hex, NULL, 16);
+      i += 2;
+      if (value == '\0')
+        return FALSE;
+    }
+    decoded[output_length++] = (char)value;
+  }
+  decoded[output_length] = '\0';
+
+  *decoded_out = decoded;
+  if (decoded_length_out)
+    *decoded_length_out = output_length;
+  return TRUE;
+}
+
+static boolean normalize_repository_path(
+    const char *encoded_path,
+    Dowa_Arena *arena,
+    char **normalized_out)
+{
+  char *decoded = NULL;
+  size_t decoded_length = 0;
+  if (!decode_url_component(encoded_path ? encoded_path : "", arena, &decoded, &decoded_length))
+    return FALSE;
+
+  size_t start = 0;
+  size_t end = decoded_length;
+  if (start < end && decoded[start] == '/')
+  {
+    start++;
+    if (start < end && decoded[start] == '/')
+      return FALSE;
+  }
+  if (end > start && decoded[end - 1] == '/')
+  {
+    if (end - 1 > start && decoded[end - 2] == '/')
+      return FALSE;
+    end--;
+  }
+
+  size_t segment_start = start;
+  for (size_t i = start; i <= end; i++)
+  {
+    boolean at_end = i == end;
+    unsigned char c = at_end ? '/' : (unsigned char)decoded[i];
+    if (!at_end && (iscntrl(c) || c == '\\' || c == '?' || c == '#'))
+      return FALSE;
+
+    if (c == '/')
+    {
+      size_t segment_length = i - segment_start;
+      if (segment_length == 0 && !at_end)
+        return FALSE;
+      if ((segment_length == 1 && decoded[segment_start] == '.') ||
+          (segment_length == 2 && decoded[segment_start] == '.' &&
+           decoded[segment_start + 1] == '.'))
+        return FALSE;
+      segment_start = i + 1;
+    }
   }
 
-  size_t len = strlen(input_path);
-  char *result = Dowa_Arena_Allocate(arena, len + 1);
-  size_t j = 0;
+  size_t normalized_length = end - start;
+  char *normalized = Dowa_Arena_Allocate(arena, normalized_length + 1);
+  memcpy(normalized, decoded + start, normalized_length);
+  normalized[normalized_length] = '\0';
+  *normalized_out = normalized;
+  return TRUE;
+}
+
+static boolean validate_revision(const char *revision)
+{
+  if (!revision || revision[0] == '\0')
+    return FALSE;
+  if (strcmp(revision, "tip") == 0)
+    return TRUE;
 
-  for (size_t i = 0; i < len; i++)
+  size_t length = strlen(revision);
+  if (length > 40)
+    return FALSE;
+  for (size_t i = 0; i < length; i++)
   {
-    if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/'))
+    if (!isxdigit((unsigned char)revision[i]))
+      return FALSE;
+  }
+  return TRUE;
+}
+
+static char *encode_repository_path(const char *path, Dowa_Arena *arena)
+{
+  static const char hex[] = "0123456789ABCDEF";
+  size_t length = strlen(path);
+  char *encoded = Dowa_Arena_Allocate(arena, length * 3 + 1);
+  size_t output = 0;
+  for (size_t i = 0; i < length; i++)
+  {
+    unsigned char c = (unsigned char)path[i];
+    if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/')
+      encoded[output++] = (char)c;
+    else
     {
-      if (i + 1 < len && input_path[i+1] == '.')
-      {
-        // Skip ".."
-        i++;
-        continue;
-      }
-      // Skip "."
-      continue;
+      encoded[output++] = '%';
+      encoded[output++] = hex[c >> 4];
+      encoded[output++] = hex[c & 0x0F];
     }
-    result[j++] = input_path[i];
   }
-  result[j] = '\0';
+  encoded[output] = '\0';
+  return encoded;
+}
 
-  // Remove leading/trailing slashes
-  while (result[0] == '/')
-    memmove(result, result + 1, strlen(result));
-  while (j > 0 && result[j-1] == '/')
-    result[--j] = '\0';
+static boolean safe_header_value(const char *value, size_t maximum_length)
+{
+  if (!value)
+    return TRUE;
+  size_t length = strlen(value);
+  return length <= maximum_length &&
+         strchr(value, '\r') == NULL &&
+         strchr(value, '\n') == NULL;
+}
 
-  return result;
+static boolean extension_is(const char *extension, const char *expected)
+{
+  return extension && strcasecmp(extension, expected) == 0;
 }
 
-Seobeo_Client_Response  *hg_proxy_request(
-  const char *method,
-  const char *path,
-  const char *req_body,
-  const char *hg_custom)
+static const char *repository_file_content_type(
+    const char *path,
+    boolean *inline_preview,
+    boolean *sandbox_content)
 {
-  char full_path[MAX_PATH];
-  snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path);
-  Seobeo_Log(SEOBEO_DEBUG, "HG Proxy PATH %s\n", full_path);
-  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path);
-  Seobeo_Client_Request_Set_Method(p_req, method);
-  Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0");
-  Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json");
+  const char *extension = strrchr(path, '.');
+  *inline_preview = TRUE;
+  *sandbox_content = FALSE;
 
-  if (hg_custom && hg_custom[0] != '\0')
+  if (extension_is(extension, ".png")) return "image/png";
+  if (extension_is(extension, ".jpg") ||
+      extension_is(extension, ".jpeg")) return "image/jpeg";
+  if (extension_is(extension, ".gif")) return "image/gif";
+  if (extension_is(extension, ".webp")) return "image/webp";
+  if (extension_is(extension, ".avif")) return "image/avif";
+  if (extension_is(extension, ".bmp")) return "image/bmp";
+  if (extension_is(extension, ".ico")) return "image/x-icon";
+  if (extension_is(extension, ".svg"))
   {
-    char buffer[1024];
-    snprintf(buffer, 1024, "x-hgarg-1: %s", hg_custom);
-    Seobeo_Client_Request_Add_Header_Array(p_req, buffer);
-    Seobeo_Log(SEOBEO_DEBUG, "HG CUSTOM %s\n", buffer);
+    *sandbox_content = TRUE;
+    return "image/svg+xml";
   }
+  if (extension_is(extension, ".mp4") ||
+      extension_is(extension, ".m4v")) return "video/mp4";
+  if (extension_is(extension, ".webm")) return "video/webm";
+  if (extension_is(extension, ".mov")) return "video/quicktime";
+  if (extension_is(extension, ".ogv")) return "video/ogg";
+  if (extension_is(extension, ".mp3")) return "audio/mpeg";
+  if (extension_is(extension, ".wav")) return "audio/wav";
+  if (extension_is(extension, ".ogg") ||
+      extension_is(extension, ".oga")) return "audio/ogg";
+  if (extension_is(extension, ".flac")) return "audio/flac";
+  if (extension_is(extension, ".m4a")) return "audio/mp4";
+  if (extension_is(extension, ".aac")) return "audio/aac";
+  if (extension_is(extension, ".pdf")) return "application/pdf";
+  if (extension_is(extension, ".wasm")) return "application/wasm";
 
-  if (req_body)
-    Seobeo_Client_Request_Set_Body(p_req, req_body, strlen(req_body));
-  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
-  Seobeo_Client_Request_Destroy(p_req);
-  return p_resp;
+  *inline_preview = FALSE;
+  if (extension_is(extension, ".md") ||
+      extension_is(extension, ".markdown")) return "text/markdown; charset=utf-8";
+  if (extension_is(extension, ".txt") ||
+      extension_is(extension, ".log") ||
+      extension_is(extension, ".c") ||
+      extension_is(extension, ".h") ||
+      extension_is(extension, ".cc") ||
+      extension_is(extension, ".cpp") ||
+      extension_is(extension, ".js") ||
+      extension_is(extension, ".jsx") ||
+      extension_is(extension, ".ts") ||
+      extension_is(extension, ".tsx") ||
+      extension_is(extension, ".css") ||
+      extension_is(extension, ".html") ||
+      extension_is(extension, ".htm") ||
+      extension_is(extension, ".xml") ||
+      extension_is(extension, ".json") ||
+      extension_is(extension, ".yaml") ||
+      extension_is(extension, ".yml") ||
+      extension_is(extension, ".toml") ||
+      extension_is(extension, ".sh") ||
+      extension_is(extension, ".py") ||
+      extension_is(extension, ".rs") ||
+      extension_is(extension, ".go"))
+    return "text/plain; charset=utf-8";
+  return "application/octet-stream";
 }
 
-Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+static Seobeo_Client_Response *hg_proxy_request(
+    const char *method,
+    const char *path,
+    const char *request_body,
+    size_t request_body_length,
+    const char *hg_argument,
+    const char *accept)
 {
-  Seobeo_Request_Entry *resp = NULL;
-
-  void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path");
-  const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
-
-  char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
-  Seobeo_Url_Decode(decoded_path, rel_path);
-
-  char *safe_path = sanitize_path(decoded_path, arena);
+  char url[MAX_PATH_LENGTH];
+  int url_length = snprintf(
+      url, sizeof(url), "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path);
+  if (url_length < 0 || (size_t)url_length >= sizeof(url))
+    return NULL;
 
-  Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path);
+  Seobeo_Client_Request *request = Seobeo_Client_Request_Create(url);
+  if (!request)
+    return NULL;
 
-  char hg_path[MAX_PATH];
-  if (strlen(safe_path) > 0)
-    snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path);
-  else
-    snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json");
+  Seobeo_Client_Request_Set_Method(request, method);
+  Seobeo_Client_Request_Add_Header_Map(request, "User-Agent", "Seobeo/1.0");
+  Seobeo_Client_Request_Add_Header_Map(
+      request, "Accept", accept ? accept : "application/json");
+  Seobeo_Client_Request_Set_Timeout_Milliseconds(request, HG_API_TIMEOUT_MS);
 
-  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL);
-
-  Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);
-
-  if (hg_response->status_code != 200)
+  if (hg_argument && hg_argument[0] != '\0')
   {
-    Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n");
-    Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena);
-    return resp;
-  }
-
-  char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length);
-  char *temp2 = Dowa_Arena_Allocate(arena, 256);
-  snprintf(temp2, 256, "%zu", hg_response->body_length);
-
-  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
-  Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
-  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
-  return resp;
-}
-
-Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena)
-{
-  Seobeo_Request_Entry *resp = NULL;
-
-  void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path");
-  const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
-  char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
-  Seobeo_Url_Decode(decoded_path, rel_path);
-  char *safe_path = sanitize_path(decoded_path, arena);
-
-  Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path);
-
-  if (strlen(safe_path) == 0)
-  {
-    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-    Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena);
-    return resp;
+    if (!safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH))
+    {
+      Seobeo_Client_Request_Destroy(request);
+      return NULL;
+    }
+    Seobeo_Client_Request_Add_Header_Map(request, "x-hgarg-1", hg_argument);
   }
 
-  char hg_path[MAX_PATH];
-  snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path);
-  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL);
+  if (request_body && request_body_length > 0)
+    Seobeo_Client_Request_Set_Body(request, request_body, request_body_length);
+
+  Seobeo_Client_Response *response = Seobeo_Client_Request_Execute(request);
+  Seobeo_Client_Request_Destroy(request);
+  return response;
+}
+
+static Seobeo_Request_Entry *forward_hg_response(
+    Seobeo_Client_Response *hg_response,
+    const char *default_content_type,
+    const char *override_content_type,
+    Dowa_Arena *arena)
+{
+  if (!hg_response)
+    return text_response(
+        arena, "502", "application/json", "{\"error\":\"Mercurial backend unavailable\"}");
+
+  const char *upstream_content_type =
+      map_value_case_insensitive(hg_response->headers, "Content-Type");
+  const char *upstream_or_default_content_type =
+      override_content_type
+          ? override_content_type
+          : upstream_content_type ? upstream_content_type : default_content_type;
+  if (!upstream_or_default_content_type)
+    upstream_or_default_content_type = "application/octet-stream";
+  char *content_type = arena_string(arena, upstream_or_default_content_type);
+
+  char *status = Dowa_Arena_Allocate(arena, 8);
+  snprintf(status, 8, "%d", hg_response->status_code);
+
+  size_t body_length = hg_response->body ? hg_response->body_length : 0;
+  char *body = Dowa_Arena_Allocate(arena, body_length + 1);
+  if (body_length > 0)
+    memcpy(body, hg_response->body, body_length);
+  body[body_length] = '\0';
+  char *content_length = Dowa_Arena_Allocate(arena, 32);
+  snprintf(content_length, 32, "%zu", body_length);
+
+  Seobeo_Request_Entry *response = NULL;
+  Dowa_HashMap_Push_Arena(response, "status", status, arena);
+  Dowa_HashMap_Push_Arena(response, "content-type", content_type, arena);
+  Dowa_HashMap_Push_Arena(response, "body", body, arena);
+  Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena);
+  Seobeo_Client_Response_Destroy(hg_response);
+  return response;
+}
+
+Seobeo_Request_Entry *ApiListDirectory(Seobeo_Request_Entry *request, Dowa_Arena *arena)
+{
+  const char *encoded_path = map_value_case_insensitive(request, "query_path");
+  char *path = NULL;
+  if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path))
+    return text_response(arena, "400", "application/json", "{\"error\":\"Invalid repository path\"}");
+
+  char *encoded = encode_repository_path(path, arena);
+  char hg_path[MAX_PATH_LENGTH];
+  int length = snprintf(
+      hg_path,
+      sizeof(hg_path),
+      encoded[0] ? "/file/tip/%s?style=json" : "/file/tip/?style=json",
+      encoded);
+  if (length < 0 || (size_t)length >= sizeof(hg_path))
+    return text_response(arena, "400", "application/json", "{\"error\":\"Repository path is too long\"}");
 
-  Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);
+  return forward_hg_response(
+      hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
+      "application/json",
+      NULL,
+      arena);
+}
+
+Seobeo_Request_Entry *ApiGetFile(Seobeo_Request_Entry *request, Dowa_Arena *arena)
+{
+  const char *encoded_path = map_value_case_insensitive(request, "query_path");
+  char *path = NULL;
+  if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &path) ||
+      path[0] == '\0')
+    return text_response(arena, "400", "text/plain", "A valid file path is required");
+
+  char *encoded = encode_repository_path(path, arena);
+  char hg_path[MAX_PATH_LENGTH];
+  int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded);
+  if (length < 0 || (size_t)length >= sizeof(hg_path))
+    return text_response(arena, "400", "text/plain", "File path is too long");
+
+  Seobeo_Client_Response *hg_response =
+      hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream");
+  if (!hg_response)
+    return forward_hg_response(NULL, "application/json", NULL, arena);
 
-  char status[4];
-  snprintf(status, 4, "%i", hg_response->status_code);
+  boolean inline_preview = FALSE;
+  boolean sandbox_content = FALSE;
+  const char *content_type =
+      repository_file_content_type(path, &inline_preview, &sandbox_content);
+  Seobeo_Request_Entry *response = forward_hg_response(
+      hg_response, "application/octet-stream", content_type, arena);
+  Dowa_HashMap_Push_Arena(
+      response,
+      "Content-Disposition",
+      inline_preview ? "inline" : "attachment",
+      arena);
+  Dowa_HashMap_Push_Arena(
+      response, "X-Content-Type-Options", "nosniff", arena);
+  if (sandbox_content)
+    Dowa_HashMap_Push_Arena(
+        response, "Content-Security-Policy", "sandbox", arena);
+  return response;
+}
 
-  if (!hg_response->body)
+Seobeo_Request_Entry *ApiGetReadme(Seobeo_Request_Entry *request, Dowa_Arena *arena)
+{
+  const char *encoded_path = map_value_case_insensitive(request, "query_path");
+  char *directory = NULL;
+  if (!normalize_repository_path(encoded_path ? encoded_path : "", arena, &directory))
+    return text_response(arena, "400", "text/plain", "Invalid repository path");
+
+  size_t readme_length = strlen(directory) + strlen("/README.md") + 1;
+  if (readme_length >= MAX_PATH_LENGTH)
+    return text_response(arena, "400", "text/plain", "README path is too long");
+
+  char *readme_path = Dowa_Arena_Allocate(arena, readme_length);
+  snprintf(
+      readme_path,
+      readme_length,
+      directory[0] ? "%s/README.md" : "README.md",
+      directory);
+  char *encoded = encode_repository_path(readme_path, arena);
+
+  char hg_path[MAX_PATH_LENGTH];
+  int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded);
+  if (length < 0 || (size_t)length >= sizeof(hg_path))
+    return text_response(arena, "400", "text/plain", "README path is too long");
+
+  Seobeo_Client_Response *hg_response =
+      hg_proxy_request("GET", hg_path, NULL, 0, NULL, "text/markdown");
+  if (hg_response && hg_response->status_code == HTTP_NOT_FOUND)
   {
-    Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-    Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena);
-    return resp;
+    Seobeo_Client_Response_Destroy(hg_response);
+    return text_response(arena, "204", "text/markdown", "");
   }
+  return forward_hg_response(
+      hg_response, "text/markdown", "text/markdown; charset=utf-8", arena);
+}
 
-  if (hg_response->status_code != 200)
+Seobeo_Request_Entry *ApiGetGraph(Seobeo_Request_Entry *request, Dowa_Arena *arena)
+{
+  const char *graph_id = map_value_case_insensitive(request, ":graph_id");
+  if (!validate_revision(graph_id))
+    return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph revision\"}");
+
+  const char *encoded_graph_top =
+      map_value_case_insensitive(request, "query_graphtop");
+  char *graph_top = NULL;
+  if (encoded_graph_top)
   {
-    Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error hg_response: %s\n", hg_response->body);
-    Dowa_HashMap_Push_Arena(resp, "status", status, arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-    Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
-    return resp;
+    if (!decode_url_component(encoded_graph_top, arena, &graph_top, NULL) ||
+        !validate_revision(graph_top))
+      return text_response(arena, "400", "application/json", "{\"error\":\"Invalid graph top revision\"}");
   }
 
-
-  char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length);
-  char *temp2 = Dowa_Arena_Allocate(arena, 256);
-  snprintf(temp2, 256, "%zu", hg_response->body_length);
+  char hg_path[MAX_PATH_LENGTH];
+  int length = graph_top
+      ? snprintf(
+            hg_path,
+            sizeof(hg_path),
+            "/graph/%s?graphtop=%s&style=json",
+            graph_id,
+            graph_top)
+      : snprintf(hg_path, sizeof(hg_path), "/graph/%s?style=json", graph_id);
+  if (length < 0 || (size_t)length >= sizeof(hg_path))
+    return text_response(arena, "400", "application/json", "{\"error\":\"Graph request is too long\"}");
 
-  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
-  Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
-  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
-
-  return resp;
+  return forward_hg_response(
+      hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
+      "application/json",
+      NULL,
+      arena);
 }
 
-Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) {
-  return ApiGetFile(req, arena);
+Seobeo_Request_Entry *ApiGetChangeset(Seobeo_Request_Entry *request, Dowa_Arena *arena)
+{
+  const char *changeset_id = map_value_case_insensitive(request, ":changeset_id");
+  if (!validate_revision(changeset_id))
+    return text_response(arena, "400", "application/json", "{\"error\":\"Invalid changeset revision\"}");
+
+  char hg_path[128];
+  int length = snprintf(hg_path, sizeof(hg_path), "/json-rev/%s", changeset_id);
+  if (length < 0 || (size_t)length >= sizeof(hg_path))
+    return text_response(arena, "400", "application/json", "{\"error\":\"Changeset request is too long\"}");
+
+  return forward_hg_response(
+      hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
+      "application/json",
+      NULL,
+      arena);
+}
+
+static int64_t monotonic_milliseconds(void)
+{
+  struct timespec now;
+  clock_gettime(CLOCK_MONOTONIC, &now);
+  return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
+}
+
+static size_t find_http_header_length(const uint8 *buffer, size_t length)
+{
+  if (!buffer || length < 4)
+    return 0;
+  for (size_t i = 0; i + 3 < length; i++)
+  {
+    if (buffer[i] == '\r' && buffer[i + 1] == '\n' &&
+        buffer[i + 2] == '\r' && buffer[i + 3] == '\n')
+      return i + 4;
+  }
+  return 0;
 }
 
-// Streaming handler for hg wire protocol - pipes data directly without buffering
-void StreamHgWireProtocol(Seobeo_Handle *p_client, Seobeo_Request_Entry *req, Dowa_Arena *arena)
+static void send_proxy_error(Seobeo_Handle *client, int status, const char *message)
 {
-  void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method");
-  const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET";
-
-  void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
-  const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : "";
-
-  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
-  const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : "";
-
-  const char *hg_custom = req[7].value;
+  const char *reason = status == 504 ? "Gateway Timeout" : "Bad Gateway";
+  char response[512];
+  int length = snprintf(
+      response,
+      sizeof(response),
+      "HTTP/1.1 %d %s\r\n"
+      "Content-Type: text/plain\r\n"
+      "Content-Length: %zu\r\n"
+      "Connection: close\r\n"
+      "\r\n"
+      "%s",
+      status,
+      reason,
+      strlen(message),
+      message);
+  if (length > 0 && (size_t)length < sizeof(response))
+  {
+    Seobeo_Handle_Queue(client, (const uint8 *)response, (uint32)length);
+    Seobeo_Handle_Flush(client);
+  }
+}
 
-  Seobeo_Log(SEOBEO_DEBUG, "HG Stream Proxy: method=%s query=%s\n", method, query_string);
+static boolean parse_content_length(const char *value, size_t *length_out)
+{
+  if (!value || !length_out || value[0] == '\0')
+    return FALSE;
+  errno = 0;
+  char *end = NULL;
+  unsigned long long parsed = strtoull(value, &end, 10);
+  if (errno != 0 || !end || *end != '\0' || parsed > SIZE_MAX)
+    return FALSE;
+  *length_out = (size_t)parsed;
+  return TRUE;
+}
 
-  // THINKING: Connect to hg serve
-  // This kinda blows, but not a good way to handle it since my client API assumes it is all stored in
-  // buffer and what not.
-  Seobeo_Handle *p_upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE);
-  if (!p_upstream || p_upstream->socket < 0)
+void StreamHgWireProtocol(
+    Seobeo_Handle *client,
+    Seobeo_Request_Entry *request,
+    Dowa_Arena *arena)
+{
+  (void)arena;
+  const char *method = map_value_case_insensitive(request, "HTTP_Method");
+  const char *query = map_value_case_insensitive(request, "QueryString");
+  const char *body = map_value_case_insensitive(request, "Body");
+  const char *content_length_value =
+      map_value_case_insensitive(request, "Content-Length");
+  const char *content_type = map_value_case_insensitive(request, "Content-Type");
+  const char *hg_argument = map_value_case_insensitive(request, "x-hgarg-1");
+
+  if (!method || !query ||
+      (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) ||
+      !safe_header_value(query, MAX_WIRE_QUERY_LENGTH) ||
+      !safe_header_value(hg_argument, MAX_WIRE_HEADER_LENGTH) ||
+      !safe_header_value(content_type, 256))
   {
-    const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 26\r\n\r\nFailed to connect upstream";
-    Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp));
-    Seobeo_Handle_Flush(p_client);
-    if (p_upstream)
-      Seobeo_Handle_Destroy(p_upstream);
+    send_proxy_error(client, 502, "Invalid Mercurial proxy request");
     return;
   }
 
-  // Create headers
-  // we only allow x-hgarg-1 and content-length
-  char request_buf[8192];
-  int req_len = snprintf(request_buf, sizeof(request_buf),
-    "%s /?%s HTTP/1.1\r\n"
-    "Host: %s:%s\r\n"
-    "User-Agent: Seobeo/1.0\r\n"
-    "Connection: close\r\n",
-    method, query_string, HG_SERVE_HOST, HG_SERVE_PORT);
+  size_t body_length = 0;
+  if (content_length_value &&
+      !parse_content_length(content_length_value, &body_length))
+  {
+    send_proxy_error(client, 502, "Invalid Mercurial request length");
+    return;
+  }
+  if (body_length > 0 && !body)
+  {
+    send_proxy_error(client, 502, "Missing Mercurial request body");
+    return;
+  }
+  if (body_length > UINT32_MAX)
+  {
+    send_proxy_error(client, 502, "Mercurial request body is too large");
+    return;
+  }
 
-  if (hg_custom && hg_custom[0] != '\0')
-    req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "x-hgarg-1: %s\r\n", hg_custom);
+  Seobeo_Handle *upstream =
+      Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE);
+  if (!upstream || upstream->socket < 0)
+  {
+    if (upstream)
+      Seobeo_Handle_Destroy(upstream);
+    send_proxy_error(client, 502, "Mercurial backend unavailable");
+    return;
+  }
 
-  if (req_body && req_body[0] != '\0')
-    req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "Content-Length: %zu\r\n\r\n%s", strlen(req_body), req_body);
-  else
-    req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "\r\n");
+  char request_header[16384];
+  int header_length = snprintf(
+      request_header,
+      sizeof(request_header),
+      "%s /?%s HTTP/1.1\r\n"
+      "Host: %s:%s\r\n"
+      "User-Agent: Seobeo/1.0\r\n"
+      "Connection: close\r\n",
+      method,
+      query,
+      HG_SERVE_HOST,
+      HG_SERVE_PORT);
+  if (header_length < 0 || (size_t)header_length >= sizeof(request_header))
+  {
+    Seobeo_Handle_Destroy(upstream);
+    send_proxy_error(client, 502, "Mercurial request headers are too large");
+    return;
+  }
 
-  Seobeo_Handle_Queue(p_upstream, (uint8*)request_buf, req_len);
-  if (Seobeo_Handle_Flush(p_upstream) < 0)
+#define APPEND_WIRE_HEADER(...) \
+  do { \
+    int appended = snprintf( \
+        request_header + header_length, \
+        sizeof(request_header) - (size_t)header_length, \
+        __VA_ARGS__); \
+    if (appended < 0 || (size_t)appended >= sizeof(request_header) - (size_t)header_length) { \
+      Seobeo_Handle_Destroy(upstream); \
+      send_proxy_error(client, 502, "Mercurial request headers are too large"); \
+      return; \
+    } \
+    header_length += appended; \
+  } while (0)
+
+  if (hg_argument && hg_argument[0] != '\0')
+    APPEND_WIRE_HEADER("x-hgarg-1: %s\r\n", hg_argument);
+  if (content_type && content_type[0] != '\0')
+    APPEND_WIRE_HEADER("Content-Type: %s\r\n", content_type);
+  if (body_length > 0)
+    APPEND_WIRE_HEADER("Content-Length: %zu\r\n", body_length);
+  APPEND_WIRE_HEADER("\r\n");
+#undef APPEND_WIRE_HEADER
+
+  if (Seobeo_Handle_Queue(
+          upstream, (const uint8 *)request_header, (uint32)header_length) != 0 ||
+      (body_length > 0 &&
+       Seobeo_Handle_Queue(upstream, (const uint8 *)body, (uint32)body_length) != 0) ||
+      Seobeo_Handle_Flush(upstream) != 0)
   {
-    const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 21\r\n\r\nUpstream write failed";
-    Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp));
-    Seobeo_Handle_Flush(p_client);
-    Seobeo_Handle_Destroy(p_upstream);
+    Seobeo_Handle_Destroy(upstream);
+    send_proxy_error(client, 502, "Mercurial backend write failed");
     return;
   }
 
-  // Responses 
-  while (1)
+  boolean response_started = FALSE;
+  int64_t last_progress = monotonic_milliseconds();
+  while (!response_started)
   {
-    int r = Seobeo_Handle_Read(p_upstream);
-    if (r < 0)
+    int read_result = Seobeo_Handle_Read(upstream);
+    if (read_result == -2 || read_result < 0)
     {
-      Seobeo_Handle_Destroy(p_upstream);
+      Seobeo_Handle_Destroy(upstream);
+      send_proxy_error(client, 502, "Mercurial backend closed before responding");
       return;
     }
-    if (p_upstream->read_buffer_len >= 4 &&
-        strstr((char*)p_upstream->read_buffer, "\r\n\r\n") != NULL)
-      break;
-    if (r == 0)
-      continue;
-  }
-
-  // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows.
-  char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n");
-  if (!hdr_end)
-  {
-    Seobeo_Handle_Destroy(p_upstream);
-    return;
-  }
-  size_t hdr_len = hdr_end - (char*)p_upstream->read_buffer + 4;
-  Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, hdr_len);
-  Seobeo_Handle_Flush(p_client);
+    if (read_result > 0)
+      last_progress = monotonic_milliseconds();
 
-  // All body 
-  size_t body_in_buffer = p_upstream->read_buffer_len - hdr_len;
-  if (body_in_buffer > 0)
-  {
-    Seobeo_Handle_Queue(p_client, p_upstream->read_buffer + hdr_len, body_in_buffer);
-    Seobeo_Handle_Flush(p_client);
-  }
-  Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len);
-  while (1)
-  {
-    int n = Seobeo_Handle_Read(p_upstream);
-    if (n > 0)
+    size_t header_size =
+        find_http_header_length(upstream->read_buffer, upstream->read_buffer_len);
+    if (header_size > 0)
     {
-      Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, p_upstream->read_buffer_len);
-      Seobeo_Handle_Flush(p_client);
-      Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len);
+      (void)header_size;
+      if (Seobeo_Handle_Queue(
+              client, upstream->read_buffer, upstream->read_buffer_len) != 0 ||
+          Seobeo_Handle_Flush(client) != 0)
+      {
+        Seobeo_Handle_Destroy(upstream);
+        return;
+      }
+      Seobeo_Handle_Consume(upstream, upstream->read_buffer_len);
+      response_started = TRUE;
+      break;
     }
-    else if (n == -2)
-      break;
-    else if (n < 0)
-      break;
+
+    if (read_result == 0)
+    {
+      if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS)
+      {
+        Seobeo_Handle_Destroy(upstream);
+        send_proxy_error(client, 504, "Mercurial backend response timed out");
+        return;
+      }
+      usleep(1000);
+    }
   }
 
-  Seobeo_Handle_Destroy(p_upstream);
+  while (TRUE)
+  {
+    int read_result = Seobeo_Handle_Read(upstream);
+    if (read_result == -2)
+      break;
+    if (read_result < 0)
+      break;
+    if (read_result == 0)
+    {
+      if (monotonic_milliseconds() - last_progress >= HG_STREAM_IDLE_TIMEOUT_MS)
+      {
+        Seobeo_Log(SEOBEO_ERROR, "Mercurial response stream timed out\n");
+        break;
+      }
+      usleep(1000);
+      continue;
+    }
+
+    last_progress = monotonic_milliseconds();
+    if (Seobeo_Handle_Queue(
+            client, upstream->read_buffer, upstream->read_buffer_len) != 0 ||
+        Seobeo_Handle_Flush(client) != 0)
+      break;
+    Seobeo_Handle_Consume(upstream, upstream->read_buffer_len);
+  }
+
+  Seobeo_Handle_Destroy(upstream);
 }
 
-Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+Seobeo_Request_Entry *GetReactHome(Seobeo_Request_Entry *request, Dowa_Arena *arena)
 {
-  Seobeo_Request_Entry *resp = NULL;
-
-  void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method");
-  const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET";
-
-  void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
-  const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : "";
-
-  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
-  const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : "";
-  size_t body_len = strlen(req_body);
-
-  const char *hg_custom = req[7].value;
-  Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len);
-
-  Seobeo_Client_Response *hg_response;
+  (void)request;
+  size_t file_size = 0;
+  char *html = Seobeo_Web_LoadFile("/index.html", &file_size);
+  if (!html)
+    return text_response(arena, "500", "text/plain", "Application shell unavailable");
 
-  char hg_path[MAX_PATH];
-  snprintf(hg_path, sizeof(hg_path), "/?%s", query_string);
-
-  hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom);
-
-  Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length);
-
-  Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type");
-
-  char *status = Dowa_Arena_Allocate(arena, 5);
-  snprintf(status, 4, "%i", hg_response->status_code);
-
-  // Use binary-safe copy to handle null bytes in mercurial bundle data
-  char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length);
-  char *temp2 = Dowa_Arena_Allocate(arena, 256);
-  snprintf(temp2, 256, "%zu", hg_response->body_length);
-
-  Dowa_HashMap_Push_Arena(resp, "status", status, arena);
-  Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena);
-  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
-  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
-
-  return resp;
+  Seobeo_Request_Entry *response = NULL;
+  char *content_length = Dowa_Arena_Allocate(arena, 32);
+  snprintf(content_length, 32, "%zu", file_size);
+  Dowa_HashMap_Push_Arena(response, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(response, "content-type", "text/html", arena);
+  Dowa_HashMap_Push_Arena(response, "body", html, arena);
+  Dowa_HashMap_Push_Arena(response, "content-length", content_length, arena);
+  return response;
 }
 
-int main(void) {
+int main(void)
+{
   Seobeo_Router_Init();
 
+  Seobeo_Router_Register("GET", "/", GetReactHome);
+  Seobeo_Router_Register("GET", "/directories", GetReactHome);
+  Seobeo_Router_Register("GET", "/directory", GetReactHome);
+  Seobeo_Router_Register("GET", "/graph", GetReactHome);
+  Seobeo_Router_Register("GET", "/changeset/:changeset_id", GetReactHome);
+
   Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory);
   Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile);
   Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme);
+  Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph);
+  Seobeo_Router_Register("GET", "/api/changeset/:changeset_id", ApiGetChangeset);
 
-  // Use streaming handler for hg wire protocol... 
   Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol);
   Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol);
 
   printf("Starting on Port 6970...\n");
-
-  int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1);
-
+  int result =
+      Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1);
   Seobeo_Router_Destroy();
-
   return result;
 }
--- a/hg-web/src/base.css	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/src/base.css	Sun Aug 02 16:50:48 2026 -0700
@@ -1,123 +1,169 @@
-/* --- Colors  ---*/
+/* Reset CSS: https://meyerweb.com/eric/tools/css/reset/ */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+
+/* ===========================================
+   Base CSS - Color Variables and Basic Setup
+   =========================================== */
+
+/* Light mode (default) */
 :root {
   --bg: #ffffff;
-  --fg: #1a1a1a;
-  --border: #e0e0e0;
-  --hover: #f5f5f5;
-  --accent: #0066cc;
-  --accent-hover: #0052a3;
-  --secondary: #6c757d;
-  --success: #28a745;
-  --warning: #ffc107;
-  --danger: #dc3545;
-  --code-bg: #f6f8fa;
-  --link: #0066cc;
-  --link-hover: #0052a3;
+  --bg-subtle: #f6f8fa;
+  --bg-code: #f6f8fa;
+  --border: #d0d7de;
+  --accent: #0969da;
+  --text-primary: #1f2328;
+  --text-secondary: #656d76;
+  --hover: #f3f4f6;
+  --success: #1a7f37;
+  --danger: #cf222e;
+  --danger-bg: #ffebe9;
+  --danger-border: #ffdce0;
+  --overlay: rgba(0, 0, 0, 0.5);
+
+  /* Graph colors - light mode */
+  --graph-1: #495057;
+  --graph-2: #1971c2;
+  --graph-3: #099268;
+  --graph-4: #e67700;
+  --graph-5: #7048e8;
+  --graph-6: #c92a2a;
+  --graph-7: #c2255c;
+  --graph-node-border: #ffffff;
 }
 
-.dark {
+/* Dark mode - applied when html has .dark class */
+:root.dark {
   --bg: #0d1117;
-  --fg: #c9d1d9;
+  --bg-subtle: #161b22;
+  --bg-code: #161b22;
   --border: #30363d;
-  --hover: #161b22;
   --accent: #58a6ff;
-  --accent-hover: #79c0ff;
-  --secondary: #8b949e;
-  --success: #3fb950;
-  --warning: #d29922;
+  --text-primary: #e6edf3;
+  --text-secondary: #8b949e;
+  --hover: #1c2128;
+  --success: #238636;
   --danger: #f85149;
-  --code-bg: #161b22;
-  --link: #58a6ff;
-  --link-hover: #79c0ff;
+  --danger-bg: #f8514926;
+  --danger-border: #f8514966;
+  --overlay: rgba(0, 0, 0, 0.7);
+
+  /* Graph colors - dark mode */
+  --graph-1: #868e96;
+  --graph-2: #4dabf7;
+  --graph-3: #63e6be;
+  --graph-4: #ffbc42;
+  --graph-5: #b197fc;
+  --graph-6: #ff8787;
+  --graph-7: #f06595;
+  --graph-node-border: #1a1a1a;
 }
 
+/* System preference fallback (when no explicit class is set) */
 @media (prefers-color-scheme: dark) {
-  :root:not(.light-mode) {
+  :root:not(.light) {
     --bg: #0d1117;
-    --fg: #c9d1d9;
+    --bg-subtle: #161b22;
+    --bg-code: #161b22;
     --border: #30363d;
-    --hover: #161b22;
     --accent: #58a6ff;
-    --accent-hover: #79c0ff;
-    --secondary: #8b949e;
-    --success: #3fb950;
-    --warning: #d29922;
+    --text-primary: #e6edf3;
+    --text-secondary: #8b949e;
+    --hover: #1c2128;
+    --success: #238636;
     --danger: #f85149;
-    --code-bg: #161b22;
-    --link: #58a6ff;
-    --link-hover: #79c0ff;
+    --danger-bg: #f8514926;
+    --danger-border: #f8514966;
+    --overlay: rgba(0, 0, 0, 0.7);
+
+    --graph-1: #868e96;
+    --graph-2: #4dabf7;
+    --graph-3: #63e6be;
+    --graph-4: #ffbc42;
+    --graph-5: #b197fc;
+    --graph-6: #ff8787;
+    --graph-7: #f06595;
+    --graph-node-border: #1a1a1a;
   }
 }
 
-/* --- Reset and Base Styles --- */
+/* Fonts */
+@font-face {
+  font-family: "Roboto";
+  src: url("/public/fonts/Roboto-Regular.ttf");
+}
+@font-face {
+  font-family: "Roboto Light";
+  src: url("/public/fonts/Roboto-Thin.ttf");
+}
+@font-face {
+  font-family: "More Thin";
+  src: url("/public/fonts/more-sugar.thin.otf");
+}
+@font-face {
+  font-family: "More";
+  src: url("/public/fonts/more-sugar.regular.otf");
+}
+
+button {
+  font-family: "More Thin", sans-serif;
+}
+
+/* Reset and Base */
 * {
   margin: 0;
   padding: 0;
   box-sizing: border-box;
 }
 
-html {
-  background: var(--bg);
-  color: var(--fg);
-}
-
-body {
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
-  line-height: 1.6;
+html, body {
   background: var(--bg);
-  color: var(--fg);
-  font-size: 16px;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-main {
-  max-width: 1200px;
-  margin: 0 auto;
-  padding: 2rem;
+  color: var(--text-primary);
+  font-family: "More Thin", sans-serif;
+  line-height: 1.6;
+  transition: background 0.2s, color 0.2s;
 }
 
 a {
-  color: var(--link);
+  color: var(--accent);
   text-decoration: none;
 }
 
 a:hover {
-  color: var(--link-hover);
   text-decoration: underline;
 }
 
-h1, h2, h3, h4, h5, h6 {
-  margin-bottom: 1rem;
-  font-weight: 600;
-  line-height: 1.25;
-}
-
-h1 { font-size: 2rem; }
-h2 { font-size: 1.75rem; }
-h3 { font-size: 1.5rem; }
-h4 { font-size: 1.25rem; }
-h5 { font-size: 1.1rem; }
-h6 { font-size: 1rem; }
-
-p {
-  margin-bottom: 1rem;
-}
-
 code {
-  background: var(--code-bg);
+  background: var(--bg-code);
   padding: 0.2em 0.4em;
   border-radius: 3px;
-  font-family: 'Monaco', 'Courier New', monospace;
   font-size: 0.9em;
 }
 
 pre {
-  background: var(--code-bg);
+  background: var(--bg-code);
   padding: 1rem;
   border-radius: 6px;
   overflow-x: auto;
-  margin-bottom: 1rem;
 }
 
 pre code {
@@ -125,17 +171,18 @@
   padding: 0;
 }
 
-/* Mobile responsive */
-@media (max-width: 768px) {
-  body {
-    font-size: 14px;
-  }
+/* Icon invert for dark mode */
+:root.dark .icon-invert {
+  filter: invert(0.8);
+}
 
-  main {
-    padding: 1rem;
-  }
+:root.light .icon-invert,
+:root:not(.dark):not(.light) .icon-invert {
+  filter: none;
+}
 
-  h1 { font-size: 1.75rem; }
-  h2 { font-size: 1.5rem; }
-  h3 { font-size: 1.25rem; }
+@media (prefers-color-scheme: dark) {
+  :root:not(.light) .icon-invert {
+    filter: invert(0.8);
+  }
 }
--- a/hg-web/src/build.ts	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-import { readdir } from "node:fs/promises";
-const files = await readdir(import.meta.dir);
-console.log(files);
-
-
-const outputPath = Bun.argv[2];
-
-if (!outputPath) {
-  console.error("Please provide an output path. Usage: bun build.ts <output_path>");
-  process.exit(1);
-}
-
-const build = await Bun.build({
-  entrypoints: ["./hg-web/src/main.tsx"], 
-  outdir: outputPath,
-  metafile: true, 
-});
-
-if (build.success) {
-  console.log(`Build successful! Files saved to: ${outputPath}`);
-  console.log(JSON.stringify(build.metafile, null, 2));
-} else {
-  console.error("Build failed:", build.logs);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/app.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,725 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { Graph, useGraphData } from "hg-web/src/components/graph";
+import { DirectoryBrowser } from "hg-web/src/components/directory-browser";
+import { Header } from "hg-web/src/components/header";
+import { Footer } from "hg-web/src/components/footer";
+import { ThemeProvider, useTheme } from "hg-web/src/components/theme";
+
+type Page = 'landing' | 'graph' | 'directory' | 'changeset';
+
+type RouteState = {
+  page: Page;
+  graphCommit?: string;
+  graphTip?: string;
+  dirPath?: string;
+  changesetId?: string;
+  returnDepth?: number;
+}
+
+type ChangesetDetail = {
+  node: string;
+  date: [number, number];
+  desc: string;
+  branch: string;
+  bookmarks: string[];
+  tags: string[];
+  user: string;
+  parents: string[];
+  files: Array<{
+    file: string;
+    status: string;
+  }>;
+  diff: Array<{
+    blockno: number;
+    lines: Array<{ t: string; n: number; l: string }>;
+  }>;
+};
+
+type DiffLine = ChangesetDetail['diff'][number]['lines'][number];
+
+type DiffCell = {
+  lineNumber: number | null;
+  text: string;
+  kind: 'context' | 'add' | 'remove' | 'meta';
+};
+
+type SideBySideRow = {
+  left?: DiffCell;
+  right?: DiffCell;
+  range?: string;
+};
+
+function trimDiffLine(line: string): string {
+  return line.endsWith('\n') ? line.slice(0, -1) : line;
+}
+
+function contentDiffLine(line: DiffLine): string {
+  const text = trimDiffLine(line.l);
+  if ((line.t === '+' || line.t === '-' || line.t === '' || line.t === ' ') &&
+      text.startsWith(line.t || ' ')) {
+    return text.slice(1);
+  }
+  return text;
+}
+
+function buildSideBySideRows(lines: DiffLine[]): SideBySideRow[] {
+  const rows: SideBySideRow[] = [];
+  let removals: DiffCell[] = [];
+  let additions: DiffCell[] = [];
+  let oldLine: number | null = null;
+  let newLine: number | null = null;
+  let inHunk = false;
+
+  const flushChanges = () => {
+    const count = Math.max(removals.length, additions.length);
+    for (let index = 0; index < count; index++) {
+      rows.push({ left: removals[index], right: additions[index] });
+    }
+    removals = [];
+    additions = [];
+  };
+
+  for (const line of lines) {
+    if (line.t === '@') {
+      flushChanges();
+      const range = trimDiffLine(line.l);
+      const match = range.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
+      oldLine = match ? Number(match[1]) : null;
+      newLine = match ? Number(match[2]) : null;
+      inHunk = true;
+      rows.push({ range });
+      continue;
+    }
+
+    if (!inHunk && (line.t === '-' || line.t === '+')) {
+      const cell: DiffCell = {
+        lineNumber: null,
+        text: trimDiffLine(line.l),
+        kind: 'meta',
+      };
+      if (line.t === '-') removals.push(cell);
+      else additions.push(cell);
+      continue;
+    }
+
+    if (line.t === '-') {
+      removals.push({
+        lineNumber: oldLine,
+        text: contentDiffLine(line),
+        kind: 'remove',
+      });
+      if (oldLine !== null) oldLine++;
+      continue;
+    }
+
+    if (line.t === '+') {
+      additions.push({
+        lineNumber: newLine,
+        text: contentDiffLine(line),
+        kind: 'add',
+      });
+      if (newLine !== null) newLine++;
+      continue;
+    }
+
+    flushChanges();
+    const text = contentDiffLine(line);
+    rows.push({
+      left: { lineNumber: oldLine, text, kind: 'context' },
+      right: { lineNumber: newLine, text, kind: 'context' },
+    });
+    if (oldLine !== null) oldLine++;
+    if (newLine !== null) newLine++;
+  }
+
+  flushChanges();
+  return rows;
+}
+
+function diffBlockFilename(
+  block: ChangesetDetail['diff'][number],
+  fallback?: string,
+): string {
+  if (fallback) return fallback;
+  const newFileHeader = block.lines.find(
+    line => line.t === '+' && line.l.startsWith('+++ '),
+  );
+  if (!newFileHeader) return `Diff block ${block.blockno}`;
+  return trimDiffLine(newFileHeader.l).replace(/^\+\+\+ (?:b\/)?/, '').split('\t')[0];
+}
+
+function SideBySideDiff({
+  block,
+  filename,
+}: {
+  block: ChangesetDetail['diff'][number];
+  filename: string;
+}) {
+  const rows = buildSideBySideRows(block.lines);
+  return (
+    <div className="side-by-side-diff">
+      <div className="diff-file-header">{filename}</div>
+      <div className="diff-column-headings">
+        <span>Before</span>
+        <span>After</span>
+      </div>
+      <div className="diff-grid">
+        {rows.map((row, index) => (
+          row.range ? (
+            <div className="diff-range-row" key={`range-${index}`}>{row.range}</div>
+          ) : (
+            <React.Fragment key={`row-${index}`}>
+              <span className={`diff-side-number diff-${row.left?.kind || 'empty'}`}>
+                {row.left?.lineNumber ?? ''}
+              </span>
+              <code className={`diff-side-code diff-left diff-${row.left?.kind || 'empty'}`}>
+                {row.left?.text ?? ''}
+              </code>
+              <span className={`diff-side-number diff-column-divider diff-${row.right?.kind || 'empty'}`}>
+                {row.right?.lineNumber ?? ''}
+              </span>
+              <code className={`diff-side-code diff-right diff-${row.right?.kind || 'empty'}`}>
+                {row.right?.text ?? ''}
+              </code>
+            </React.Fragment>
+          )
+        ))}
+      </div>
+    </div>
+  );
+}
+
+// Icons
+const ICONS = {
+  folder: "/icons/folder.png",
+};
+
+const GraphIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+    <circle cx="6" cy="6" r="3"/>
+    <circle cx="6" cy="18" r="3"/>
+    <circle cx="18" cy="12" r="3"/>
+    <line x1="6" y1="9" x2="6" y2="15"/>
+    <path d="M8.5 7.5L15.5 11"/>
+  </svg>
+);
+
+const FolderIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
+    <path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
+  </svg>
+);
+
+const API_BASE = '/api/repo';
+
+function parseRoute(): RouteState {
+  const params = new URLSearchParams(window.location.search);
+  const pathname = window.location.pathname;
+  const changesetMatch = pathname.match(/^\/changeset\/([^/]+)$/);
+
+  if (changesetMatch) {
+    try {
+      const changesetId = decodeURIComponent(changesetMatch[1]);
+      if (/^(?:[0-9a-f]{1,40}|tip)$/i.test(changesetId)) {
+        return { page: 'changeset', changesetId };
+      }
+    } catch {
+      return { page: 'landing' };
+    }
+  }
+
+  if (pathname.startsWith('/graph') || params.has('graph')) {
+    return {
+      page: 'graph',
+      graphCommit: params.get('commit') || undefined,
+      graphTip: params.get('tip') || undefined,
+    };
+  }
+
+  if (pathname.startsWith('/directory') || params.has('path')) {
+    return {
+      page: 'directory',
+      dirPath: params.get('path') || '',
+    };
+  }
+
+  return { page: 'landing' };
+}
+
+function buildUrl(state: RouteState): string {
+  const params = new URLSearchParams();
+
+  switch (state.page) {
+    case 'graph':
+      if (state.graphCommit) params.set('commit', state.graphCommit);
+      if (state.graphTip) params.set('tip', state.graphTip);
+      return `/graph${params.toString() ? '?' + params.toString() : ''}`;
+    case 'directory':
+      if (state.dirPath) params.set('path', state.dirPath);
+      return `/directory${params.toString() ? '?' + params.toString() : ''}`;
+    case 'changeset':
+      return state.changesetId ? `/changeset/${encodeURIComponent(state.changesetId)}` : '/graph';
+    default:
+      return '/';
+  }
+}
+
+function isRouteState(value: unknown): value is RouteState {
+  if (!value || typeof value !== 'object' || !('page' in value)) return false;
+  return ['landing', 'graph', 'directory', 'changeset'].includes(
+    String((value as { page: unknown }).page),
+  );
+}
+
+// Landing Page Component
+function LandingPage({
+  onNavigateToGraph,
+  onNavigateToDirectory,
+  onNavigateToChangeset,
+}: {
+  onNavigateToGraph: () => void;
+  onNavigateToDirectory: (path?: string) => void;
+  onNavigateToChangeset: (node: string) => void;
+}) {
+  const [directories, setDirectories] = useState<any[]>([]);
+  const [files, setFiles] = useState<any[]>([]);
+  const [dirLoading, setDirLoading] = useState(true);
+
+  const { data: graphData, loading: graphLoading } = useGraphData();
+
+  useEffect(() => {
+    fetch(`${API_BASE}/list`)
+      .then(r => r.json())
+      .then(data => {
+        setDirectories(data.directories || []);
+        setFiles(data.files || []);
+        setDirLoading(false);
+      })
+      .catch(() => setDirLoading(false));
+  }, []);
+
+  const previewItems = [
+    ...directories.slice(0, 6),
+    ...files.slice(0, Math.max(0, 6 - directories.length))
+  ].slice(0, 6);
+
+  return (
+    <div className="landing-grid">
+      {/* Graph Preview */}
+      <div className="landing-section">
+        <div className="landing-section-header">
+          <span className="landing-section-title">
+            <GraphIcon />
+            Recent Commits
+          </span>
+          <a href="/graph" className="landing-section-link" onClick={(e) => {
+            e.preventDefault();
+            onNavigateToGraph();
+          }}>
+            View all
+          </a>
+        </div>
+        <div className="landing-section-content">
+          {graphLoading ? (
+            <div className="loading-state">Loading commits...</div>
+          ) : graphData ? (
+            <Graph
+              data={graphData}
+              maxRows={8}
+              onCommitClick={onNavigateToChangeset}
+            />
+          ) : (
+            <div className="empty-state">Failed to load commits</div>
+          )}
+        </div>
+      </div>
+
+      {/* Directory Preview */}
+      <div className="landing-section">
+        <div className="landing-section-header">
+          <span className="landing-section-title">
+            <FolderIcon />
+            Repository Files
+          </span>
+          <a href="/directory" className="landing-section-link" onClick={(e) => {
+            e.preventDefault();
+            onNavigateToDirectory();
+          }}>
+            Browse all
+          </a>
+        </div>
+        <div className="landing-section-content">
+          {dirLoading ? (
+            <div className="loading-state">Loading files...</div>
+          ) : previewItems.length > 0 ? (
+            previewItems.map((item) => (
+              <div
+                key={item.abspath}
+                className="dir-item"
+                onClick={() => onNavigateToDirectory(item.abspath)}
+              >
+                <span className="dir-item-icon">
+                  <img
+                    className="icon-invert"
+                    src={directories.includes(item) ? ICONS.folder : "/icons/file.svg"}
+                    alt=""
+                  />
+                </span>
+                <span className="dir-item-name">{item.basename}</span>
+              </div>
+            ))
+          ) : (
+            <div className="empty-state">No files found</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+// Graph Page Component
+function GraphPage({
+  onBack,
+  initialCommit,
+  initialTip,
+  onOpenChangeset,
+}: {
+  onBack: () => void;
+  initialCommit?: string;
+  initialTip?: string;
+  onOpenChangeset: (node: string) => void;
+}) {
+  const { data, loading, error, loadMore, hasMore, tip, currentCommit } = useGraphData({
+    initialCommit: initialCommit || null,
+    graphTop: initialTip || null,
+  });
+
+  useEffect(() => {
+    if (tip && currentCommit) {
+      const params = new URLSearchParams();
+      params.set('commit', currentCommit);
+      params.set('tip', tip);
+      const newUrl = `/graph?${params.toString()}`;
+      window.history.replaceState({ page: 'graph', graphCommit: currentCommit, graphTip: tip }, '', newUrl);
+    }
+  }, [currentCommit, tip]);
+
+  return (
+    <div>
+      <div className="page-header">
+        <button className="back-button" onClick={onBack} aria-label="Back">
+          &larr; Back
+        </button>
+        <span className="page-title">Commit Graph</span>
+      </div>
+
+      {tip && (
+        <div className="graph-params">
+          <span className="graph-param">
+            <span className="graph-param-label">Tip:</span>
+            <span className="graph-param-value">{tip.substring(0, 12)}</span>
+          </span>
+          {currentCommit && currentCommit !== tip && (
+            <span className="graph-param">
+              <span className="graph-param-label">Current:</span>
+              <span className="graph-param-value">{currentCommit.substring(0, 12)}</span>
+            </span>
+          )}
+        </div>
+      )}
+
+      {error && (
+        <div className="error-message">Error: {error}</div>
+      )}
+
+      <Graph
+        data={data}
+        loading={loading}
+        hasMore={hasMore}
+        onLoadMore={loadMore}
+        onCommitClick={onOpenChangeset}
+      />
+    </div>
+  );
+}
+
+function ChangesetPage({
+  changesetId,
+  onBack,
+  onOpenChangeset,
+}: {
+  changesetId: string;
+  onBack: () => void;
+  onOpenChangeset: (node: string) => void;
+}) {
+  const [changeset, setChangeset] = useState<ChangesetDetail | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState<string | null>(null);
+
+  useEffect(() => {
+    const controller = new AbortController();
+
+    setLoading(true);
+    setError(null);
+    setChangeset(null);
+
+    fetch(`/api/changeset/${encodeURIComponent(changesetId)}`, { signal: controller.signal })
+      .then(async response => {
+        if (!response.ok) {
+          const message = await response.text();
+          throw new Error(message || `Unable to load changeset (${response.status})`);
+        }
+        return response.json() as Promise<ChangesetDetail>;
+      })
+      .then(setChangeset)
+      .catch(err => {
+        if (err.name !== 'AbortError') setError(err.message);
+      })
+      .finally(() => {
+        if (!controller.signal.aborted) setLoading(false);
+      });
+
+    return () => controller.abort();
+  }, [changesetId]);
+
+  return (
+    <div>
+      <div className="page-header">
+        <button className="back-button" onClick={onBack} aria-label="Back">
+          &larr; Back
+        </button>
+        <span className="page-title">Changeset</span>
+      </div>
+
+      {loading && <div className="loading-state">Loading changeset...</div>}
+      {error && <div className="error-message">Error: {error}</div>}
+
+      {changeset && (
+        <article className="changeset-paper">
+          <div className="changeset-heading">
+            <code>{changeset.node}</code>
+            <span className="changeset-branch">{changeset.branch}</span>
+          </div>
+          <h2>{changeset.desc}</h2>
+          <div className="changeset-meta">
+            <span>{changeset.user}</span>
+            <time dateTime={new Date(changeset.date[0] * 1000).toISOString()}>
+              {new Date(changeset.date[0] * 1000).toLocaleString()}
+            </time>
+          </div>
+
+          {(changeset.bookmarks.length > 0 || changeset.tags.length > 0) && (
+            <div className="changeset-labels">
+              {changeset.bookmarks.map(bookmark => <span key={`bookmark-${bookmark}`}>{bookmark}</span>)}
+              {changeset.tags.map(tag => <span key={`tag-${tag}`}>{tag}</span>)}
+            </div>
+          )}
+
+          {changeset.parents.length > 0 && (
+            <div className="changeset-parents">
+              <strong>Parents</strong>
+              {changeset.parents.map(parent => (
+                <button type="button" key={parent} onClick={() => onOpenChangeset(parent)}>
+                  {parent.substring(0, 12)}
+                </button>
+              ))}
+            </div>
+          )}
+
+          {changeset.files.length > 0 && (
+            <div className="changeset-files">
+              <strong>Files</strong>
+              {changeset.files.map(file => (
+                <code key={file.file}>
+                  <span className={`changeset-file-status status-${file.status}`}>
+                    {file.status}
+                  </span>
+                  {file.file}
+                </code>
+              ))}
+            </div>
+          )}
+
+          <section className="changeset-diff" aria-label="Changeset diff">
+            <h3>Diff</h3>
+            {changeset.diff.length === 0 ? (
+              <div className="empty-state">No textual changes in this changeset.</div>
+            ) : changeset.diff.map((block, index) => (
+              <SideBySideDiff
+                key={block.blockno}
+                block={block}
+                filename={diffBlockFilename(block, changeset.files[index]?.file)}
+              />
+            ))}
+          </section>
+        </article>
+      )}
+    </div>
+  );
+}
+
+// Directory Page Component
+function DirectoryPage({
+  onBack,
+  initialPath,
+  onPathChange,
+}: {
+  onBack: () => void;
+  initialPath?: string;
+  onPathChange: (path: string) => void;
+}) {
+  return (
+    <div>
+      <div className="page-header">
+        <button className="back-button" onClick={onBack} aria-label="Back">
+          &larr; Back
+        </button>
+        <span className="page-title">Repository Files</span>
+      </div>
+
+      <DirectoryBrowser
+        initialPath={initialPath}
+        onPathChange={onPathChange}
+      />
+    </div>
+  );
+}
+
+// Main App Content (uses theme context)
+function AppContent() {
+  const [route, setRoute] = useState<RouteState>(parseRoute);
+  const { isDark, toggleTheme } = useTheme();
+
+  // Handle browser back/forward
+  useEffect(() => {
+    const handlePopState = (event: PopStateEvent) => {
+      setRoute(isRouteState(event.state) ? event.state : parseRoute());
+    };
+    window.addEventListener('popstate', handlePopState);
+    return () => window.removeEventListener('popstate', handlePopState);
+  }, []);
+
+  const navigate = useCallback((newRoute: RouteState) => {
+    const url = buildUrl(newRoute);
+    window.history.pushState(newRoute, '', url);
+    setRoute(newRoute);
+  }, []);
+
+  const navigateToLanding = useCallback(() => {
+    navigate({ page: 'landing' });
+  }, [navigate]);
+
+  const navigateToGraph = useCallback((commit?: string, tip?: string) => {
+    navigate({ page: 'graph', graphCommit: commit, graphTip: tip });
+  }, [navigate]);
+
+  const navigateToDirectory = useCallback((path?: string) => {
+    navigate({ page: 'directory', dirPath: path || '' });
+  }, [navigate]);
+
+  const navigateToChangeset = useCallback((changesetId: string) => {
+    const returnDepth = route.page === 'changeset'
+      ? (route.returnDepth ?? 0) + 1
+      : 1;
+    navigate({ page: 'changeset', changesetId, returnDepth });
+  }, [navigate, route.page, route.returnDepth]);
+
+  const navigateBackFromChangeset = useCallback(() => {
+    if (route.returnDepth !== undefined && route.returnDepth > 0) {
+      window.history.go(-route.returnDepth);
+      return;
+    }
+    navigateToGraph();
+  }, [navigateToGraph, route.returnDepth]);
+
+  const handleDirectoryPathChange = useCallback((path: string) => {
+    // Update URL without full navigation
+    const params = new URLSearchParams();
+    if (path) params.set('path', path);
+    const newUrl = `/directory${params.toString() ? '?' + params.toString() : ''}`;
+    window.history.replaceState({ page: 'directory', dirPath: path }, '', newUrl);
+    setRoute(prev => ({ ...prev, dirPath: path }));
+  }, []);
+
+  return (
+    <div className="app-container">
+      <Header
+        title="Zenbu Repository"
+        showThemeToggle={true}
+        isDark={isDark}
+        onToggleTheme={toggleTheme}
+      />
+
+      {/* Navigation Tabs */}
+      <div className="nav-tabs">
+        <button
+          className={`nav-tab ${route.page === 'landing' ? 'active' : ''}`}
+          onClick={navigateToLanding}
+        >
+          Home
+        </button>
+        <button
+          className={`nav-tab ${route.page === 'graph' || route.page === 'changeset' ? 'active' : ''}`}
+          onClick={() => navigateToGraph()}
+        >
+          <GraphIcon />
+          Graph
+        </button>
+        <button
+          className={`nav-tab ${route.page === 'directory' ? 'active' : ''}`}
+          onClick={() => navigateToDirectory()}
+        >
+          <FolderIcon />
+          Files
+        </button>
+      </div>
+
+      {/* Page Content */}
+      {route.page === 'landing' && (
+        <LandingPage
+          onNavigateToGraph={() => navigateToGraph()}
+          onNavigateToDirectory={navigateToDirectory}
+          onNavigateToChangeset={navigateToChangeset}
+        />
+      )}
+
+      {route.page === 'graph' && (
+        <GraphPage
+          onBack={navigateToLanding}
+          initialCommit={route.graphCommit}
+          initialTip={route.graphTip}
+          onOpenChangeset={navigateToChangeset}
+        />
+      )}
+
+      {route.page === 'directory' && (
+        <DirectoryPage
+          onBack={navigateToLanding}
+          initialPath={route.dirPath}
+          onPathChange={handleDirectoryPathChange}
+        />
+      )}
+
+      {route.page === 'changeset' && route.changesetId && (
+        <ChangesetPage
+          changesetId={route.changesetId}
+          onBack={navigateBackFromChangeset}
+          onOpenChangeset={navigateToChangeset}
+        />
+      )}
+
+      <Footer />
+    </div>
+  );
+}
+
+// App wrapper with ThemeProvider
+function App() {
+  return (
+    <ThemeProvider>
+      <AppContent />
+    </ThemeProvider>
+  );
+}
+
+export { App };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/directory-browser.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,649 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import createMarkdownModule from 'markdown_converter/markdown_to_html_wasm/markdown_to_html_bin.js';
+import hljs from 'third_party/highlight/highlight.min.js';
+
+// --- ICONS (served as static files) ---
+const ICONS = {
+  folder: "/icons/folder.png",
+  file: "/icons/file.svg",
+  close: "/icons/close.png"
+};
+
+const API_BASE = '/api/repo';
+
+// File extensions that should be displayed as code
+const CODE_EXTENSIONS = new Set([
+  'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp',
+  'cs', 'go', 'rs', 'swift', 'kt', 'scala', 'php', 'pl', 'sh', 'bash',
+  'zsh', 'fish', 'ps1', 'bat', 'cmd', 'html', 'htm', 'css', 'scss',
+  'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg',
+  'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue',
+  'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake',
+  'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample',
+  'bzl', 'bazel'
+]);
+
+const BAZEL_FILENAMES = new Set([
+  'build', 'build.bazel', 'module.bazel', 'workspace', 'workspace.bazel'
+]);
+
+type StaticPreviewKind = 'image' | 'video' | 'audio' | 'pdf';
+
+const IMAGE_EXTENSIONS = new Set([
+  'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'bmp', 'ico', 'svg'
+]);
+const VIDEO_EXTENSIONS = new Set(['mp4', 'm4v', 'webm', 'mov', 'ogv']);
+const AUDIO_EXTENSIONS = new Set(['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac']);
+
+// Prefetch cache
+const prefetchCache = new Map<string, Promise<any>>();
+
+function isCodeFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  const basename = filename.toLowerCase();
+  return CODE_EXTENSIONS.has(ext) ||
+         CODE_EXTENSIONS.has(basename) ||
+         BAZEL_FILENAMES.has(basename) ||
+         basename === 'dockerfile' ||
+         basename === 'makefile' ||
+         basename.startsWith('.');
+}
+
+function isMarkdownFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  return ext === 'md' || ext === 'markdown';
+}
+
+function getStaticPreviewKind(filename: string): StaticPreviewKind | null {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  if (IMAGE_EXTENSIONS.has(ext)) return 'image';
+  if (VIDEO_EXTENSIONS.has(ext)) return 'video';
+  if (AUDIO_EXTENSIONS.has(ext)) return 'audio';
+  if (ext === 'pdf') return 'pdf';
+  return null;
+}
+
+function prefetchDirectory(path: string): void {
+  const cacheKey = `dir:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  const url = path
+    ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+    : `${API_BASE}/list`;
+
+  prefetchCache.set(cacheKey, fetch(url).then(r => r.json()).catch(() => null));
+}
+
+function prefetchFile(path: string): void {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  prefetchCache.set(cacheKey,
+    fetch(`${API_BASE}/file?path=${encodeURIComponent(path)}`)
+      .then(r => r.ok ? r.text() : null)
+      .catch(() => null)
+  );
+}
+
+async function getCachedFile(path: string): Promise<string | null> {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) {
+    return prefetchCache.get(cacheKey);
+  }
+  prefetchFile(path);
+  return prefetchCache.get(cacheKey)!;
+}
+
+/**
+ * Component: Breadcrumb
+ */
+function Breadcrumb({ currentPath, onNavigate }: { currentPath: string; onNavigate: (path: string) => void }) {
+  if (!currentPath) {
+    return (
+      <nav className="breadcrumb">
+        <span className="nav-item active">root</span>
+      </nav>
+    );
+  }
+
+  const parts = currentPath.split('/').filter(p => p);
+  const crumbs = parts.map((part, index) => ({
+    name: part,
+    fullPath: parts.slice(0, index + 1).join('/')
+  }));
+
+  return (
+    <nav className="breadcrumb">
+      <a
+        href="#"
+        onClick={(e) => { e.preventDefault(); onNavigate(''); }}
+        title="Go to Root"
+      >
+        root
+      </a>
+      {crumbs.map((crumb, index) => {
+        const isLast = index === crumbs.length - 1;
+        return (
+          <React.Fragment key={crumb.fullPath}>
+            <span className="separator">/</span>
+            {isLast ? (
+              <span className="nav-item active">{crumb.name}</span>
+            ) : (
+              <a
+                href="#"
+                onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }}
+              >
+                {crumb.name}
+              </a>
+            )}
+          </React.Fragment>
+        );
+      })}
+    </nav>
+  );
+}
+
+/**
+ * Component: FileViewer
+ * Shows file content inline with syntax highlighting
+ */
+function FileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const codeRef = useRef<HTMLElement>(null);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (content && codeRef.current) {
+      hljs.highlightElement(codeRef.current);
+    }
+  }, [content]);
+
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  const getLanguage = () => {
+    const ext = filename.split('.').pop()?.toLowerCase() || '';
+    const basename = filename.toLowerCase();
+    if (BAZEL_FILENAMES.has(basename) || ext === 'bzl' || ext === 'bazel') {
+      return 'python';
+    }
+    const langMap: Record<string, string> = {
+      js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
+      py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
+      c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
+      sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
+      json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
+      html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss',
+      sql: 'sql', md: 'markdown', markdown: 'markdown', xml: 'xml',
+      dockerfile: 'dockerfile', makefile: 'makefile'
+    };
+    return langMap[ext] || 'plaintext';
+  };
+
+  const addLineNumbers = (text: string) => {
+    const lines = text.split('\n');
+    return lines.map((_, i) => i + 1).join('\n');
+  };
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <pre style={{ display: 'flex' }}>
+              <span className="file-viewer-line-numbers">{addLineNumbers(content)}</span>
+              <code ref={codeRef} className={`language-${getLanguage()}`}>{content}</code>
+            </pre>
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: MarkdownViewerModal
+ */
+function MarkdownViewerModal({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading || !wasmReady ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <div className="readme-content" ref={contentRef} />
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function StaticFileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const filename = filePath.split('/').pop() || filePath;
+  const previewKind = getStaticPreviewKind(filename);
+  const fileUrl = `${API_BASE}/file?path=${encodeURIComponent(filePath)}`;
+  const [failed, setFailed] = useState(false);
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div
+        className="file-viewer static-file-viewer"
+        role="dialog"
+        aria-modal="true"
+        aria-label={`Preview ${filename}`}
+        onClick={(event) => event.stopPropagation()}
+      >
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <span className="file-viewer-actions">
+            <a href={fileUrl} download={filename}>Download</a>
+            <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+              <img className="icon-invert" src={ICONS.close} alt="Close" />
+            </button>
+          </span>
+        </div>
+        <div className="file-viewer-content static-file-preview">
+          {failed && <div className="error-message">Unable to preview this file.</div>}
+          {!failed && previewKind === 'image' && (
+            <img
+              className="static-file-image"
+              src={fileUrl}
+              alt={filename}
+              onError={() => setFailed(true)}
+            />
+          )}
+          {!failed && previewKind === 'video' && (
+            <video
+              className="static-file-video"
+              src={fileUrl}
+              controls
+              onError={() => setFailed(true)}
+            />
+          )}
+          {!failed && previewKind === 'audio' && (
+            <audio
+              className="static-file-audio"
+              src={fileUrl}
+              controls
+              onError={() => setFailed(true)}
+            />
+          )}
+          {!failed && previewKind === 'pdf' && (
+            <iframe
+              className="static-file-pdf"
+              src={fileUrl}
+              title={filename}
+              onError={() => setFailed(true)}
+            />
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileList
+ */
+function FileList({ directories, files, onNavigate, onOpenFile }: {
+  directories: any[];
+  files: any[];
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const isEmpty = directories.length === 0 && files.length === 0;
+
+  if (isEmpty) {
+    return (
+      <div className="file-list-container">
+        <div className="empty-state">This directory is empty.</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="file-list-container">
+      <div className="file-header">Files</div>
+      <div id="fileListBody">
+        {directories.map((dir) => (
+          <FileRow
+            key={dir.abspath}
+            item={dir}
+            iconUrl={ICONS.folder}
+            isDir={true}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+        {files.map((file) => (
+          <FileRow
+            key={file.abspath}
+            item={file}
+            iconUrl={ICONS.file}
+            isDir={false}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileRow
+ */
+function FileRow({ item, iconUrl, isDir, onNavigate, onOpenFile }: {
+  item: { abspath: string; basename: string };
+  iconUrl: string;
+  isDir: boolean;
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const handleClick = (e: React.MouseEvent) => {
+    e.preventDefault();
+    if (isDir) {
+      onNavigate(item.abspath);
+    } else if (isCodeFile(item.basename) || getStaticPreviewKind(item.basename)) {
+      onOpenFile(item.abspath);
+    } else {
+      window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank');
+    }
+  };
+
+  const handleMouseEnter = () => {
+    if (isDir) {
+      prefetchDirectory(item.abspath);
+    } else if (isCodeFile(item.basename)) {
+      prefetchFile(item.abspath);
+    }
+  };
+
+  const href = isDir
+    ? `#`
+    : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`;
+
+  return (
+    <div className="file-row" onMouseEnter={handleMouseEnter}>
+      <span className="icon">
+        <img className="icon-invert" src={iconUrl} alt={isDir ? "Directory" : "File"} />
+      </span>
+      <span className="name">
+        <a href={href} onClick={handleClick}>
+          {item.basename}
+        </a>
+      </span>
+    </div>
+  );
+}
+
+/**
+ * Component: ReadmeViewer
+ */
+function ReadmeViewer({ content }: { content: string | null }) {
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  if (!content) return null;
+
+  return (
+    <div className="readme-section">
+      <div className="readme-header">
+        <img className="icon-invert" src={ICONS.file} width="16" alt="" style={{ opacity: 0.5 }} />
+        README.md
+      </div>
+      <div className="readme-content" ref={contentRef}>
+        {!wasmReady && 'Loading...'}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Directory Browser Component (no header/footer - for embedding in app)
+ */
+interface DirectoryBrowserProps {
+  initialPath?: string;
+  onPathChange?: (path: string) => void;
+}
+
+function DirectoryBrowser({ initialPath = '', onPathChange }: DirectoryBrowserProps) {
+  const [currentPath, setCurrentPath] = useState(initialPath);
+  const [content, setContent] = useState<{ files: any[]; directories: any[] }>({ files: [], directories: [] });
+  const [readme, setReadme] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [viewingFile, setViewingFile] = useState<string | null>(null);
+  const requestGeneration = useRef(0);
+
+  // Sync with initialPath prop
+  useEffect(() => {
+    setCurrentPath(initialPath);
+  }, [initialPath]);
+
+  useEffect(() => {
+    const generation = ++requestGeneration.current;
+    fetchDirectory(currentPath, generation);
+    fetchReadme(currentPath, generation);
+  }, [currentPath]);
+
+  const navigate = useCallback((path: string) => {
+    setCurrentPath(path);
+    onPathChange?.(path);
+  }, [onPathChange]);
+
+  const fetchDirectory = async (path: string, generation: number) => {
+    setLoading(true);
+    setError(null);
+    try {
+      const cacheKey = `dir:${path}`;
+      let data;
+      if (prefetchCache.has(cacheKey)) {
+        data = await prefetchCache.get(cacheKey);
+        prefetchCache.delete(cacheKey);
+      } else {
+        const url = path
+          ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+          : `${API_BASE}/list`;
+        const response = await fetch(url);
+        if (response.ok) {
+          data = await response.json();
+        }
+      }
+
+      if (data?.error) {
+        throw new Error(data.error);
+      }
+
+      if (generation === requestGeneration.current) {
+        setContent({
+          files: data?.files || [],
+          directories: data?.directories || []
+        });
+      }
+    } catch (err: any) {
+      console.error('Error loading directory:', err);
+      if (generation === requestGeneration.current) setError(err.message);
+    } finally {
+      if (generation === requestGeneration.current) setLoading(false);
+    }
+  };
+
+  const fetchReadme = async (path: string, generation: number) => {
+    if (generation === requestGeneration.current) setReadme(null);
+    try {
+      const url = path
+        ? `${API_BASE}/readme?path=${encodeURIComponent(path)}`
+        : `${API_BASE}/readme`;
+      const response = await fetch(url);
+      if (response.ok) {
+        const text = await response.text();
+        if (generation === requestGeneration.current) setReadme(text || null);
+      }
+    } catch (err) {
+      // Readme is optional
+    }
+  };
+
+  const handleOpenFile = useCallback((path: string) => {
+    setViewingFile(path);
+  }, []);
+
+  const handleCloseFile = useCallback(() => {
+    setViewingFile(null);
+  }, []);
+
+  return (
+    <>
+      <Breadcrumb currentPath={currentPath} onNavigate={navigate} />
+
+      {error && <div className="error-message">Error: {error}</div>}
+
+      {loading ? (
+        <div className="file-list-container">
+          <div className="loading-state">Loading files...</div>
+        </div>
+      ) : (
+        <>
+          <FileList
+            directories={content.directories}
+            files={content.files}
+            onNavigate={navigate}
+            onOpenFile={handleOpenFile}
+          />
+          <ReadmeViewer content={readme} />
+        </>
+      )}
+
+      {/* File Viewer Modal */}
+      {viewingFile && (
+        isMarkdownFile(viewingFile) ? (
+          <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} />
+        ) : getStaticPreviewKind(viewingFile) ? (
+          <StaticFileViewer filePath={viewingFile} onClose={handleCloseFile} />
+        ) : (
+          <FileViewer filePath={viewingFile} onClose={handleCloseFile} />
+        )
+      )}
+    </>
+  );
+}
+
+export { DirectoryBrowser };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/footer.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,31 @@
+import React from 'react';
+
+interface FooterProps {
+  showCloneUrl?: boolean;
+  cloneUrl?: string;
+}
+
+function Footer({
+  showCloneUrl = false,
+  cloneUrl = "hg clone http://zenbu.babocoder.com/repo",
+}: FooterProps) {
+  const currentYear = new Date().getFullYear();
+
+  return (
+    <footer className="footer">
+      {showCloneUrl && (
+        <div className="clone-box">
+          <div className="clone-box-inner">
+            <span className="clone-label">Clone HTTPS</span>
+            <code className="clone-url">{cloneUrl}</code>
+          </div>
+        </div>
+      )}
+      <div className="footer-content">
+        <span className="footer-text">&copy; 2026 June Park</span>
+      </div>
+    </footer>
+  );
+}
+
+export { Footer };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/graph.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,313 @@
+import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
+
+// Configuration constants for the layout
+const rowHeight = 40;
+const colWidth = 20;
+const nodeRadius = 4.5;
+
+// --- Interfaces ---
+
+interface Changeset {
+  node: string;
+  date: [number, number];
+  desc: string;
+  branch: string;
+  bookmarks: string[];
+  tags: string[];
+  user: string;
+  phase: string;
+  col: number;
+  row: number;
+  color: number;
+  edges: Array<{
+    bcolor: string;
+    col: number;
+    color: number;
+    nextcol: number;
+    width: number;
+  }>;
+  parents: string[];
+}
+
+interface GraphData {
+  node: string;
+  changeset_count: number;
+  changesets: Changeset[];
+}
+
+interface UseGraphDataOptions {
+  initialCommit?: string | null;
+  graphTop?: string | null;
+}
+
+interface UseGraphDataResult {
+  data: GraphData | null;
+  loading: boolean;
+  error: string | null;
+  loadMore: () => void;
+  hasMore: boolean;
+  tip: string | null;
+  currentCommit: string | null;
+}
+
+// --- Hook Logic ---
+
+function useGraphData({ initialCommit = null, graphTop = null }: UseGraphDataOptions = {}): UseGraphDataResult {
+  const [data, setData] = useState<GraphData | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [tip, setTip] = useState<string | null>(graphTop);
+  const [currentCommit, setCurrentCommit] = useState<string | null>(initialCommit);
+  const [hasMore, setHasMore] = useState(true);
+
+  const fetchData = useCallback(async (commit: string | null, tipNode: string | null, append: boolean = false) => {
+    if (loading) return;
+    setLoading(true);
+    setError(null);
+
+    try {
+      const url = !commit 
+        ? `/api/graph/tip?style=json` 
+        : `/api/graph/${commit}?graphtop=${tipNode}&style=json`;
+
+      const response = await fetch(url);
+      if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
+      
+      const result: GraphData = await response.json();
+
+      setData(prev => {
+        if (append && prev) {
+          const existingNodes = new Set(prev.changesets.map(cs => cs.node));
+          const newChangesets = result.changesets.filter(cs => !existingNodes.has(cs.node));
+          
+          // Re-index rows to ensure they increment correctly for the canvas height
+          const startRow = prev.changesets.length;
+          const reindexed = newChangesets.map((cs, idx) => ({
+            ...cs,
+            row: startRow + idx
+          }));
+
+          return {
+            ...result,
+            changesets: [...prev.changesets, ...reindexed]
+          };
+        }
+        return result;
+      });
+
+      if (!tip && !append) setTip(result.node);
+
+      if (result.changesets.length > 0) {
+        const lastNode = result.changesets[result.changesets.length - 1].node;
+        setCurrentCommit(lastNode);
+        setHasMore(result.changesets.length >= 30);
+      } else {
+        setHasMore(false);
+      }
+    } catch (err: any) {
+      setError(err.message);
+    } finally {
+      setLoading(false);
+    }
+  }, [tip, loading]);
+
+  useEffect(() => {
+    fetchData(initialCommit, graphTop, false);
+  }, [initialCommit, graphTop]);
+
+  const loadMore = useCallback(() => {
+    if (!loading && hasMore && currentCommit && tip) {
+      fetchData(currentCommit, tip, true);
+    }
+  }, [loading, hasMore, currentCommit, tip, fetchData]);
+
+  return { data, loading, error, loadMore, hasMore, tip, currentCommit };
+}
+
+// --- Pencil Rendering Logic ---
+
+const drawPencilLine = (
+  ctx: CanvasRenderingContext2D,
+  x1: number, y1: number,
+  x2: number, y2: number,
+  texture: CanvasPattern | null, // Ensure type safety
+  isCurve: boolean = false
+) => {
+  const strokes = 3;
+  ctx.save();
+
+  for (let s = 0; s < strokes; s++) {
+    ctx.beginPath();
+    ctx.strokeStyle = texture;
+    ctx.globalAlpha = 0.2 + (s * 0.2); 
+    ctx.lineWidth = 1.5 - (s * 0.2); // Pencil lines are usually thinner
+
+    // 2. Realistic Jitter: Actually return a random small number
+    const jitter = () => (Math.random() - 0.5) * 1.5;
+
+    ctx.moveTo(x1 + jitter(), y1 + jitter());
+
+    if (isCurve) {
+      const cpY = y1 + (y2 - y1) / 2;
+      ctx.bezierCurveTo(
+        x1 + jitter(), cpY + jitter(), 
+        x2 + jitter(), cpY + jitter(), 
+        x2 + jitter(), y2 + jitter()
+      );
+    } else {
+      ctx.lineTo(x2 + jitter(), y2 + jitter());
+    }
+    
+    ctx.stroke();
+  }
+  ctx.restore();
+}
+
+// --- Main Component ---
+
+interface GraphProps {
+  data: GraphData | null;
+  loading?: boolean;
+  hasMore?: boolean;
+  onLoadMore?: () => void;
+  onCommitClick?: (node: string) => void;
+  maxRows?: number;
+}
+
+const Graph = ({ data, loading, hasMore, onLoadMore, onCommitClick, maxRows }: GraphProps) => {
+  const canvasRef = useRef<HTMLCanvasElement>(null);
+  const containerRef = useRef<HTMLDivElement>(null);
+  const [assetError, setAssetError] = useState<string | null>(null);
+
+  const changesets = useMemo(() => 
+    maxRows && data?.changesets ? data.changesets.slice(0, maxRows) : data?.changesets || [], [data, maxRows]);
+
+  useEffect(() => {
+    const canvas = canvasRef.current;
+    if (!canvas || !changesets.length) return;
+
+    const ctx = canvas.getContext('2d');
+    if (!ctx) return;
+
+    let cancelled = false;
+    let pencilPattern: CanvasPattern | null = null;
+    let loadedAssets = 0;
+    const pencilImage = new Image();
+    const pandaImage = new Image();
+    const dpr = window.devicePixelRatio || 1;
+    const maxCol = Math.max(...changesets.map(cs => cs.col), 0);
+    const canvasWidth = (maxCol + 2) * colWidth;
+    
+    // Scale for high-DPI screens
+    canvas.width = canvasWidth * dpr;
+    canvas.height = changesets.length * rowHeight * dpr;
+    canvas.style.width = `${canvasWidth}px`;
+    canvas.style.height = `${changesets.length * rowHeight}px`;
+    ctx.scale(dpr, dpr);
+    ctx.clearRect(0, 0, canvasWidth, changesets.length * rowHeight);
+
+    const getX = (col: number) => (col + 1) * colWidth;
+    const getY = (row: number) => (row * rowHeight) + (rowHeight / 2);
+
+    const renderCanvas = () => {
+      if (!pencilPattern) return; // Don't draw if the pattern isn't ready
+    
+      // Pass 1: Draw Connecting Edges
+      changesets.forEach((cs, i) => {
+        if (!cs.edges) return;
+        cs.edges.forEach(edge => {
+          const sX = getX(edge.col), sY = getY(i);
+          const eX = getX(edge.nextcol), eY = getY(i + 1);
+          
+          drawPencilLine(ctx, sX, sY, eX, eY, pencilPattern, edge.col !== edge.nextcol);
+        });
+      });
+
+      // Pass 2: Draw Commit Nodes
+      changesets.forEach((cs, i) => {
+        const x = getX(cs.col), y = getY(i);
+        ctx.drawImage(pandaImage, x-10, y-10, 20, 20);
+      });
+    };
+
+    const handleAssetLoad = () => {
+      loadedAssets++;
+      if (loadedAssets !== 2 || cancelled) return;
+      pencilPattern = ctx.createPattern(pencilImage, "repeat");
+      renderCanvas();
+    };
+
+    const handleAssetError = () => {
+      if (!cancelled) setAssetError('Unable to load the graph artwork.');
+    };
+
+    setAssetError(null);
+    pencilImage.onload = handleAssetLoad;
+    pencilImage.onerror = handleAssetError;
+    pandaImage.onload = handleAssetLoad;
+    pandaImage.onerror = handleAssetError;
+    pencilImage.src = "/pencil_lines.png";
+    pandaImage.src = "/panda.png";
+
+    return () => {
+      cancelled = true;
+      pencilImage.onload = null;
+      pencilImage.onerror = null;
+      pandaImage.onload = null;
+      pandaImage.onerror = null;
+    };
+  }, [changesets]);
+
+  // Handle Infinite Scroll via Intersection Observer
+  useEffect(() => {
+    if (!onLoadMore || !hasMore) return;
+    const observer = new IntersectionObserver((entries) => {
+      if (entries[0].isIntersecting && !loading) {
+        onLoadMore();
+      }
+    }, { threshold: 0.1 });
+
+    const sentinel = document.getElementById('infinite-scroll-sentinel');
+    if (sentinel) observer.observe(sentinel);
+    return () => observer.disconnect();
+  }, [onLoadMore, hasMore, loading]);
+
+  return (
+    <div className="graph-container">
+      {assetError && <div className="error-message">{assetError}</div>}
+      <div 
+        ref={containerRef} 
+        className="graph-wrapper"
+      >
+        <div className="graph-canvas-column">
+          <canvas ref={canvasRef} style={{ display: 'block' }} />
+        </div>
+
+        <div className="graph-details-column">
+          {changesets.map((cs) => (
+            <button
+              type="button"
+              key={cs.node}
+              className="graph-row"
+              onClick={() => onCommitClick?.(cs.node)}
+              aria-label={`Open changeset ${cs.node.substring(0, 12)}: ${cs.desc}`}
+            >
+              <span className="graph-row-meta">
+                <span className="graph-hash">{cs.node.substring(0, 12)}</span>
+                <span className="graph-user">{cs.user.split(' <')[0]}</span>
+                {cs.branch && <span className="graph-branch">{cs.branch}</span>}
+              </span>
+              <span className="graph-desc">{cs.desc}</span>
+            </button>
+          ))}
+          <div id="infinite-scroll-sentinel" style={{ height: '50px' }} />
+        </div>
+      </div>
+      
+      {loading && <div className="graph-loading-row">Loading repository history...</div>}
+    </div>
+  );
+};
+
+export { Graph, useGraphData };
+export type { GraphData, Changeset, UseGraphDataResult };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/header.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,60 @@
+import React from 'react';
+
+// Icons
+const ICONS = {
+  repo: "/public/epi_all_colors.svg",
+};
+
+const SunIcon = () => (
+  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+    <circle cx="12" cy="12" r="5"/>
+    <line x1="12" y1="1" x2="12" y2="3"/>
+    <line x1="12" y1="21" x2="12" y2="23"/>
+    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
+    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
+    <line x1="1" y1="12" x2="3" y2="12"/>
+    <line x1="21" y1="12" x2="23" y2="12"/>
+    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
+    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
+  </svg>
+);
+
+const MoonIcon = () => (
+  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
+  </svg>
+);
+
+interface HeaderProps {
+  title?: string;
+  subtitle?: string;
+  showThemeToggle?: boolean;
+  isDark?: boolean;
+  onToggleTheme?: () => void;
+}
+
+function Header({
+  title = "Zenbu Repository",
+  subtitle,
+  showThemeToggle = true,
+  isDark = false,
+  onToggleTheme,
+}: HeaderProps) {
+  return (
+    <header className="header">
+      <img src={ICONS.repo} alt="Zenbu" className="header-icon" />
+      <div className="header-content">
+        <h1><a href="/">{title}</a></h1>
+        {subtitle && <p className="header-subtitle">{subtitle}</p>}
+      </div>
+      {showThemeToggle && onToggleTheme && (
+        <button className="theme-toggle" onClick={onToggleTheme} title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}>
+          {isDark ? <SunIcon /> : <MoonIcon />}
+          <span>{isDark ? 'Light' : 'Dark'}</span>
+        </button>
+      )}
+    </header>
+  );
+}
+
+export { Header };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/repo-browser.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,584 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import createMarkdownModule from 'markdown_converter/markdown_to_html_wasm/markdown_to_html_bin.js';
+import hljs from 'third_party/highlight/highlight.min.js';
+import { Header } from "hg-web/src/components/header";
+import { Footer } from "hg-web/src/components/footer";
+import { ThemeProvider, useTheme } from "hg-web/src/components/theme";
+
+// --- ICONS (served as static files) ---
+const ICONS = {
+  folder: "/icons/folder.png",
+  file: "/icons/file.svg",
+  close: "/icons/close.png"
+};
+
+const API_BASE = '/api/repo';
+
+// File extensions that should be displayed as code
+const CODE_EXTENSIONS = new Set([
+  'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp',
+  'cs', 'go', 'rs', 'swift', 'kt', 'scala', 'php', 'pl', 'sh', 'bash',
+  'zsh', 'fish', 'ps1', 'bat', 'cmd', 'html', 'htm', 'css', 'scss',
+  'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg',
+  'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue',
+  'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake',
+  'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample'
+]);
+
+// Prefetch cache
+const prefetchCache = new Map<string, Promise<any>>();
+
+function isCodeFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  const basename = filename.toLowerCase();
+  return CODE_EXTENSIONS.has(ext) ||
+         CODE_EXTENSIONS.has(basename) ||
+         basename === 'dockerfile' ||
+         basename === 'makefile' ||
+         basename.startsWith('.');
+}
+
+function isMarkdownFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  return ext === 'md' || ext === 'markdown';
+}
+
+function prefetchDirectory(path: string): void {
+  const cacheKey = `dir:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  const url = path
+    ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+    : `${API_BASE}/list`;
+
+  prefetchCache.set(cacheKey, fetch(url).then(r => r.json()).catch(() => null));
+}
+
+function prefetchFile(path: string): void {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  prefetchCache.set(cacheKey,
+    fetch(`${API_BASE}/file?path=${encodeURIComponent(path)}`)
+      .then(r => r.ok ? r.text() : null)
+      .catch(() => null)
+  );
+}
+
+async function getCachedFile(path: string): Promise<string | null> {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) {
+    return prefetchCache.get(cacheKey);
+  }
+  prefetchFile(path);
+  return prefetchCache.get(cacheKey)!;
+}
+
+/**
+ * Component: Breadcrumb
+ */
+function Breadcrumb({ currentPath, onNavigate }: { currentPath: string; onNavigate: (path: string) => void }) {
+  if (!currentPath) {
+    return (
+      <nav className="breadcrumb">
+        <span className="nav-item active">root</span>
+      </nav>
+    );
+  }
+
+  const parts = currentPath.split('/').filter(p => p);
+  const crumbs = parts.map((part, index) => ({
+    name: part,
+    fullPath: parts.slice(0, index + 1).join('/')
+  }));
+
+  return (
+    <nav className="breadcrumb">
+      <a
+        href="/"
+        onClick={(e) => { e.preventDefault(); onNavigate(''); }}
+        title="Go to Root"
+      >
+        root
+      </a>
+      {crumbs.map((crumb, index) => {
+        const isLast = index === crumbs.length - 1;
+        return (
+          <React.Fragment key={crumb.fullPath}>
+            <span className="separator">/</span>
+            {isLast ? (
+              <span className="nav-item active">{crumb.name}</span>
+            ) : (
+              <a
+                href={`?path=${encodeURIComponent(crumb.fullPath)}`}
+                onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }}
+              >
+                {crumb.name}
+              </a>
+            )}
+          </React.Fragment>
+        );
+      })}
+    </nav>
+  );
+}
+
+/**
+ * Component: FileViewer
+ * Shows file content inline with syntax highlighting
+ */
+function FileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const codeRef = useRef<HTMLElement>(null);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (content && codeRef.current) {
+      hljs.highlightElement(codeRef.current);
+    }
+  }, [content]);
+
+  // Close on escape key
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  // Get language from file extension for highlight.js
+  const getLanguage = () => {
+    const ext = filename.split('.').pop()?.toLowerCase() || '';
+    const langMap: Record<string, string> = {
+      js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
+      py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
+      c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
+      sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
+      json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
+      html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss',
+      sql: 'sql', md: 'markdown', markdown: 'markdown', xml: 'xml',
+      dockerfile: 'dockerfile', makefile: 'makefile'
+    };
+    return langMap[ext] || 'plaintext';
+  };
+
+  const addLineNumbers = (text: string) => {
+    const lines = text.split('\n');
+    return lines.map((_, i) => i + 1).join('\n');
+  };
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <pre style={{ display: 'flex' }}>
+              <span className="file-viewer-line-numbers">{addLineNumbers(content)}</span>
+              <code ref={codeRef} className={`language-${getLanguage()}`}>{content}</code>
+            </pre>
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: MarkdownViewerModal
+ * Shows markdown content rendered in a modal
+ */
+function MarkdownViewerModal({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  // Close on escape key
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading || !wasmReady ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <div className="readme-content" ref={contentRef} />
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileList
+ */
+function FileList({ directories, files, onNavigate, onOpenFile }: {
+  directories: any[];
+  files: any[];
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const isEmpty = directories.length === 0 && files.length === 0;
+
+  if (isEmpty) {
+    return (
+      <div className="file-list-container">
+        <div className="empty-state">This directory is empty.</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="file-list-container">
+      <div className="file-header">Files</div>
+
+      <div id="fileListBody">
+        {directories.map((dir) => (
+          <FileRow
+            key={dir.abspath}
+            item={dir}
+            iconUrl={ICONS.folder}
+            isDir={true}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+
+        {files.map((file) => (
+          <FileRow
+            key={file.abspath}
+            item={file}
+            iconUrl={ICONS.file}
+            isDir={false}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileRow
+ */
+function FileRow({ item, iconUrl, isDir, onNavigate, onOpenFile }: {
+  item: { abspath: string; basename: string };
+  iconUrl: string;
+  isDir: boolean;
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const handleClick = (e: React.MouseEvent) => {
+    e.preventDefault();
+    if (isDir) {
+      onNavigate(item.abspath);
+    } else if (isCodeFile(item.basename)) {
+      onOpenFile(item.abspath);
+    } else {
+      window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank');
+    }
+  };
+
+  const handleMouseEnter = () => {
+    if (isDir) {
+      prefetchDirectory(item.abspath);
+    } else if (isCodeFile(item.basename)) {
+      prefetchFile(item.abspath);
+    }
+  };
+
+  const href = isDir
+    ? `?path=${encodeURIComponent(item.abspath)}`
+    : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`;
+
+  return (
+    <div className="file-row" onMouseEnter={handleMouseEnter}>
+      <span className="icon">
+        <img className="icon-invert" src={iconUrl} alt={isDir ? "Directory" : "File"} />
+      </span>
+      <span className="name">
+        <a href={href} onClick={handleClick}>
+          {item.basename}
+        </a>
+      </span>
+    </div>
+  );
+}
+
+/**
+ * Component: ReadmeViewer
+ */
+function ReadmeViewer({ content }: { content: string | null }) {
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  if (!content) return null;
+
+  return (
+    <div className="readme-section">
+      <div className="readme-header">
+        <img className="icon-invert" src={ICONS.file} width="16" alt="" style={{ opacity: 0.5 }} />
+        README.md
+      </div>
+      <div className="readme-content" ref={contentRef}>
+        {!wasmReady && 'Loading...'}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Repository Browser Content (uses theme context)
+ */
+function RepoBrowserContent() {
+  const [currentPath, setCurrentPath] = useState(getCurrentPath());
+  const [content, setContent] = useState<{ files: any[]; directories: any[] }>({ files: [], directories: [] });
+  const [readme, setReadme] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [viewingFile, setViewingFile] = useState<string | null>(null);
+
+  const { isDark, toggleTheme } = useTheme();
+
+  function getCurrentPath() {
+    const params = new URLSearchParams(window.location.search);
+    return params.get('path') || '';
+  }
+
+  useEffect(() => {
+    const handlePopState = () => setCurrentPath(getCurrentPath());
+    window.addEventListener('popstate', handlePopState);
+    return () => window.removeEventListener('popstate', handlePopState);
+  }, []);
+
+  useEffect(() => {
+    fetchDirectory(currentPath);
+    fetchReadme(currentPath);
+  }, [currentPath]);
+
+  const navigate = (path: string) => {
+    const newUrl = path ? `?path=${encodeURIComponent(path)}` : '/';
+    window.history.pushState({ path }, '', newUrl);
+    setCurrentPath(path);
+  };
+
+  const fetchDirectory = async (path: string) => {
+    setLoading(true);
+    setError(null);
+    try {
+      const cacheKey = `dir:${path}`;
+      let data;
+      if (prefetchCache.has(cacheKey)) {
+        data = await prefetchCache.get(cacheKey);
+        prefetchCache.delete(cacheKey);
+      } else {
+        const url = path
+          ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+          : `${API_BASE}/list`;
+        const response = await fetch(url);
+        if (response.ok) {
+          data = await response.json();
+        }
+      }
+
+      if (data?.error) {
+        throw new Error(data.error);
+      }
+
+      setContent({
+        files: data?.files || [],
+        directories: data?.directories || []
+      });
+    } catch (err: any) {
+      console.error('Error loading directory:', err);
+      setError(err.message);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchReadme = async (path: string) => {
+    setReadme(null);
+    const readmePath = path ? `${path}/README.md` : 'README.md';
+    try {
+      const response = await fetch(`${API_BASE}/file?path=${encodeURIComponent(readmePath)}`);
+      if (response.ok) {
+        const text = await response.text();
+        setReadme(text);
+      }
+    } catch (err) {
+      // Readme is optional, ignore errors
+    }
+  };
+
+  const handleOpenFile = useCallback((path: string) => {
+    setViewingFile(path);
+  }, []);
+
+  const handleCloseFile = useCallback(() => {
+    setViewingFile(null);
+  }, []);
+
+  return (
+    <>
+      <div className="repo-container">
+        <Header
+          title="Zenbu Repository"
+          subtitle="Browse and manage the mercurial codebase"
+          showThemeToggle={true}
+          isDark={isDark}
+          onToggleTheme={toggleTheme}
+        />
+
+        {/* Clone Bar */}
+        <div className="clone-box">
+          <div className="clone-box-inner">
+            <span className="clone-label">Clone HTTPS</span>
+            <code className="clone-url">hg clone http://zenbu.babocoder.com/repo</code>
+          </div>
+        </div>
+
+        {/* Navigation & Content */}
+        <Breadcrumb currentPath={currentPath} onNavigate={navigate} />
+
+        {error && <div className="error-message">Error: {error}</div>}
+
+        {loading ? (
+          <div className="file-list-container">
+            <div className="loading-state">Loading files...</div>
+          </div>
+        ) : (
+          <>
+            <FileList
+              directories={content.directories}
+              files={content.files}
+              onNavigate={navigate}
+              onOpenFile={handleOpenFile}
+            />
+            <ReadmeViewer content={readme} />
+          </>
+        )}
+
+        <Footer />
+      </div>
+
+      {/* File Viewer Modal */}
+      {viewingFile && (
+        isMarkdownFile(viewingFile) ? (
+          <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} />
+        ) : (
+          <FileViewer filePath={viewingFile} onClose={handleCloseFile} />
+        )
+      )}
+    </>
+  );
+}
+
+/**
+ * Main Application Component with ThemeProvider
+ */
+function RepoBrowser() {
+  return (
+    <ThemeProvider>
+      <RepoBrowserContent />
+    </ThemeProvider>
+  );
+}
+
+export { RepoBrowser };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/theme.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,72 @@
+import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
+
+interface ThemeContextType {
+  isDark: boolean;
+  toggleTheme: () => void;
+}
+
+const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
+
+// Apply theme class to document root
+function applyTheme(isDark: boolean) {
+  const root = document.documentElement;
+  if (isDark) {
+    root.classList.add('dark');
+    root.classList.remove('light');
+  } else {
+    root.classList.add('light');
+    root.classList.remove('dark');
+  }
+}
+
+interface ThemeProviderProps {
+  children: ReactNode;
+}
+
+function ThemeProvider({ children }: ThemeProviderProps) {
+  const [isDark, setIsDark] = useState(() => {
+    const saved = localStorage.getItem('theme');
+    if (saved) return saved === 'dark';
+    return window.matchMedia('(prefers-color-scheme: dark)').matches;
+  });
+
+  // Apply theme on mount and change
+  useEffect(() => {
+    applyTheme(isDark);
+    localStorage.setItem('theme', isDark ? 'dark' : 'light');
+  }, [isDark]);
+
+  // Listen for system theme changes
+  useEffect(() => {
+    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+    const handleChange = (e: MediaQueryListEvent) => {
+      // Only apply if no explicit preference is saved
+      if (!localStorage.getItem('theme')) {
+        setIsDark(e.matches);
+      }
+    };
+
+    mediaQuery.addEventListener('change', handleChange);
+    return () => mediaQuery.removeEventListener('change', handleChange);
+  }, []);
+
+  const toggleTheme = useCallback(() => {
+    setIsDark(prev => !prev);
+  }, []);
+
+  return (
+    <ThemeContext.Provider value={{ isDark, toggleTheme }}>
+      {children}
+    </ThemeContext.Provider>
+  );
+}
+
+function useTheme(): ThemeContextType {
+  const context = useContext(ThemeContext);
+  if (context === undefined) {
+    throw new Error('useTheme must be used within a ThemeProvider');
+  }
+  return context;
+}
+
+export { ThemeProvider, useTheme };
--- a/hg-web/src/index.css	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/src/index.css	Sun Aug 02 16:50:48 2026 -0700
@@ -1,179 +1,1004 @@
+/* ===========================================
+   Component Styles
+   Import base.css before this file
+   =========================================== */
+
+/* ===========================================
+   App Layout
+   =========================================== */
+.app-container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 40px 20px;
+}
+
+/* ===========================================
+   Header
+   =========================================== */
 .header {
-  border-bottom: 1px solid var(--border);
-  padding-bottom: 1rem;
-  margin-bottom: 2rem;
+  display: flex;
+  align-items: center;
+  margin-bottom: 24px;
+  gap: 15px;
+}
+
+.header-icon {
+  width: 32px;
+  height: 32px;
+  opacity: 0.8;
+  cursor: pointer;
 }
 
 .header h1 {
-  margin-bottom: 0.5rem;
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.header h1 a {
+  color: inherit;
+  text-decoration: none;
+}
+
+.header-subtitle {
+  color: var(--text-secondary);
+  margin: 0;
+  font-size: 14px;
 }
 
-.header .description {
-  color: var(--secondary);
-  font-size: 0.95rem;
+/* ===========================================
+   Navigation Tabs
+   =========================================== */
+.nav-tabs {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 24px;
+  border-bottom: 1px solid var(--border);
+  padding-bottom: 8px;
 }
 
-.clone-info {
-  background: var(--code-bg);
-  border: 1px solid var(--border);
+.nav-tab {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 16px;
   border-radius: 6px;
-  padding: 1rem;
-  margin-bottom: 2rem;
+  background: transparent;
+  border: 1px solid transparent;
+  color: var(--text-secondary);
+  cursor: pointer;
+  font-size: 14px;
+  transition: all 0.2s;
+}
+
+.nav-tab:hover {
+  background: var(--bg-subtle);
+  color: var(--text-primary);
+}
+
+.nav-tab.active {
+  background: var(--bg-subtle);
+  border-color: var(--border);
+  color: var(--text-primary);
+  font-weight: 500;
 }
 
-.clone-info code {
-  background: none;
-  color: var(--fg);
-  font-size: 0.95rem;
+.nav-tab svg {
+  color: var(--text-secondary);
+}
+
+/* ===========================================
+   Landing Page
+   =========================================== */
+.landing-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 24px;
+}
+
+@media (max-width: 768px) {
+  .landing-grid {
+    grid-template-columns: 1fr;
+  }
+}
+
+.landing-section {
+  background: var(--bg);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  overflow: hidden;
 }
 
-.breadcrumb {
-  margin-bottom: 1.5rem;
-  font-size: 0.95rem;
+.landing-section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  background: var(--bg-subtle);
+  border-bottom: 1px solid var(--border);
 }
 
-.breadcrumb a {
-  color: var(--link);
+.landing-section-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: 600;
+  font-size: 14px;
+  color: var(--text-primary);
 }
 
-.breadcrumb a:hover {
+.landing-section-title svg {
+  color: var(--text-secondary);
+}
+
+.landing-section-link {
+  font-size: 12px;
+  color: var(--accent);
+  text-decoration: none;
+  padding: 4px 8px;
+  border-radius: 4px;
+  transition: background 0.2s;
+}
+
+.landing-section-link:hover {
+  background: var(--bg-subtle);
   text-decoration: underline;
 }
 
-.breadcrumb span {
-  color: var(--secondary);
-  margin: 0 0.5rem;
+.landing-section-content {
+  padding: 0;
+}
+
+/* ===========================================
+   Directory Items (Landing Preview)
+   =========================================== */
+.dir-item {
+  display: flex;
+  align-items: center;
+  padding: 10px 16px;
+  border-bottom: 1px solid var(--border);
+  font-size: 14px;
+  cursor: pointer;
+  transition: background 0.1s;
+}
+
+.dir-item:last-child {
+  border-bottom: none;
+}
+
+.dir-item:hover {
+  background: var(--hover);
+}
+
+.dir-item-icon {
+  margin-right: 12px;
+  opacity: 0.7;
+}
+
+.dir-item-icon img {
+  width: 18px;
+  height: 18px;
+}
+
+.dir-item-name {
+  color: var(--text-primary);
+}
+
+.dir-item-name:hover {
+  color: var(--accent);
+}
+
+/* ===========================================
+   Page Header (Graph Page)
+   =========================================== */
+.page-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 16px;
 }
 
-.file-list {
+.back-button {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 12px;
+  background: var(--bg-subtle);
   border: 1px solid var(--border);
   border-radius: 6px;
+  color: var(--text-secondary);
+  cursor: pointer;
+  font-size: 13px;
+  transition: all 0.2s;
+}
+
+.back-button:hover {
+  background: var(--hover);
+  color: var(--text-primary);
+}
+
+.page-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+/* ===========================================
+   Graph Params
+   =========================================== */
+.graph-params {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 16px;
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+
+.graph-param {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 8px;
+  background: var(--bg-subtle);
+  border-radius: 4px;
+}
+
+.graph-param-label {
+  font-weight: 500;
+}
+
+.graph-param-value {
+  font-family: monospace;
+  color: var(--accent);
+}
+
+/* ===========================================
+   Graph Component
+   =========================================== */
+.graph-container {
+  background: var(--bg);
+  color: var(--text-primary);
+  font-family: "More Thin", sans-serif;
+  border-radius: 6px;
+  border: 1px solid var(--border);
   overflow: hidden;
 }
 
-.file-item {
+.graph-wrapper {
   display: flex;
-  align-items: center;
-  padding: 0.75rem 1rem;
-  border-bottom: 1px solid var(--border);
-  transition: background-color 0.2s;
+  align-items: flex-start;
+  max-height: 600px;
+  overflow-y: auto;
+  position: relative;
+}
+
+.graph-canvas-column {
+  flex-shrink: 0;
+  position: sticky;
+  left: 0;
+  z-index: 1;
+  border-right: 1px solid var(--border);
+}
+
+.graph-details-column {
+  flex-grow: 1;
+  overflow-x: hidden;
 }
 
-.file-item:last-child {
-  border-bottom: none;
+.graph-row {
+  width: 100%;
+  height: 40px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  padding: 0 12px;
+  background: transparent;
+  color: inherit;
+  text-align: left;
+  border: 0;
+  border-bottom: 1px solid var(--border);
+  font-size: 12px;
+  cursor: pointer;
+  transition: background-color 0.1s;
 }
 
-.file-item:hover {
+.graph-row:hover {
+  background: var(--hover);
+}
+
+.graph-row:focus-visible {
+  outline: 2px solid var(--accent);
+  outline-offset: -2px;
   background: var(--hover);
 }
 
-.file-item .icon {
-  margin-right: 0.75rem;
-  font-size: 1.2rem;
-  width: 20px;
-  text-align: center;
+.graph-row-meta {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 2px;
+  align-items: center;
+}
+
+.graph-hash {
+  color: var(--accent);
+  font-family: monospace;
+}
+
+.graph-user {
+  color: var(--text-secondary);
+  font-weight: 500;
+}
+
+.graph-branch {
+  color: var(--text-secondary);
+  font-size: 10px;
+  background: var(--bg-subtle);
+  padding: 1px 6px;
+  border-radius: 3px;
+}
+
+.graph-desc {
+  color: var(--text-primary);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.graph-badge-tip {
+  background: var(--success);
+  color: #fff;
+  padding: 0 4px;
+  border-radius: 2px;
+  font-size: 10px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.graph-badge-tag {
+  background: var(--accent);
+  color: #fff;
+  padding: 0 4px;
+  border-radius: 2px;
+  font-size: 10px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.graph-loading-row {
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--text-secondary);
+  font-size: 12px;
+}
+
+/* ===========================================
+   Changeset Detail
+   =========================================== */
+.changeset-paper {
+  background: var(--bg);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  padding: 24px;
+}
+
+.changeset-heading,
+.changeset-meta,
+.changeset-parents,
+.changeset-files,
+.changeset-labels {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.changeset-heading {
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.changeset-heading code {
+  overflow-wrap: anywhere;
+}
+
+.changeset-paper h2 {
+  margin-bottom: 8px;
+}
+
+.changeset-meta {
+  color: var(--text-secondary);
+  justify-content: space-between;
+  margin-bottom: 16px;
 }
 
-.file-item .name {
-  flex: 1;
-  font-family: 'Monaco', 'Courier New', monospace;
-  font-size: 0.9rem;
+.changeset-branch,
+.changeset-labels span {
+  background: var(--bg-subtle);
+  border: 1px solid var(--border);
+  border-radius: 999px;
+  padding: 2px 8px;
+  font-size: 12px;
+}
+
+.changeset-labels,
+.changeset-parents,
+.changeset-files {
+  margin: 12px 0;
+}
+
+.changeset-parents button {
+  border: 0;
+  background: transparent;
+  color: var(--accent);
+  cursor: pointer;
+  font-family: monospace;
+}
+
+.changeset-parents button:hover {
+  text-decoration: underline;
+}
+
+.changeset-parents button:focus-visible {
+  outline: 2px solid var(--accent);
+  outline-offset: 2px;
+}
+
+.changeset-files code {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  overflow-wrap: anywhere;
+}
+
+.changeset-file-status {
+  color: var(--text-secondary);
+  font-size: 10px;
+  text-transform: uppercase;
+}
+
+.changeset-file-status.status-added {
+  color: var(--success);
 }
 
-.file-item .name a {
-  color: var(--fg);
+.changeset-file-status.status-removed {
+  color: var(--danger);
+}
+
+.changeset-diff {
+  margin-top: 24px;
+}
+
+.changeset-diff h3 {
+  margin-bottom: 10px;
+}
+
+.side-by-side-diff {
+  margin-bottom: 18px;
+  overflow-x: auto;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  background: var(--bg-code);
+}
+
+.diff-file-header {
+  padding: 9px 12px;
+  border-bottom: 1px solid var(--border);
+  background: var(--bg-subtle);
+  font-family: monospace;
+  font-weight: 600;
+}
+
+.diff-column-headings {
+  display: grid;
+  grid-template-columns: minmax(380px, 1fr) minmax(380px, 1fr);
+  min-width: 760px;
+  border-bottom: 1px solid var(--border);
+  color: var(--text-secondary);
+  font-size: 12px;
+  font-weight: 600;
+  text-transform: uppercase;
+}
+
+.diff-column-headings span {
+  padding: 6px 12px;
+}
+
+.diff-column-headings span + span {
+  border-left: 1px solid var(--border);
+}
+
+.diff-grid {
+  display: grid;
+  grid-template-columns: 52px minmax(328px, 1fr) 52px minmax(328px, 1fr);
+  min-width: 760px;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 12px;
+  line-height: 1.5;
 }
 
-.file-item .name a:hover {
-  color: var(--link);
+.diff-side-number,
+.diff-side-code {
+  min-height: 22px;
+  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
+}
+
+.diff-side-number {
+  padding: 2px 8px;
+  color: var(--text-secondary);
+  text-align: right;
+  user-select: none;
+  border-right: 1px solid var(--border);
+}
+
+.diff-side-code {
+  display: block;
+  margin: 0;
+  padding: 2px 10px;
+  border-radius: 0;
+  background: transparent;
+  white-space: pre;
+}
+
+.diff-column-divider {
+  border-left: 1px solid var(--border);
+}
+
+.diff-add {
+  background: color-mix(in srgb, var(--success) 18%, transparent);
+}
+
+.diff-remove {
+  background: color-mix(in srgb, var(--danger) 18%, transparent);
+}
+
+.diff-empty {
+  background: color-mix(in srgb, var(--bg-subtle) 70%, transparent);
+}
+
+.diff-meta {
+  color: var(--text-secondary);
+  background: var(--bg-subtle);
+}
+
+.diff-range-row {
+  grid-column: 1 / -1;
+  padding: 4px 10px;
+  color: var(--accent);
+  background: var(--bg-subtle);
+  border-bottom: 1px solid var(--border);
+  white-space: pre;
 }
 
-.file-item.directory .icon {
-  color: var(--accent);
+/* ===========================================
+   Common States
+   =========================================== */
+.empty-state {
+  padding: 40px;
+  text-align: center;
+  color: var(--text-secondary);
+}
+
+.loading-state {
+  padding: 40px;
+  text-align: center;
+  color: var(--text-secondary);
+}
+
+.error-message {
+  padding: 15px;
+  border: 1px solid var(--danger-border);
+  background: var(--danger-bg);
+  color: var(--danger);
+  border-radius: 6px;
+  margin-bottom: 20px;
+}
+
+/* ===========================================
+   Repository Browser
+   =========================================== */
+.repo-container {
+  font-family: "More Thin", sans-serif;
+  max-width: 980px;
+  margin: 40px auto;
+  color: var(--text-primary);
+  padding: 0 20px;
+}
+
+/* Clone Box */
+.clone-box {
+  background: var(--bg-subtle);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  padding: 12px 16px;
+  margin-bottom: 24px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.clone-label {
+  font-weight: 600;
+  font-size: 13px;
+  margin-right: 10px;
+  color: var(--text-primary);
 }
 
-.file-item.file .icon {
-  color: var(--secondary);
+.clone-url {
+  font-family: "More Thin", sans-serif;
+  background: var(--bg);
+  border: 1px solid var(--border);
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+  color: var(--text-secondary);
+  flex-grow: 1;
+}
+
+/* Breadcrumb */
+.breadcrumb {
+  display: flex;
+  align-items: center;
+  font-size: 14px;
+  margin-bottom: 16px;
+  color: var(--text-secondary);
+  padding: 8px 0;
+}
+
+.breadcrumb a {
+  color: var(--accent);
+  text-decoration: none;
+  border-radius: 4px;
+  padding: 2px 6px;
+}
+
+.breadcrumb a:hover {
+  background: var(--bg-subtle);
+  text-decoration: underline;
+}
+
+.breadcrumb .separator {
+  margin: 0 4px;
+  color: var(--text-secondary);
+  opacity: 0.5;
+}
+
+.breadcrumb .nav-item.active {
+  font-weight: 600;
+  color: var(--text-primary);
+  padding: 2px 6px;
+}
+
+/* File List */
+.file-list-container {
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  overflow: hidden;
+  background: var(--bg);
 }
 
-.readme-section {
-  margin-top: 2rem;
-  padding-top: 2rem;
-  border-top: 1px solid var(--border);
+.file-header {
+  background: var(--bg-subtle);
+  border-bottom: 1px solid var(--border);
+  padding: 12px 16px;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text-secondary);
+}
+
+.file-row {
+  display: flex;
+  align-items: center;
+  padding: 10px 16px;
+  border-bottom: 1px solid var(--border);
+  transition: background 0.1s;
+}
+
+.file-row:last-child {
+  border-bottom: none;
+}
+
+.file-row:hover {
+  background: var(--hover);
 }
 
-.readme-section h2 {
-  margin-bottom: 1rem;
-  font-size: 1.5rem;
+.file-row .icon img {
+  width: 20px;
+  height: 20px;
+  vertical-align: middle;
+  margin-right: 12px;
+}
+
+.file-row .name a {
+  color: var(--text-primary);
+  text-decoration: none;
+  font-size: 14px;
+}
+
+.file-row .name a:hover {
+  color: var(--accent);
+  text-decoration: underline;
+}
+
+/* Readme */
+.readme-section {
+  margin-top: 32px;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+}
+
+.readme-header {
+  background: var(--bg-subtle);
+  padding: 10px 16px;
+  font-size: 12px;
+  font-weight: 600;
+  border-bottom: 1px solid var(--border);
+  display: flex;
+  align-items: center;
+  gap: 8px;
 }
 
 .readme-content {
+  padding: 32px;
+  background: var(--bg);
+  overflow-x: auto;
+  color: var(--text-primary);
+}
+
+/* File Viewer Modal */
+.file-viewer-overlay {
+  position: fixed;
+  inset: 0;
+  background: var(--overlay);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+  padding: 20px;
+}
+
+.file-viewer {
+  background: var(--bg);
   border: 1px solid var(--border);
   border-radius: 6px;
-  padding: 1.5rem;
-  background: var(--code-bg);
+  width: 100%;
+  max-width: 900px;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+}
+
+.file-viewer-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  background: var(--bg-subtle);
+  border-bottom: 1px solid var(--border);
+  border-radius: 6px 6px 0 0;
+}
+
+.file-viewer-title {
+  font-weight: 600;
+  font-size: 14px;
+  color: var(--text-primary);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.file-viewer-close {
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  padding: 4px;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
-.readme-content h1 { font-size: 1.75rem; margin-top: 1.5rem; }
-.readme-content h2 { font-size: 1.5rem; margin-top: 1.25rem; }
-.readme-content h3 { font-size: 1.25rem; margin-top: 1rem; }
+.file-viewer-close:hover {
+  background: var(--hover);
+}
+
+.file-viewer-close img {
+  width: 16px;
+  height: 16px;
+  opacity: 0.7;
+}
 
-.readme-content h1:first-child,
-.readme-content h2:first-child,
-.readme-content h3:first-child {
-  margin-top: 0;
+.file-viewer-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.file-viewer-actions a {
+  font-size: 13px;
+}
+
+.file-viewer-content {
+  overflow: auto;
+  flex: 1;
+}
+
+.static-file-viewer {
+  max-width: 1100px;
 }
 
-.readme-content ul,
-.readme-content ol {
-  margin-left: 2rem;
-  margin-bottom: 1rem;
+.static-file-preview {
+  min-height: 240px;
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: var(--bg-subtle);
+}
+
+.static-file-image,
+.static-file-video {
+  display: block;
+  max-width: 100%;
+  max-height: 75vh;
+  object-fit: contain;
 }
 
-.readme-content li {
-  margin-bottom: 0.5rem;
+.static-file-audio {
+  width: min(100%, 640px);
 }
 
-.readme-content img {
-  max-width: 100%;
-  height: auto;
-  border-radius: 6px;
+.static-file-pdf {
+  width: 100%;
+  height: 75vh;
+  border: 0;
+  background: #fff;
+}
+
+.file-viewer-content pre {
+  margin: 0;
+  padding: 16px;
+  background: var(--bg-code);
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  overflow-x: auto;
 }
 
-.empty-state {
+.file-viewer-content code {
+  background: transparent;
+  padding: 0;
+}
+
+.file-viewer-loading {
+  padding: 40px;
   text-align: center;
-  padding: 3rem 1rem;
-  color: var(--secondary);
+  color: var(--text-secondary);
+}
+
+.file-viewer-line-numbers {
+  display: inline-block;
+  user-select: none;
+  text-align: right;
+  padding-right: 16px;
+  margin-right: 16px;
+  border-right: 1px solid var(--border);
+  color: var(--text-secondary);
+  opacity: 0.5;
 }
 
-.error-message {
-  background: var(--danger);
-  color: white;
-  padding: 1rem;
+/* Theme Toggle Button */
+.theme-toggle {
+  margin-left: auto;
+  background: var(--bg-subtle);
+  border: 1px solid var(--border);
   border-radius: 6px;
-  margin-bottom: 1rem;
+  padding: 8px 12px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  color: var(--text-secondary);
+  font-size: 13px;
+  transition: all 0.2s;
+}
+
+.theme-toggle:hover {
+  background: var(--hover);
+  color: var(--text-primary);
+}
+
+.theme-toggle svg {
+  flex-shrink: 0;
+}
+
+/* Description */
+.description {
+  color: var(--text-secondary);
+  margin: 0;
+  font-size: 14px;
 }
 
-/* Mobile responsive */
+/* ===========================================
+   Mobile Responsive
+   =========================================== */
 @media (max-width: 768px) {
-  main {
-    padding: 1rem;
+  .app-container {
+    padding: 20px 15px;
   }
 
-  .file-item {
-    padding: 0.5rem 0.75rem;
+  .repo-container {
+    padding: 0 15px;
+  }
+
+  .file-row {
+    padding: 8px 12px;
   }
 
-  .file-item .name {
-    font-size: 0.85rem;
-  }
-
-  .clone-info {
-    padding: 0.75rem;
-    overflow-x: auto;
-  }
-
-  .readme-content {
-    padding: 1rem;
+  .clone-box {
+    padding: 10px 12px;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
   }
 }
+
+/* ===========================================
+   Footer
+   =========================================== */
+.footer {
+  margin-top: 48px;
+  padding-top: 24px;
+  border-top: 1px solid var(--border);
+}
+
+.footer-content {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 16px 0;
+  color: var(--text-secondary);
+  font-size: 13px;
+}
+
+.footer-separator {
+  opacity: 0.5;
+}
+
+.footer-text {
+  color: var(--text-secondary);
+}
+
+.clone-box-inner {
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+
+/* Header content wrapper */
+.header-content {
+  flex: 1;
+}
+
+.header-content h1 {
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.header-content h1 a {
+  color: inherit;
+  text-decoration: none;
+}
+
+.header-content h1 a:hover {
+  text-decoration: none;
+}
--- a/hg-web/src/index.html	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/src/index.html	Sun Aug 02 16:50:48 2026 -0700
@@ -4,8 +4,13 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Zenbu Repository</title>
+    <link rel="stylesheet" href="/index.css">
+    <link rel="stylesheet" href="/base.css">
+
     <link rel="stylesheet" href="/a11y-dark.min.css"  media="(prefers-color-scheme: dark)">
     <link rel="stylesheet" href="/a11y-light.min.css" media="(prefers-color-scheme: light)">
+
+    <link rel="preload" href="/public/fonts/more-sugar.regular.otf" as="font" type="font/otf" crossorigin>
     <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
   </head>
   <body>
--- a/hg-web/src/main.tsx	Sun Jan 25 10:44:04 2026 -0800
+++ b/hg-web/src/main.tsx	Sun Aug 02 16:50:48 2026 -0700
@@ -1,8 +1,6 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
-import { RepoBrowser } from "hg-web/src/repo-browser";
+import { App } from "hg-web/src/components/app";
 
-const root = ReactDOM.createRoot(document.getElementById('root'));
-
-// Use JSX syntax (<RepoBrowser />) 
-root.render(<RepoBrowser />);
+const root = ReactDOM.createRoot(document.getElementById('root')!);
+root.render(<App />);
Binary file hg-web/src/panda.png has changed
Binary file hg-web/src/pencil_lines.png has changed
--- a/hg-web/src/repo-browser.tsx	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,879 +0,0 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
-import createMarkdownModule from 'markdown_converter/markdown_to_html_wasm/markdown_to_html_bin.js';
-import hljs from 'third_party/highlight/highlight.min.js';
-
-// --- ICONS (served as static files) ---
-const ICONS = {
-  folder: "/icons/folder.png",
-  file: "/icons/file.svg",
-  home: "/icons/home.png",
-  repo: "/public/epi_all_colors.svg",
-  close: "/icons/close.png"
-};
-
-// SVG Icons for theme toggle
-const SunIcon = ({ color = "currentColor" }: { color?: string }) => (
-  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-    <circle cx="12" cy="12" r="5"/>
-    <line x1="12" y1="1" x2="12" y2="3"/>
-    <line x1="12" y1="21" x2="12" y2="23"/>
-    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
-    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
-    <line x1="1" y1="12" x2="3" y2="12"/>
-    <line x1="21" y1="12" x2="23" y2="12"/>
-    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
-    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
-  </svg>
-);
-
-const MoonIcon = ({ color = "currentColor" }: { color?: string }) => (
-  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
-  </svg>
-);
-
-const API_BASE = '/api/repo';
-
-// File extensions that should be displayed as code
-const CODE_EXTENSIONS = new Set([
-  'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp',
-  'cs', 'go', 'rs', 'swift', 'kt', 'scala', 'php', 'pl', 'sh', 'bash',
-  'zsh', 'fish', 'ps1', 'bat', 'cmd', 'html', 'htm', 'css', 'scss',
-  'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg',
-  'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue',
-  'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake',
-  'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample'
-]);
-
-// Prefetch cache
-const prefetchCache = new Map<string, Promise<any>>();
-
-function isCodeFile(filename: string): boolean {
-  const ext = filename.split('.').pop()?.toLowerCase() || '';
-  const basename = filename.toLowerCase();
-  return CODE_EXTENSIONS.has(ext) ||
-         CODE_EXTENSIONS.has(basename) ||
-         basename === 'dockerfile' ||
-         basename === 'makefile' ||
-         basename.startsWith('.');
-}
-
-function isMarkdownFile(filename: string): boolean {
-  const ext = filename.split('.').pop()?.toLowerCase() || '';
-  return ext === 'md' || ext === 'markdown';
-}
-
-function prefetchDirectory(path: string): void {
-  const cacheKey = `dir:${path}`;
-  if (prefetchCache.has(cacheKey)) return;
-
-  const url = path
-    ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
-    : `${API_BASE}/list`;
-
-  prefetchCache.set(cacheKey, fetch(url).then(r => r.json()).catch(() => null));
-}
-
-function prefetchFile(path: string): void {
-  const cacheKey = `file:${path}`;
-  if (prefetchCache.has(cacheKey)) return;
-
-  prefetchCache.set(cacheKey,
-    fetch(`${API_BASE}/file?path=${encodeURIComponent(path)}`)
-      .then(r => r.ok ? r.text() : null)
-      .catch(() => null)
-  );
-}
-
-async function getCachedFile(path: string): Promise<string | null> {
-  const cacheKey = `file:${path}`;
-  if (prefetchCache.has(cacheKey)) {
-    return prefetchCache.get(cacheKey);
-  }
-  prefetchFile(path);
-  return prefetchCache.get(cacheKey)!;
-}
-
-/**
- * Component: Styles
- * Injected CSS for the polished look with dark/light mode support
- */
-const GlobalStyles = ({ isDark }: { isDark: boolean }) => (
-  <style>{`
-    :root {
-      --bg-color: ${isDark ? '#0d1117' : '#ffffff'};
-      --bg-subtle: ${isDark ? '#161b22' : '#f6f8fa'};
-      --bg-code: ${isDark ? '#1c2128' : '#f6f8fa'};
-      --border-color: ${isDark ? '#30363d' : '#d0d7de'};
-      --accent-color: ${isDark ? '#58a6ff' : '#0969da'};
-      --text-primary: ${isDark ? '#e6edf3' : '#1f2328'};
-      --text-secondary: ${isDark ? '#8b949e' : '#656d76'};
-      --hover-color: ${isDark ? '#1c2128' : '#f3f4f6'};
-      --radius: 6px;
-      --code-bg: ${isDark ? '#161b22' : '#ffffff'};
-    }
-
-    body {
-      background: var(--bg-color);
-      transition: background 0.2s;
-    }
-
-    .repo-container {
-      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-      max-width: 980px;
-      margin: 40px auto;
-      color: var(--text-primary);
-      padding: 0 20px;
-    }
-
-    /* Header */
-    .header {
-      display: flex;
-      align-items: center;
-      margin-bottom: 20px;
-      gap: 15px;
-    }
-    .header-icon { width: 32px; height: 32px; opacity: 0.8; }
-    .header h1 { margin: 0; font-size: 24px; font-weight: 600; }
-    .description { color: var(--text-secondary); margin: 0; font-size: 14px; }
-
-    /* Theme Toggle */
-    .theme-toggle {
-      margin-left: auto;
-      background: var(--bg-subtle);
-      border: 1px solid var(--border-color);
-      border-radius: var(--radius);
-      padding: 8px 12px;
-      cursor: pointer;
-      display: flex;
-      align-items: center;
-      gap: 6px;
-      color: var(--text-secondary);
-      font-size: 13px;
-      transition: all 0.2s;
-    }
-    .theme-toggle:hover {
-      background: var(--hover-color);
-      color: var(--text-primary);
-    }
-    .theme-toggle svg {
-      flex-shrink: 0;
-    }
-
-    /* Clone Box */
-    .clone-box {
-      background: var(--bg-subtle);
-      border: 1px solid var(--border-color);
-      border-radius: var(--radius);
-      padding: 12px 16px;
-      margin-bottom: 24px;
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-    }
-    .clone-label { font-weight: 600; font-size: 13px; margin-right: 10px; color: var(--text-primary); }
-    .clone-url {
-      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
-      background: var(--bg-color);
-      border: 1px solid var(--border-color);
-      padding: 4px 8px;
-      border-radius: 4px;
-      font-size: 12px;
-      color: var(--text-secondary);
-      flex-grow: 1;
-    }
-
-    /* Breadcrumb */
-    #breadcrumb {
-      display: flex;
-      align-items: center;
-      font-size: 14px;
-      margin-bottom: 16px;
-      color: var(--text-secondary);
-      padding: 8px 0;
-    }
-    #breadcrumb a {
-      color: var(--accent-color);
-      text-decoration: none;
-      border-radius: 4px;
-      padding: 2px 6px;
-    }
-    #breadcrumb a:hover { background: var(--bg-subtle); text-decoration: underline; }
-    #breadcrumb .separator { margin: 0 4px; color: var(--text-secondary); opacity: 0.5; }
-    #breadcrumb .nav-item.active { font-weight: 600; color: var(--text-primary); padding: 2px 6px;}
-
-    /* File List Table Structure */
-    .file-list-container {
-      border: 1px solid var(--border-color);
-      border-radius: var(--radius);
-      overflow: hidden;
-      background: var(--bg-color);
-    }
-    .file-header {
-      background: var(--bg-subtle);
-      border-bottom: 1px solid var(--border-color);
-      padding: 12px 16px;
-      font-size: 13px;
-      font-weight: 600;
-      color: var(--text-secondary);
-    }
-    .empty-state { padding: 40px; text-align: center; color: var(--text-secondary); }
-    .error-message {
-      padding: 15px; border: 1px solid ${isDark ? '#f8514966' : '#ffdce0'};
-      background: ${isDark ? '#f8514926' : '#ffebe9'}; color: ${isDark ? '#f85149' : '#cf222e'};
-      border-radius: var(--radius); margin-bottom: 20px;
-    }
-
-    /* File Row */
-    .file-row {
-      display: flex;
-      align-items: center;
-      padding: 10px 16px;
-      border-bottom: 1px solid var(--border-color);
-      transition: background 0.1s;
-    }
-    .file-row:last-child { border-bottom: none; }
-    .file-row:hover { background: var(--hover-color); }
-
-    .file-row .icon img {
-      width: 20px;
-      height: 20px;
-      vertical-align: middle;
-      margin-right: 12px;
-      filter: ${isDark ? 'invert(0.8)' : 'none'};
-    }
-    .file-row .name a {
-      color: var(--text-primary);
-      text-decoration: none;
-      font-size: 14px;
-    }
-    .file-row .name a:hover { color: var(--accent-color); text-decoration: underline; }
-
-    /* Readme */
-    #readmeSection { margin-top: 32px; border: 1px solid var(--border-color); border-radius: var(--radius); }
-    .readme-header {
-      background: var(--bg-subtle);
-      padding: 10px 16px;
-      font-size: 12px; font-weight: 600;
-      border-bottom: 1px solid var(--border-color);
-      display: flex; align-items: center; gap: 8px;
-    }
-    .readme-header img {
-      filter: ${isDark ? 'invert(0.7)' : 'none'};
-    }
-    #readmeContent { padding: 32px; background: var(--bg-color); overflow-x: auto; color: var(--text-primary); }
-
-    /* File Viewer */
-    .file-viewer-overlay {
-      position: fixed;
-      inset: 0;
-      background: ${isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.5)'};
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      z-index: 1000;
-      padding: 20px;
-    }
-    .file-viewer {
-      background: var(--bg-color);
-      border: 1px solid var(--border-color);
-      border-radius: var(--radius);
-      width: 100%;
-      max-width: 900px;
-      max-height: 90vh;
-      display: flex;
-      flex-direction: column;
-      box-shadow: 0 8px 32px rgba(0,0,0,0.3);
-    }
-    .file-viewer-header {
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      padding: 12px 16px;
-      background: var(--bg-subtle);
-      border-bottom: 1px solid var(--border-color);
-      border-radius: var(--radius) var(--radius) 0 0;
-    }
-    .file-viewer-title {
-      font-weight: 600;
-      font-size: 14px;
-      color: var(--text-primary);
-      display: flex;
-      align-items: center;
-      gap: 8px;
-    }
-    .file-viewer-close {
-      background: transparent;
-      border: none;
-      cursor: pointer;
-      padding: 4px;
-      border-radius: 4px;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
-    .file-viewer-close:hover {
-      background: var(--hover-color);
-    }
-    .file-viewer-close img {
-      width: 16px;
-      height: 16px;
-      filter: ${isDark ? 'invert(0.7)' : 'none'};
-      opacity: 0.7;
-    }
-    .file-viewer-content {
-      overflow: auto;
-      flex: 1;
-    }
-    .file-viewer-content pre {
-      margin: 0;
-      padding: 16px;
-      background: var(--code-bg);
-      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
-      font-size: 13px;
-      line-height: 1.5;
-      overflow-x: auto;
-    }
-    .file-viewer-content code {
-      background: transparent;
-      padding: 0;
-    }
-    .file-viewer-loading {
-      padding: 40px;
-      text-align: center;
-      color: var(--text-secondary);
-    }
-    .file-viewer-line-numbers {
-      display: inline-block;
-      user-select: none;
-      text-align: right;
-      padding-right: 16px;
-      margin-right: 16px;
-      border-right: 1px solid var(--border-color);
-      color: var(--text-secondary);
-      opacity: 0.5;
-    }
-  `}</style>
-);
-
-/**
- * Component: Breadcrumb
- */
-function Breadcrumb({ currentPath, onNavigate }) {
-  if (!currentPath) {
-    return (
-      <nav id="breadcrumb">
-        <span className="nav-item active">root</span>
-      </nav>
-    );
-  }
-
-  const parts = currentPath.split('/').filter(p => p);
-  const crumbs = parts.map((part, index) => ({
-    name: part,
-    fullPath: parts.slice(0, index + 1).join('/')
-  }));
-
-  return (
-    <nav id="breadcrumb">
-      <a 
-        href="/" 
-        onClick={(e) => { e.preventDefault(); onNavigate(''); }}
-        title="Go to Root"
-      >
-        root
-      </a>
-      {crumbs.map((crumb, index) => {
-        const isLast = index === crumbs.length - 1;
-        return (
-          <React.Fragment key={crumb.fullPath}>
-            <span className="separator">/</span>
-            {isLast ? (
-              <span className="nav-item active">{crumb.name}</span>
-            ) : (
-              <a 
-                href={`?path=${encodeURIComponent(crumb.fullPath)}`}
-                onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }}
-              >
-                {crumb.name}
-              </a>
-            )}
-          </React.Fragment>
-        );
-      })}
-    </nav>
-  );
-}
-
-/**
- * Component: FileViewer
- * Shows file content inline with syntax highlighting
- */
-function FileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
-  const [content, setContent] = useState<string | null>(null);
-  const [loading, setLoading] = useState(true);
-  const codeRef = useRef<HTMLElement>(null);
-
-  const filename = filePath.split('/').pop() || filePath;
-
-  useEffect(() => {
-    setLoading(true);
-    getCachedFile(filePath).then((text) => {
-      setContent(text);
-      setLoading(false);
-    });
-  }, [filePath]);
-
-  useEffect(() => {
-    if (content && codeRef.current) {
-      hljs.highlightElement(codeRef.current);
-    }
-  }, [content]);
-
-  // Close on escape key
-  useEffect(() => {
-    const handleKeyDown = (e: KeyboardEvent) => {
-      if (e.key === 'Escape') onClose();
-    };
-    window.addEventListener('keydown', handleKeyDown);
-    return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [onClose]);
-
-  // Get language from file extension for highlight.js
-  const getLanguage = () => {
-    const ext = filename.split('.').pop()?.toLowerCase() || '';
-    const langMap: Record<string, string> = {
-      js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
-      py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
-      c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
-      sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
-      json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
-      html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss',
-      sql: 'sql', md: 'markdown', markdown: 'markdown', xml: 'xml',
-      dockerfile: 'dockerfile', makefile: 'makefile'
-    };
-    return langMap[ext] || 'plaintext';
-  };
-
-  const addLineNumbers = (text: string) => {
-    const lines = text.split('\n');
-    return lines.map((_, i) => i + 1).join('\n');
-  };
-
-  return (
-    <div className="file-viewer-overlay" onClick={onClose}>
-      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
-        <div className="file-viewer-header">
-          <span className="file-viewer-title">
-            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
-            {filename}
-          </span>
-          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
-            <img src={ICONS.close} alt="Close" />
-          </button>
-        </div>
-        <div className="file-viewer-content">
-          {loading ? (
-            <div className="file-viewer-loading">Loading...</div>
-          ) : content ? (
-            <pre style={{ display: 'flex' }}>
-              <span className="file-viewer-line-numbers">{addLineNumbers(content)}</span>
-              <code ref={codeRef} className={`language-${getLanguage()}`}>{content}</code>
-            </pre>
-          ) : (
-            <div className="file-viewer-loading">Unable to load file</div>
-          )}
-        </div>
-      </div>
-    </div>
-  );
-}
-
-/**
- * Component: MarkdownViewerModal
- * Shows markdown content rendered in a modal
- */
-function MarkdownViewerModal({ filePath, onClose }: { filePath: string; onClose: () => void }) {
-  const [content, setContent] = useState<string | null>(null);
-  const [loading, setLoading] = useState(true);
-  const contentRef = useRef<HTMLDivElement>(null);
-  const moduleRef = useRef<any>(null);
-  const [wasmReady, setWasmReady] = useState(false);
-
-  const filename = filePath.split('/').pop() || filePath;
-
-  useEffect(() => {
-    createMarkdownModule().then((Module: any) => {
-      moduleRef.current = Module;
-      setWasmReady(true);
-    });
-  }, []);
-
-  useEffect(() => {
-    setLoading(true);
-    getCachedFile(filePath).then((text) => {
-      setContent(text);
-      setLoading(false);
-    });
-  }, [filePath]);
-
-  useEffect(() => {
-    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
-
-    const Module = moduleRef.current;
-    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
-    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
-
-    const ptr = markdownToHtmlPtr(content);
-    const html = Module.UTF8ToString(ptr);
-    markdownFree(ptr);
-    contentRef.current.innerHTML = html;
-  }, [content, wasmReady]);
-
-  // Close on escape key
-  useEffect(() => {
-    const handleKeyDown = (e: KeyboardEvent) => {
-      if (e.key === 'Escape') onClose();
-    };
-    window.addEventListener('keydown', handleKeyDown);
-    return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [onClose]);
-
-  return (
-    <div className="file-viewer-overlay" onClick={onClose}>
-      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
-        <div className="file-viewer-header">
-          <span className="file-viewer-title">
-            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
-            {filename}
-          </span>
-          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
-            <img src={ICONS.close} alt="Close" />
-          </button>
-        </div>
-        <div className="file-viewer-content">
-          {loading || !wasmReady ? (
-            <div className="file-viewer-loading">Loading...</div>
-          ) : content ? (
-            <div id="readmeContent" ref={contentRef} style={{ padding: 32 }} />
-          ) : (
-            <div className="file-viewer-loading">Unable to load file</div>
-          )}
-        </div>
-      </div>
-    </div>
-  );
-}
-
-/**
- * Component: FileList
- */
-function FileList({ directories, files, onNavigate, onOpenFile }: {
-  directories: any[];
-  files: any[];
-  onNavigate: (path: string) => void;
-  onOpenFile: (path: string) => void;
-}) {
-  const isEmpty = directories.length === 0 && files.length === 0;
-
-  if (isEmpty) {
-    return (
-      <div className="file-list-container">
-        <div className="empty-state">This directory is empty.</div>
-      </div>
-    );
-  }
-
-  return (
-    <div className="file-list-container">
-       {/* Optional header row like GitHub */}
-      <div className="file-header">
-        Files
-      </div>
-
-      <div id="fileListBody">
-        {directories.map((dir) => (
-          <FileRow
-            key={dir.abspath}
-            item={dir}
-            iconUrl={ICONS.folder}
-            isDir={true}
-            onNavigate={onNavigate}
-            onOpenFile={onOpenFile}
-          />
-        ))}
-
-        {files.map((file) => (
-          <FileRow
-            key={file.abspath}
-            item={file}
-            iconUrl={ICONS.file}
-            isDir={false}
-            onNavigate={onNavigate}
-            onOpenFile={onOpenFile}
-          />
-        ))}
-      </div>
-    </div>
-  );
-}
-
-/**
- * Component: FileRow
- */
-function FileRow({ item, iconUrl, isDir, onNavigate, onOpenFile }: {
-  item: { abspath: string; basename: string };
-  iconUrl: string;
-  isDir: boolean;
-  onNavigate: (path: string) => void;
-  onOpenFile: (path: string) => void;
-}) {
-  const handleClick = (e: React.MouseEvent) => {
-    e.preventDefault();
-    if (isDir) {
-      onNavigate(item.abspath);
-    } else if (isCodeFile(item.basename)) {
-      onOpenFile(item.abspath);
-    } else {
-      // For non-code files, open in new tab
-      window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank');
-    }
-  };
-
-  const handleMouseEnter = () => {
-    // Prefetch on hover
-    if (isDir) {
-      prefetchDirectory(item.abspath);
-    } else if (isCodeFile(item.basename)) {
-      prefetchFile(item.abspath);
-    }
-  };
-
-  const href = isDir
-    ? `?path=${encodeURIComponent(item.abspath)}`
-    : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`;
-
-  return (
-    <div className="file-row" onMouseEnter={handleMouseEnter}>
-      <span className="icon">
-        <img src={iconUrl} alt={isDir ? "Directory" : "File"} />
-      </span>
-      <span className="name">
-        <a href={href} onClick={handleClick}>
-          {item.basename}
-        </a>
-      </span>
-    </div>
-  );
-}
-
-/**
- * Component: ReadmeViewer
- */
-function ReadmeViewer({ content }: { content: string | null }) {
-  const contentRef = useRef<HTMLDivElement>(null);
-  const moduleRef = useRef<any>(null);
-  const [wasmReady, setWasmReady] = useState(false);
-
-  useEffect(() => {
-    createMarkdownModule().then((Module: any) => {
-      moduleRef.current = Module;
-      setWasmReady(true);
-    });
-  }, []);
-
-  useEffect(() => {
-    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
-
-    const Module = moduleRef.current;
-    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
-    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
-
-    const ptr = markdownToHtmlPtr(content);
-    const html = Module.UTF8ToString(ptr);
-    markdownFree(ptr);
-    contentRef.current.innerHTML = html;
-  }, [content, wasmReady]);
-
-  if (!content) return null;
-
-  return (
-    <div id="readmeSection">
-      <div className="readme-header">
-        <img src={ICONS.file} width="16" alt="" style={{opacity:0.5}} />
-        README.md
-      </div>
-      <div id="readmeContent" ref={contentRef}>
-        {!wasmReady && 'Loading...'}
-      </div>
-    </div>
-  );
-}
-
-/**
- * Main Application Component
- */
-function RepoBrowser() {
-  const [currentPath, setCurrentPath] = useState(getCurrentPath());
-  const [content, setContent] = useState<{ files: any[]; directories: any[] }>({ files: [], directories: [] });
-  const [readme, setReadme] = useState<string | null>(null);
-  const [error, setError] = useState<string | null>(null);
-  const [loading, setLoading] = useState(false);
-  const [isDarkMode, setIsDarkMode] = useState(() => {
-    // Check localStorage or system preference
-    const saved = localStorage.getItem('theme');
-    if (saved) return saved === 'dark';
-    return window.matchMedia('(prefers-color-scheme: dark)').matches;
-  });
-  const [viewingFile, setViewingFile] = useState<string | null>(null);
-
-  function getCurrentPath() {
-    const params = new URLSearchParams(window.location.search);
-    return params.get('path') || '';
-  }
-
-  // Persist theme preference
-  useEffect(() => {
-    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
-  }, [isDarkMode]);
-
-  useEffect(() => {
-    const handlePopState = () => setCurrentPath(getCurrentPath());
-    window.addEventListener('popstate', handlePopState);
-    return () => window.removeEventListener('popstate', handlePopState);
-  }, []);
-
-  useEffect(() => {
-    fetchDirectory(currentPath);
-    fetchReadme(currentPath);
-  }, [currentPath]);
-
-  const navigate = (path: string) => {
-    const newUrl = path ? `?path=${encodeURIComponent(path)}` : '/';
-    window.history.pushState({ path }, '', newUrl);
-    setCurrentPath(path);
-  };
-
-  const fetchDirectory = async (path: string) => {
-    setLoading(true);
-    setError(null);
-    try {
-      // Check prefetch cache first
-      const cacheKey = `dir:${path}`;
-      let data;
-      if (prefetchCache.has(cacheKey)) {
-        data = await prefetchCache.get(cacheKey);
-        prefetchCache.delete(cacheKey); // Clear after use for fresh data next time
-      } else {
-        const url = path
-          ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
-          : `${API_BASE}/list`;
-        const response = await fetch(url);
-        if (response.ok) {
-          data = await response.json();
-        }
-      }
-
-      if (data?.error) {
-        throw new Error(data.error);
-      }
-
-      setContent({
-        files: data?.files || [],
-        directories: data?.directories || []
-      });
-    } catch (err: any) {
-      console.error('Error loading directory:', err);
-      setError(err.message);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const fetchReadme = async (path: string) => {
-    setReadme(null);
-    const readmePath = path ? `${path}/README.md` : 'README.md';
-    try {
-      const response = await fetch(`${API_BASE}/file?path=${encodeURIComponent(readmePath)}`);
-      if (response.ok) {
-        const text = await response.text();
-        setReadme(text);
-      }
-    } catch (err) {
-      // Readme is optional, ignore errors
-    }
-  };
-
-  const handleOpenFile = useCallback((path: string) => {
-    setViewingFile(path);
-  }, []);
-
-  const handleCloseFile = useCallback(() => {
-    setViewingFile(null);
-  }, []);
-
-  const toggleTheme = () => {
-    setIsDarkMode(prev => !prev);
-  };
-
-  return (
-    <>
-      <GlobalStyles isDark={isDarkMode} />
-      <div className="repo-container">
-
-        {/* Header */}
-        <div className="header">
-          <img src={ICONS.repo} alt="Repo" className="header-icon" />
-          <div>
-            <h1>Zenbu Repository</h1>
-            <p className="description">Browse and manage the mercurial codebase</p>
-          </div>
-          <button className="theme-toggle" onClick={toggleTheme} title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}>
-            {isDarkMode ? <SunIcon color="#f0c674" /> : <MoonIcon color="#6e7681" />}
-            {isDarkMode ? 'Light' : 'Dark'}
-          </button>
-        </div>
-
-        {/* Clone Bar */}
-        <div className="clone-box">
-          <div style={{display:'flex', alignItems:'center', width:'100%'}}>
-             <span className="clone-label">Clone HTTPS</span>
-             <code className="clone-url">hg clone http://zenbu.babocoder.com/repo</code>
-          </div>
-        </div>
-
-        {/* Navigation & Content */}
-        <Breadcrumb currentPath={currentPath} onNavigate={navigate} />
-
-        {error && <div className="error-message">Error: {error}</div>}
-
-        {loading ? (
-          <div className="file-list-container" style={{padding: '40px', textAlign: 'center', color: 'var(--text-secondary)'}}>
-             Loading files...
-          </div>
-        ) : (
-          <>
-            <FileList
-              directories={content.directories}
-              files={content.files}
-              onNavigate={navigate}
-              onOpenFile={handleOpenFile}
-            />
-            <ReadmeViewer content={readme} />
-          </>
-        )}
-      </div>
-
-      {/* File Viewer Modal */}
-      {viewingFile && (
-        isMarkdownFile(viewingFile) ? (
-          <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} />
-        ) : (
-          <FileViewer filePath={viewingFile} onClose={handleCloseFile} />
-        )
-      )}
-    </>
-  );
-}
-
-export { RepoBrowser };
--- a/markdown_converter/markdown_to_html.c	Sun Jan 25 10:44:04 2026 -0800
+++ b/markdown_converter/markdown_to_html.c	Sun Aug 02 16:50:48 2026 -0700
@@ -61,14 +61,6 @@
   buf->length += len;
 }
 
-static void buffer_append_n(StringBuffer *buf, const char *str, size_t n)
-{
-  buffer_grow(buf, n);
-  memcpy(buf->data + buf->length, str, n);
-  buf->length += n;
-  buf->data[buf->length] = '\0';
-}
-
 static void buffer_append_char(StringBuffer *buf, char c)
 {
   buffer_grow(buf, 1);
@@ -76,6 +68,39 @@
   buf->data[buf->length] = '\0';
 }
 
+static void buffer_append_html_escaped_n(StringBuffer *buf, const char *text, size_t len)
+{
+  for (size_t i = 0; i < len; i++) {
+    switch (text[i]) {
+      case '&': buffer_append(buf, "&amp;"); break;
+      case '<': buffer_append(buf, "&lt;"); break;
+      case '>': buffer_append(buf, "&gt;"); break;
+      case '"': buffer_append(buf, "&quot;"); break;
+      case '\'': buffer_append(buf, "&#39;"); break;
+      default: buffer_append_char(buf, text[i]); break;
+    }
+  }
+}
+
+static int is_safe_url(const char *url, size_t len, int is_image)
+{
+  if (len == 0) return 0;
+
+  for (size_t i = 0; i < len; i++) {
+    unsigned char c = (unsigned char)url[i];
+    if (iscntrl(c) || isspace(c)) return 0;
+  }
+
+  const char *colon = memchr(url, ':', len);
+  if (!colon) return 1;
+
+  size_t scheme_len = (size_t)(colon - url);
+  if (scheme_len == 4 && strncasecmp(url, "http", scheme_len) == 0) return 1;
+  if (scheme_len == 5 && strncasecmp(url, "https", scheme_len) == 0) return 1;
+  if (!is_image && scheme_len == 6 && strncasecmp(url, "mailto", scheme_len) == 0) return 1;
+  return 0;
+}
+
 static void buffer_free(StringBuffer *buf)
 {
   if (buf) {
@@ -160,28 +185,6 @@
   return 1;
 }
 
-// Check if line starts with a specific HTML tag (e.g., "script", "style")
-static int is_html_tag(const char *line, const char *tag)
-{
-  line = skip_whitespace(line);
-  if (*line != '<') return 0;
-  line++;
-
-  // Skip optional /
-  int is_closing = 0;
-  if (*line == '/') {
-    is_closing = 1;
-    line++;
-  }
-
-  size_t tag_len = strlen(tag);
-  if (strncasecmp(line, tag, tag_len) != 0) return 0;
-
-  char next = line[tag_len];
-  // Tag must be followed by space, >, or end for closing tags
-  return next == '>' || next == ' ' || next == '\t' || next == '\n' || next == '\0';
-}
-
 // Check if line is ordered list item
 static int is_ordered_list(const char *line)
 {
@@ -359,11 +362,16 @@
         while (url_end < len && text[url_end] != ')') url_end++;
 
         if (url_end < len) {
-          buffer_append(buf, "<a href=\"");
-          buffer_append_n(buf, text + url_start, url_end - url_start);
-          buffer_append(buf, "\">");
-          buffer_append_n(buf, text + link_start, link_end - link_start);
-          buffer_append(buf, "</a>");
+          size_t url_len = url_end - url_start;
+          if (is_safe_url(text + url_start, url_len, 0)) {
+            buffer_append(buf, "<a href=\"");
+            buffer_append_html_escaped_n(buf, text + url_start, url_len);
+            buffer_append(buf, "\">");
+            process_inline(buf, text + link_start, link_end - link_start);
+            buffer_append(buf, "</a>");
+          } else {
+            process_inline(buf, text + link_start, link_end - link_start);
+          }
           i = url_end + 1;
           continue;
         }
@@ -382,11 +390,16 @@
         while (url_end < len && text[url_end] != ')') url_end++;
 
         if (url_end < len) {
-          buffer_append(buf, "<img src=\"");
-          buffer_append_n(buf, text + url_start, url_end - url_start);
-          buffer_append(buf, "\" alt=\"");
-          buffer_append_n(buf, text + alt_start, alt_end - alt_start);
-          buffer_append(buf, "\">");
+          size_t url_len = url_end - url_start;
+          if (is_safe_url(text + url_start, url_len, 1)) {
+            buffer_append(buf, "<img src=\"");
+            buffer_append_html_escaped_n(buf, text + url_start, url_len);
+            buffer_append(buf, "\" alt=\"");
+            buffer_append_html_escaped_n(buf, text + alt_start, alt_end - alt_start);
+            buffer_append(buf, "\">");
+          } else {
+            buffer_append_html_escaped_n(buf, text + alt_start, alt_end - alt_start);
+          }
           i = url_end + 1;
           continue;
         }
@@ -449,25 +462,14 @@
 
       if (end < len) {
         buffer_append(buf, "<code>");
-        buffer_append_n(buf, text + start, end - start);
+        buffer_append_html_escaped_n(buf, text + start, end - start);
         buffer_append(buf, "</code>");
         i = end + 1;
         continue;
       }
     }
 
-    // This might not be needed for now.
-    // HTML escape special characters
-    // if (text[i] == '<') {
-    //   buffer_append(buf, "&lt;");
-    // } else if (text[i] == '>') {
-    //   buffer_append(buf, "&gt;");
-    // } else if (text[i] == '&') {
-    //   buffer_append(buf, "&amp;");
-    // } else {
-    //   buffer_append_char(buf, text[i]);
-    // }
-    buffer_append_char(buf, text[i]);
+    buffer_append_html_escaped_n(buf, text + i, 1);
     i++;
   }
 }
@@ -759,48 +761,11 @@
       }
     }
 
-    // HTML block - pass through unchanged
+    // Repository markdown is untrusted. Render raw HTML as text.
     if (is_html_block_start(line)) {
-      // Check if it's a script or style tag that needs special handling
-      int is_script = is_html_tag(line, "script");
-      int is_style = is_html_tag(line, "style");
-
-      if (is_script || is_style) {
-        const char *end_tag = is_script ? "</script>" : "</style>";
-
-        // Output the opening line
-        buffer_append(buf, line);
-        buffer_append_char(buf, '\n');
-
-        free(line);
-        if (*ptr == '\n') ptr++;
-
-        // Collect content until closing tag
-        while (*ptr) {
-          line_start = ptr;
-          while (*ptr && *ptr != '\n') ptr++;
-          line_len = ptr - line_start;
-
-          line = (char *)malloc(line_len + 1);
-          if (!line) break;
-          memcpy(line, line_start, line_len);
-          line[line_len] = '\0';
-
-          buffer_append(buf, line);
-          buffer_append_char(buf, '\n');
-
-          int found_end = (strstr(line, end_tag) != NULL);
-          free(line);
-          if (*ptr == '\n') ptr++;
-
-          if (found_end) break;
-        }
-        continue;
-      }
-
-      // Regular HTML tag - just pass through the line
-      buffer_append(buf, line);
-      buffer_append_char(buf, '\n');
+      buffer_append(buf, "<p>");
+      process_inline(buf, line, line_len);
+      buffer_append(buf, "</p>");
       free(line);
       if (*ptr == '\n') ptr++;
       continue;
--- a/markdown_converter/tests/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ b/markdown_converter/tests/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -1,3 +1,11 @@
+load("@rules_cc//cc:cc_test.bzl", "cc_test")
+
+cc_test(
+    name = "markdown_to_html_test",
+    srcs = ["markdown_to_html_test.c"],
+    deps = ["//markdown_converter:markdown_to_html_c"],
+)
+
 # load("//gui_ze:gui_ze.bzl", "bun_run", "move_files_into_dir")
 # 
 # # Test for WASM module (run with: bazel test //markdown_converter:markdown_to_html_wasm_test)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markdown_converter/tests/markdown_to_html_test.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,32 @@
+#include "markdown_converter/markdown_to_html.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int failures = 0;
+
+static void expect_equal(const char *name, const char *markdown, const char *expected)
+{
+  char *actual = markdown_to_html(markdown);
+  if (!actual || strcmp(actual, expected) != 0) {
+    fprintf(stderr, "%s\nexpected: %s\nactual:   %s\n", name, expected, actual ? actual : "(null)");
+    failures++;
+  }
+  markdown_free(actual);
+}
+
+int main(void)
+{
+  expect_equal("normal link", "[link](https://example.com)",
+               "<p><a href=\"https://example.com\">link</a></p>");
+  expect_equal("escape paragraph", "a & <b>", "<p>a &amp; &lt;b&gt;</p>");
+  expect_equal("escape raw script", "<script>alert('x')</script>",
+               "<p>&lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt;</p>");
+  expect_equal("reject script link", "[open](javascript:alert)", "<p>open</p>");
+  expect_equal("reject data image", "![preview](data:image/svg+xml,test)", "<p>preview</p>");
+  expect_equal("escape link attribute", "[link](https://example.com/\"x)",
+               "<p><a href=\"https://example.com/&quot;x\">link</a></p>");
+  expect_equal("escape inline code", "`<script>`", "<p><code>&lt;script&gt;</code></p>");
+
+  return failures == 0 ? 0 : 1;
+}
--- a/medi/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
-load("//gui_ze:gui_ze.bzl", "bun_bundle", "move_files_into_dir")
-
-cc_binary(
-    name = "medi",
-    srcs = ["main.c"],
-    deps = [
-        "//seobeo:seobeo",
-        "//media_processor:media_processor",
-        "//dowa:dowa",
-    ],
-    data = [
-        ":all_assets",
-    ],
-)
-
-filegroup(
-  name = "all_assets",
-  srcs = glob(["src/**"]) + [":compiled_js"],
-)
-
-move_files_into_dir(
-  name = "compiled_js",
-  srcs = [
-    ":medi_bundle",
-  ],
-  dest = "src",
-)
-
-filegroup(
-  name = "src_ts_files",
-  srcs = glob([
-    "src/**/*.ts",
-    "src/**/*.tsx",
-    "src/**/*.js",
-    "src/**/*.jsx",
-  ], allow_empty = True),
-)
-
-
-
-bun_bundle(
-  name = "medi_bundle",
-  src = "src/main.tsx",
-  deps = [
-    ":src_ts_files"
-  ],
-  visibility = ["//visibility:public"],
-)
--- a/medi/README.md	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-# medi
-
-Media upload and processing server. Upload images and videos, get optimized WebP images and HLS video streams.
-
-## Features
-
-- **Image Upload**: Converts to WebP format with configurable quality
-- **Video Upload**: Converts to HLS streaming format with thumbnails
-- **Background Processing**: Non-blocking uploads with status polling
-- **Web UI**: Drag-and-drop upload interface
-- **REST API**: Programmatic access to upload and retrieve media
-
-## Running
-
-```bash
-# Build and run
-bazel run //medi:medi
-
-# Run on custom port
-bazel run //medi:medi -- 3000
-```
-
-Server starts at `http://localhost:8080` by default.
-
-## API
-
-### Upload Media
-
-```bash
-POST /api/upload
-Content-Type: multipart/form-data
-
-# Response
-{
-  "id": "1706123456_12345_0",
-  "type": "image",
-  "status": "processing"
-}
-```
-
-### Get Media Info
-
-```bash
-GET /api/media/{id}
-
-# Response (completed image)
-{
-  "id": "1706123456_12345_0",
-  "type": "image",
-  "status": "completed",
-  "webp": "/media/1706123456_12345_0/image.webp",
-  "original": "/media/1706123456_12345_0/original.jpg"
-}
-
-# Response (completed video)
-{
-  "id": "1706123456_12345_1",
-  "type": "video",
-  "status": "completed",
-  "hls": "/media/1706123456_12345_1/video.m3u8",
-  "thumbnail": "/media/1706123456_12345_1/video_thumb.jpg",
-  "original": "/media/1706123456_12345_1/original.mp4"
-}
-```
-
-### List All Media
-
-```bash
-GET /api/media
-
-# Response
-{
-  "items": [
-    {"id": "1706123456_12345_0", "type": "image"},
-    {"id": "1706123456_12345_1", "type": "video"}
-  ]
-}
-```
-
-## Output Structure
-
-```
-uploads/          # Temporary upload storage (deleted after processing)
-media/            # Processed media
-├── {id}/
-│   ├── original.{ext}   # Original uploaded file
-│   ├── image.webp       # (images) WebP conversion
-│   ├── video.m3u8       # (videos) HLS playlist
-│   ├── video_000.ts     # (videos) HLS segments
-│   └── video_thumb.jpg  # (videos) Thumbnail
-```
-
-## Supported Formats
-
-### Images
-JPG, JPEG, PNG, GIF, WebP, BMP, TIFF, HEIC, AVIF
-
-### Videos
-MP4, MOV, AVI, MKV, WebM, FLV, WMV, M4V, 3GP
-
-## Dependencies
-
-- `//seobeo:seobeo` - HTTP server
-- `//media_processor:media_processor` - Media processing
-- FFmpeg (system installed)
-
-## Building
-
-```bash
-bazel build //medi:medi
-```
--- a/medi/main.c	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,531 +0,0 @@
-#include "seobeo/seobeo.h"
-#include "media_processor/media_processor.h"
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <dirent.h>
-#include <pthread.h>
-#include <unistd.h>
-
-#define UPLOAD_DIR "./uploads"
-#define PROCESSED_DIR "./media"
-#define MAX_UPLOAD_SIZE (100 * 1024 * 1024)  // 100MB
-
-// Background processing queue
-typedef struct ProcessJob {
-    char* input_path;
-    char* id;
-    MediaType type;
-    struct ProcessJob* next;
-} ProcessJob;
-
-static ProcessJob* job_queue_head = NULL;
-static ProcessJob* job_queue_tail = NULL;
-static pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
-static pthread_cond_t job_queue_cond = PTHREAD_COND_INITIALIZER;
-
-// ============================================================================
-// Helper Functions
-// ============================================================================
-
-static void ensure_directories(void) {
-    mkdir(UPLOAD_DIR, 0755);
-    mkdir(PROCESSED_DIR, 0755);
-}
-
-static char* extract_filename_from_content_disposition(const char* header, Dowa_Arena* arena) {
-    const char* filename_start = strstr(header, "filename=\"");
-    if (!filename_start) return NULL;
-
-    filename_start += 10;  // Skip 'filename="'
-    const char* filename_end = strchr(filename_start, '"');
-    if (!filename_end) return NULL;
-
-    size_t len = filename_end - filename_start;
-    char* filename = Dowa_Arena_Allocate(arena, len + 1);
-    memcpy(filename, filename_start, len);
-    filename[len] = '\0';
-    return filename;
-}
-
-static char* find_multipart_boundary(const char* content_type, Dowa_Arena* arena) {
-    const char* boundary_start = strstr(content_type, "boundary=");
-    if (!boundary_start) return NULL;
-
-    boundary_start += 9;  // Skip 'boundary='
-
-    // Handle quoted boundary
-    if (*boundary_start == '"') {
-        boundary_start++;
-        const char* boundary_end = strchr(boundary_start, '"');
-        if (!boundary_end) return NULL;
-        size_t len = boundary_end - boundary_start;
-        char* boundary = Dowa_Arena_Allocate(arena, len + 1);
-        memcpy(boundary, boundary_start, len);
-        boundary[len] = '\0';
-        return boundary;
-    }
-
-    // Unquoted boundary
-    size_t len = strlen(boundary_start);
-    char* boundary = Dowa_Arena_Allocate(arena, len + 1);
-    strcpy(boundary, boundary_start);
-    // Trim whitespace/newline
-    while (len > 0 && (boundary[len-1] == '\r' || boundary[len-1] == '\n' || boundary[len-1] == ' ')) {
-        boundary[--len] = '\0';
-    }
-    return boundary;
-}
-
-// ============================================================================
-// Background Processing Worker
-// ============================================================================
-
-static void enqueue_job(const char* input_path, const char* id, MediaType type) {
-    ProcessJob* job = malloc(sizeof(ProcessJob));
-    job->input_path = strdup(input_path);
-    job->id = strdup(id);
-    job->type = type;
-    job->next = NULL;
-
-    pthread_mutex_lock(&job_queue_mutex);
-    if (job_queue_tail) {
-        job_queue_tail->next = job;
-        job_queue_tail = job;
-    } else {
-        job_queue_head = job_queue_tail = job;
-    }
-    pthread_cond_signal(&job_queue_cond);
-    pthread_mutex_unlock(&job_queue_mutex);
-}
-
-static void* processing_worker(void* arg) {
-    (void)arg;
-
-    while (1) {
-        pthread_mutex_lock(&job_queue_mutex);
-        while (!job_queue_head) {
-            pthread_cond_wait(&job_queue_cond, &job_queue_mutex);
-        }
-        ProcessJob* job = job_queue_head;
-        job_queue_head = job->next;
-        if (!job_queue_head) job_queue_tail = NULL;
-        pthread_mutex_unlock(&job_queue_mutex);
-
-        // Process the job
-        Seobeo_Log(SEOBEO_INFO, "Processing media: %s (id=%s)", job->input_path, job->id);
-
-        MediaProcessorOpts opts = media_processor_opts_default(PROCESSED_DIR);
-        MediaResult* result = media_process_file(job->input_path, job->id, &opts);
-
-        if (result->status == MEDIA_STATUS_COMPLETED) {
-            Seobeo_Log(SEOBEO_INFO, "Processing completed for %s", job->id);
-        } else {
-            Seobeo_Log(SEOBEO_ERROR, "Processing failed for %s: %s", job->id, result->error_message);
-        }
-
-        media_result_free(result);
-
-        // Clean up temp upload file
-        unlink(job->input_path);
-
-        free(job->input_path);
-        free(job->id);
-        free(job);
-    }
-
-    return NULL;
-}
-
-void Seobeo_Render_Html(
-    char *final_body,
-    char *template,
-    Dowa_Arena *arena
-)
-{
-  size_t current_offset = 0;
-  char *cursor = template;
-
-  int32 token_len = 2;
-
-  while (1)
-  {
-    char *start_tag = strstr(cursor, "{{");
-    if (!start_tag) break;
-
-    char *end_tag = strstr(start_tag, "}}");
-    if (!end_tag) break;
-
-    size_t leading_len = start_tag - cursor;
-    memcpy(final_body + current_offset, cursor, leading_len);
-    current_offset += leading_len;
-
-    size_t name_len = end_tag - (start_tag + token_len);
-    char *include_name = Dowa_Arena_Allocate(arena, name_len + 1);
-    memcpy(include_name, start_tag + token_len, name_len);
-    include_name[name_len] = '\0';
-
-    size_t sub_file_size = 0;
-    char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size);
-    Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content);
-    if (sub_content)
-    {
-      memcpy(final_body + current_offset, sub_content, sub_file_size);
-      current_offset += sub_file_size;
-      free(sub_content);
-    }
-
-    cursor = end_tag + 2;
-  }
-  strcpy(final_body + current_offset, cursor);
-}
-
-
-void Seobeo_Render_Html_FilePath(
-    char *final_body,
-    char *path,
-    Dowa_Arena *arena
-) {
-  Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path);
-  size_t html_size = 0;
-  char *template = Seobeo_Web_LoadFile(path, &html_size);
-  if (!template) return;
-  Seobeo_Render_Html(final_body, template, arena);
-}
-
-// ============================================================================
-// API Route Handlers
-// ============================================================================
-
-// GET / - Serve the main HTML page
-Seobeo_Request_Entry* handle_index(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
-  Seobeo_Request_Entry *resp = NULL; 
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/index.html", arena);
-  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
-  return resp;
-}
-
-// POST /api/upload - Handle file upload
-Seobeo_Request_Entry* handle_upload(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
-    const char* content_type = Dowa_HashMap_Get(req, "content-type");
-    const char* body = Dowa_HashMap_Get(req, "Body");
-    const char* content_length_str = Dowa_HashMap_Get(req, "content-length");
-
-    Seobeo_Request_Entry* resp = NULL;
-
-    if (!content_type || !body) {
-        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing content\"}", arena);
-        return resp;
-    }
-
-    size_t content_length = content_length_str ? atol(content_length_str) : strlen(body);
-
-    if (content_length > MAX_UPLOAD_SIZE) {
-        Dowa_HashMap_Push_Arena(resp, "status", "413", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"File too large\"}", arena);
-        return resp;
-    }
-
-    // Parse multipart form data
-    char* boundary = find_multipart_boundary(content_type, arena);
-    if (!boundary) {
-        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid multipart data\"}", arena);
-        return resp;
-    }
-
-    // Find file content in multipart data
-    char boundary_marker[256];
-    snprintf(boundary_marker, sizeof(boundary_marker), "--%s", boundary);
-
-    const char* part_start = strstr(body, boundary_marker);
-    if (!part_start) {
-        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"No file found\"}", arena);
-        return resp;
-    }
-
-    // Skip to headers
-    part_start = strstr(part_start, "\r\n");
-    if (!part_start) {
-        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid format\"}", arena);
-        return resp;
-    }
-    part_start += 2;
-
-    // Find filename from Content-Disposition
-    char* filename = NULL;
-    const char* header_end = strstr(part_start, "\r\n\r\n");
-    if (header_end) {
-        char headers[1024] = {0};
-        size_t headers_len = header_end - part_start;
-        if (headers_len < sizeof(headers)) {
-            memcpy(headers, part_start, headers_len);
-            filename = extract_filename_from_content_disposition(headers, arena);
-        }
-    }
-
-    if (!filename) {
-        filename = "upload";
-    }
-
-    // Find file content
-    const char* file_start = header_end + 4;  // Skip \r\n\r\n
-
-    // Find end boundary
-    char end_boundary[256];
-    snprintf(end_boundary, sizeof(end_boundary), "\r\n--%s", boundary);
-    const char* file_end = strstr(file_start, end_boundary);
-
-    if (!file_end) {
-        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Incomplete upload\"}", arena);
-        return resp;
-    }
-
-    size_t file_size = file_end - file_start;
-
-    // Generate ID and determine type
-    char* id = media_generate_id();
-    MediaType type = media_detect_type(filename);
-
-    if (type == MEDIA_TYPE_UNKNOWN) {
-        free(id);
-        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Unsupported file type\"}", arena);
-        return resp;
-    }
-
-    // Save to temp file
-    const char* ext = media_get_extension(filename);
-    char temp_path[512];
-    snprintf(temp_path, sizeof(temp_path), "%s/%s%s", UPLOAD_DIR, id, ext ? ext : "");
-
-    FILE* f = fopen(temp_path, "wb");
-    if (!f) {
-        free(id);
-        Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Failed to save file\"}", arena);
-        return resp;
-    }
-
-    fwrite(file_start, 1, file_size, f);
-    fclose(f);
-
-    // Enqueue for processing
-    enqueue_job(temp_path, id, type);
-
-    // Return success with ID
-    char response_body[256];
-    snprintf(response_body, sizeof(response_body),
-        "{\"id\":\"%s\",\"type\":\"%s\",\"status\":\"processing\"}",
-        id, type == MEDIA_TYPE_IMAGE ? "image" : "video");
-
-    char* body_copy = Dowa_Arena_Allocate(arena, strlen(response_body) + 1);
-    strcpy(body_copy, response_body);
-
-    Dowa_HashMap_Push_Arena(resp, "status", "202", arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-    Dowa_HashMap_Push_Arena(resp, "Body", body_copy, arena);
-
-    free(id);
-    return resp;
-}
-
-// GET /api/media/:id - Get media info/status
-Seobeo_Request_Entry* handle_media_info(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
-    const char* path = Dowa_HashMap_Get(req, "path");
-
-    Seobeo_Request_Entry* resp = NULL;
-
-    // Extract ID from path (e.g., /api/media/123456)
-    const char* id = path + strlen("/api/media/");
-    if (!id || strlen(id) == 0) {
-        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing media ID\"}", arena);
-        return resp;
-    }
-
-    // Check if processed directory exists
-    char media_dir[512];
-    snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, id);
-
-    struct stat st;
-    if (stat(media_dir, &st) != 0) {
-        // Check if still processing
-        char upload_pattern[512];
-        snprintf(upload_pattern, sizeof(upload_pattern), "%s/%s", UPLOAD_DIR, id);
-
-        DIR* dir = opendir(UPLOAD_DIR);
-        boolean found = FALSE;
-        if (dir) {
-            struct dirent* entry;
-            while ((entry = readdir(dir)) != NULL) {
-                if (strncmp(entry->d_name, id, strlen(id)) == 0) {
-                    found = TRUE;
-                    break;
-                }
-            }
-            closedir(dir);
-        }
-
-        if (found) {
-            Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
-            Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-            Dowa_HashMap_Push_Arena(resp, "Body", "{\"status\":\"processing\"}", arena);
-        } else {
-            Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
-            Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-            Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Media not found\"}", arena);
-        }
-        return resp;
-    }
-
-    // Check what files exist
-    char webp_path[512], hls_path[512], thumb_path[512];
-    snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
-    snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir);
-    snprintf(thumb_path, sizeof(thumb_path), "%s/video_thumb.jpg", media_dir);
-
-    boolean is_image = (stat(webp_path, &st) == 0);
-    boolean is_video = (stat(hls_path, &st) == 0);
-
-    char response[1024];
-    if (is_image) {
-        snprintf(response, sizeof(response),
-            "{\"id\":\"%s\",\"type\":\"image\",\"status\":\"completed\","
-            "\"webp\":\"/media/%s/image.webp\","
-            "\"original\":\"/media/%s/original\"}",
-            id, id, id);
-    } else if (is_video) {
-        snprintf(response, sizeof(response),
-            "{\"id\":\"%s\",\"type\":\"video\",\"status\":\"completed\","
-            "\"hls\":\"/media/%s/video.m3u8\","
-            "\"thumbnail\":\"/media/%s/video_thumb.jpg\","
-            "\"original\":\"/media/%s/original\"}",
-            id, id, id, id);
-    } else {
-        snprintf(response, sizeof(response), "{\"id\":\"%s\",\"status\":\"processing\"}", id);
-    }
-
-    char* body = Dowa_Arena_Allocate(arena, strlen(response) + 1);
-    strcpy(body, response);
-
-    Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-    Dowa_HashMap_Push_Arena(resp, "Body", body, arena);
-
-    return resp;
-}
-
-// GET /api/media - List all media
-Seobeo_Request_Entry* handle_media_list(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
-    (void)req;
-
-    Seobeo_Request_Entry* resp = NULL;
-
-    DIR* dir = opendir(PROCESSED_DIR);
-    if (!dir) {
-        Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-        Dowa_HashMap_Push_Arena(resp, "Body", "{\"items\":[]}", arena);
-        return resp;
-    }
-
-    // Build JSON array
-    char json[16384] = "{\"items\":[";
-    size_t json_len = strlen(json);
-    boolean first = TRUE;
-
-    struct dirent* entry;
-    while ((entry = readdir(dir)) != NULL) {
-        if (entry->d_name[0] == '.') continue;
-
-        char media_dir[512];
-        snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, entry->d_name);
-
-        struct stat st;
-        if (stat(media_dir, &st) != 0 || !S_ISDIR(st.st_mode)) continue;
-
-        // Check type
-        char webp_path[512], hls_path[512];
-        snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
-        snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir);
-
-        const char* type = "unknown";
-        if (stat(webp_path, &st) == 0) type = "image";
-        else if (stat(hls_path, &st) == 0) type = "video";
-
-        char item[256];
-        snprintf(item, sizeof(item), "%s{\"id\":\"%s\",\"type\":\"%s\"}",
-            first ? "" : ",", entry->d_name, type);
-
-        size_t item_len = strlen(item);
-        if (json_len + item_len + 10 < sizeof(json)) {
-            strcpy(json + json_len, item);
-            json_len += item_len;
-            first = FALSE;
-        }
-    }
-
-    closedir(dir);
-
-    strcpy(json + json_len, "]}");
-
-    char* body = Dowa_Arena_Allocate(arena, strlen(json) + 1);
-    strcpy(body, json);
-
-    Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-    Dowa_HashMap_Push_Arena(resp, "Body", body, arena);
-
-    return resp;
-}
-
-// ============================================================================
-// Main
-// ============================================================================
-
-int main(int argc, char** argv) {
-    const char* port = "8080";
-
-    printf("===========================================\n");
-    printf("  Medi - Media Upload & Processing Server\n");
-    printf("===========================================\n");
-    printf("Starting on port %s...\n\n", port);
-
-    // Create directories
-    ensure_directories();
-
-    // Start background processing worker
-    pthread_t worker_thread;
-    pthread_create(&worker_thread, NULL, processing_worker, NULL);
-
-    // Initialize router
-    Seobeo_Router_Init();
-
-    // Register routes
-    Seobeo_Router_Register("GET", "/", handle_index);
-    Seobeo_Router_Register("POST", "/api/upload", handle_upload);
-    Seobeo_Router_Register("GET", "/api/media", handle_media_list);
-    Seobeo_Router_Register("GET", "/api/media/*", handle_media_info);
-
-    // Start server (serves static files from ./media for /media/* paths)
-    Seobeo_Web_Server_Start("medi/src", port, SEOBEO_MODE_EDGE, 4);
-
-    return 0;
-}
--- a/medi/src/app.tsx	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,232 +0,0 @@
-import React, { useState, useEffect, useCallback, useRef } from 'react';
-
-interface MediaItem {
-  id: string;
-  type: 'image' | 'video';
-  status: 'processing' | 'completed' | 'error';
-  webp?: string;
-  hls?: string;
-  thumbnail?: string;
-  original?: string;
-  error?: string;
-}
-
-const UploadIcon = () => (
-  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
-    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
-    <polyline points="17 8 12 3 7 8" />
-    <line x1="12" y1="3" x2="12" y2="15" />
-  </svg>
-);
-
-export function App() {
-  const [media, setMedia] = useState<MediaItem[]>([]);
-  const [uploading, setUploading] = useState(false);
-  const [dragover, setDragover] = useState(false);
-  const fileInputRef = useRef<HTMLInputElement>(null);
-
-  // Fetch media list on mount
-  useEffect(() => {
-    fetchMediaList();
-  }, []);
-
-  // Poll for processing status
-  useEffect(() => {
-    const processingItems = media.filter(m => m.status === 'processing');
-    if (processingItems.length === 0) return;
-
-    const interval = setInterval(() => {
-      processingItems.forEach(item => {
-        checkMediaStatus(item.id);
-      });
-    }, 2000);
-
-    return () => clearInterval(interval);
-  }, [media]);
-
-  const fetchMediaList = async () => {
-    try {
-      const res = await fetch('/api/media');
-      const data = await res.json();
-
-      // Fetch details for each item
-      const items = await Promise.all(
-        data.items.map(async (item: { id: string; type: string }) => {
-          const infoRes = await fetch(`/api/media/${item.id}`);
-          return infoRes.json();
-        })
-      );
-
-      setMedia(items);
-    } catch (err) {
-      console.error('Failed to fetch media list:', err);
-    }
-  };
-
-  const checkMediaStatus = async (id: string) => {
-    try {
-      const res = await fetch(`/api/media/${id}`);
-      const data = await res.json();
-
-      setMedia(prev => prev.map(item =>
-        item.id === id ? { ...item, ...data } : item
-      ));
-    } catch (err) {
-      console.error('Failed to check status:', err);
-    }
-  };
-
-  const uploadFile = async (file: File) => {
-    const formData = new FormData();
-    formData.append('file', file);
-
-    setUploading(true);
-
-    try {
-      const res = await fetch('/api/upload', {
-        method: 'POST',
-        body: formData,
-      });
-
-      const data = await res.json();
-
-      if (data.error) {
-        throw new Error(data.error);
-      }
-
-      // Add to list with processing status
-      setMedia(prev => [{
-        id: data.id,
-        type: data.type,
-        status: 'processing',
-      }, ...prev]);
-
-    } catch (err: any) {
-      console.error('Upload failed:', err);
-      alert('Upload failed: ' + err.message);
-    } finally {
-      setUploading(false);
-    }
-  };
-
-  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
-    const files = e.target.files;
-    if (files && files.length > 0) {
-      Array.from(files).forEach(uploadFile);
-    }
-    e.target.value = '';
-  };
-
-  const handleDrop = useCallback((e: React.DragEvent) => {
-    e.preventDefault();
-    setDragover(false);
-
-    const files = e.dataTransfer.files;
-    if (files && files.length > 0) {
-      Array.from(files).forEach(uploadFile);
-    }
-  }, []);
-
-  const handleDragOver = useCallback((e: React.DragEvent) => {
-    e.preventDefault();
-    setDragover(true);
-  }, []);
-
-  const handleDragLeave = useCallback(() => {
-    setDragover(false);
-  }, []);
-
-  return (
-    <>
-      <h1>Medi</h1>
-      <p className="subtitle">Upload images and videos for processing</p>
-
-      <div
-        className={`upload-area ${dragover ? 'dragover' : ''}`}
-        onClick={() => fileInputRef.current?.click()}
-        onDrop={handleDrop}
-        onDragOver={handleDragOver}
-        onDragLeave={handleDragLeave}
-      >
-        <input
-          ref={fileInputRef}
-          type="file"
-          accept="image/*,video/*"
-          multiple
-          onChange={handleFileSelect}
-        />
-        {uploading ? (
-          <>
-            <div className="loading-spinner" />
-            <h3>Uploading...</h3>
-          </>
-        ) : (
-          <>
-            <UploadIcon />
-            <h3>Drop files here or click to upload</h3>
-            <p>Supports images (JPG, PNG, GIF, WebP) and videos (MP4, MOV, WebM)</p>
-          </>
-        )}
-      </div>
-
-      {media.length === 0 ? (
-        <div className="empty-state">
-          <p>No media uploaded yet</p>
-        </div>
-      ) : (
-        <div className="media-grid">
-          {media.map(item => (
-            <MediaCard key={item.id} item={item} />
-          ))}
-        </div>
-      )}
-    </>
-  );
-}
-
-function MediaCard({ item }: { item: MediaItem }) {
-  const [videoError, setVideoError] = useState(false);
-
-  return (
-    <div className="media-card">
-      <div className="media-preview">
-        {item.status === 'processing' ? (
-          <div className="loading-spinner" />
-        ) : item.type === 'image' && item.webp ? (
-          <img src={item.webp} alt={item.id} />
-        ) : item.type === 'video' && item.thumbnail ? (
-          <img src={item.thumbnail} alt={item.id} />
-        ) : (
-          <span style={{ color: '#8b949e' }}>No preview</span>
-        )}
-      </div>
-
-      <div className="media-info">
-        <h4>{item.id}</h4>
-        <span className="type">{item.type}</span>
-        <div>
-          <span className={`status ${item.status}`}>
-            {item.status === 'processing' ? 'Processing...' :
-             item.status === 'completed' ? 'Ready' : 'Error'}
-          </span>
-        </div>
-      </div>
-
-      {item.status === 'completed' && (
-        <div className="media-links">
-          {item.type === 'image' ? (
-            <>
-              <a href={item.webp} target="_blank" rel="noreferrer">WebP</a>
-              <a href={item.original} target="_blank" rel="noreferrer">Original</a>
-            </>
-          ) : (
-            <>
-              <a href={item.hls} target="_blank" rel="noreferrer">HLS Stream</a>
-              <a href={item.thumbnail} target="_blank" rel="noreferrer">Thumbnail</a>
-            </>
-          )}
-        </div>
-      )}
-    </div>
-  );
-}
--- a/medi/src/index.html	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Medi - Media Upload</title>
-    <style>
-        :root {
-            --bg: #0d1117;
-            --bg-secondary: #161b22;
-            --border: #30363d;
-            --text: #e6edf3;
-            --text-secondary: #8b949e;
-            --accent: #58a6ff;
-            --success: #3fb950;
-            --error: #f85149;
-        }
-
-        * {
-            box-sizing: border-box;
-            margin: 0;
-            padding: 0;
-        }
-
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-            background: var(--bg);
-            color: var(--text);
-            min-height: 100vh;
-        }
-
-        #root {
-            max-width: 1200px;
-            margin: 0 auto;
-            padding: 40px 20px;
-        }
-
-        h1 {
-            font-size: 2rem;
-            margin-bottom: 8px;
-        }
-
-        .subtitle {
-            color: var(--text-secondary);
-            margin-bottom: 32px;
-        }
-
-        .upload-area {
-            border: 2px dashed var(--border);
-            border-radius: 12px;
-            padding: 60px 40px;
-            text-align: center;
-            cursor: pointer;
-            transition: all 0.2s;
-            background: var(--bg-secondary);
-            margin-bottom: 32px;
-        }
-
-        .upload-area:hover, .upload-area.dragover {
-            border-color: var(--accent);
-            background: rgba(88, 166, 255, 0.1);
-        }
-
-        .upload-area svg {
-            width: 64px;
-            height: 64px;
-            margin-bottom: 16px;
-            color: var(--text-secondary);
-        }
-
-        .upload-area h3 {
-            margin-bottom: 8px;
-        }
-
-        .upload-area p {
-            color: var(--text-secondary);
-            font-size: 14px;
-        }
-
-        .upload-area input {
-            display: none;
-        }
-
-        .media-grid {
-            display: grid;
-            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
-            gap: 20px;
-        }
-
-        .media-card {
-            background: var(--bg-secondary);
-            border: 1px solid var(--border);
-            border-radius: 12px;
-            overflow: hidden;
-        }
-
-        .media-preview {
-            aspect-ratio: 16/9;
-            background: #000;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            position: relative;
-        }
-
-        .media-preview img, .media-preview video {
-            max-width: 100%;
-            max-height: 100%;
-            object-fit: contain;
-        }
-
-        .media-info {
-            padding: 16px;
-        }
-
-        .media-info h4 {
-            font-size: 14px;
-            margin-bottom: 4px;
-            word-break: break-all;
-        }
-
-        .media-info .type {
-            font-size: 12px;
-            color: var(--text-secondary);
-            text-transform: uppercase;
-        }
-
-        .media-info .status {
-            font-size: 12px;
-            margin-top: 8px;
-            padding: 4px 8px;
-            border-radius: 4px;
-            display: inline-block;
-        }
-
-        .status.processing {
-            background: rgba(88, 166, 255, 0.2);
-            color: var(--accent);
-        }
-
-        .status.completed {
-            background: rgba(63, 185, 80, 0.2);
-            color: var(--success);
-        }
-
-        .status.error {
-            background: rgba(248, 81, 73, 0.2);
-            color: var(--error);
-        }
-
-        .media-links {
-            padding: 0 16px 16px;
-            display: flex;
-            gap: 8px;
-        }
-
-        .media-links a {
-            font-size: 12px;
-            color: var(--accent);
-            text-decoration: none;
-        }
-
-        .media-links a:hover {
-            text-decoration: underline;
-        }
-
-        .loading-spinner {
-            width: 40px;
-            height: 40px;
-            border: 3px solid var(--border);
-            border-top-color: var(--accent);
-            border-radius: 50%;
-            animation: spin 1s linear infinite;
-        }
-
-        @keyframes spin {
-            to { transform: rotate(360deg); }
-        }
-
-        .empty-state {
-            text-align: center;
-            padding: 60px 20px;
-            color: var(--text-secondary);
-        }
-    </style>
-</head>
-<body>
-    <div id="root"></div>
-    <script src="/medi_bundle.js"></script>
-</body>
-</html>
--- a/medi/src/main.tsx	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-import React from 'react';
-import { createRoot } from 'react-dom/client';
-import { App } from './app';
-
-const root = createRoot(document.getElementById('root')!);
-root.render(<App />);
--- a/media_processor/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-load("@rules_cc//cc:cc_library.bzl", "cc_library")
-load("@rules_cc//cc:cc_test.bzl", "cc_test")
-
-cc_library(
-    name = "media_processor",
-    hdrs = ["media_processor.h"],
-    srcs = ["media_processor.c"],
-    deps = [
-        "//third_party/ffmpeg:ffmpeg_cli",
-        "//dowa:dowa",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-cc_test(
-    name = "media_processor_test",
-    srcs = ["media_processor_test.c"],
-    deps = [":media_processor"],
-)
--- a/media_processor/README.md	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-# media_processor
-
-Reusable media processing library for images and videos.
-
-## Features
-
-- **Image Processing**: Convert images to WebP format with quality/size options
-- **Video Processing**: Convert videos to HLS format for streaming
-- **Auto-detection**: Automatically detects media type from file extension
-- **Thumbnail Generation**: Creates thumbnails for videos
-- **Original Preservation**: Keeps original files alongside processed versions
-
-## Usage
-
-```c
-#include "media_processor/media_processor.h"
-
-// Process any media file (auto-detects type)
-MediaProcessorOpts opts = media_processor_opts_default("./output");
-opts.image_quality = 85;
-opts.video_max_width = 1920;
-
-char* id = media_generate_id();
-MediaResult* result = media_process_file("upload.mp4", id, &opts);
-
-if (result->status == MEDIA_STATUS_COMPLETED) {
-    printf("HLS playlist: %s\n", result->hls_playlist);
-    printf("Thumbnail: %s\n", result->thumbnail_path);
-}
-
-media_result_free(result);
-free(id);
-```
-
-## Output Structure
-
-For images:
-```
-output/
-└── {id}/
-    ├── original.jpg    # Original file
-    └── image.webp      # Converted WebP
-```
-
-For videos:
-```
-output/
-└── {id}/
-    ├── original.mp4    # Original file
-    ├── video.m3u8      # HLS playlist
-    ├── video_000.ts    # HLS segments
-    ├── video_001.ts
-    └── video_thumb.jpg # Thumbnail
-```
-
-## Options
-
-### Image Options
-
-| Option | Default | Description |
-|--------|---------|-------------|
-| `image_quality` | 80 | WebP quality (0-100) |
-| `image_max_width` | 0 | Max width (0 = no limit) |
-| `image_max_height` | 0 | Max height (0 = no limit) |
-
-### Video Options
-
-| Option | Default | Description |
-|--------|---------|-------------|
-| `video_segment_duration` | 6 | HLS segment length in seconds |
-| `video_bitrate` | 0 | Video bitrate in kbps (0 = auto) |
-| `video_max_width` | 0 | Max width (0 = no limit) |
-| `video_max_height` | 0 | Max height (0 = no limit) |
-| `generate_thumbnail` | true | Generate video thumbnail |
-
-## Building
-
-```bash
-bazel build //media_processor:media_processor
-```
-
-## Dependencies
-
-- `//third_party/ffmpeg:ffmpeg_cli` - FFmpeg CLI wrapper
-- `//dowa:dowa` - Memory and string utilities
--- a/media_processor/media_processor.c	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,316 +0,0 @@
-#include "media_processor.h"
-#include "third_party/ffmpeg/ffmpeg_cli.h"
-#include "dowa/dowa.h"
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <stdarg.h>
-#include <time.h>
-#include <unistd.h>
-#include <sys/stat.h>
-#include <errno.h>
-
-// File extension mappings
-static const struct {
-    const char* ext;
-    MediaType type;
-} EXTENSION_MAP[] = {
-    // Images
-    {".jpg", MEDIA_TYPE_IMAGE},
-    {".jpeg", MEDIA_TYPE_IMAGE},
-    {".png", MEDIA_TYPE_IMAGE},
-    {".gif", MEDIA_TYPE_IMAGE},
-    {".webp", MEDIA_TYPE_IMAGE},
-    {".bmp", MEDIA_TYPE_IMAGE},
-    {".tiff", MEDIA_TYPE_IMAGE},
-    {".tif", MEDIA_TYPE_IMAGE},
-    {".heic", MEDIA_TYPE_IMAGE},
-    {".heif", MEDIA_TYPE_IMAGE},
-    {".avif", MEDIA_TYPE_IMAGE},
-
-    // Videos
-    {".mp4", MEDIA_TYPE_VIDEO},
-    {".mov", MEDIA_TYPE_VIDEO},
-    {".avi", MEDIA_TYPE_VIDEO},
-    {".mkv", MEDIA_TYPE_VIDEO},
-    {".webm", MEDIA_TYPE_VIDEO},
-    {".flv", MEDIA_TYPE_VIDEO},
-    {".wmv", MEDIA_TYPE_VIDEO},
-    {".m4v", MEDIA_TYPE_VIDEO},
-    {".3gp", MEDIA_TYPE_VIDEO},
-
-    // Audio
-    {".mp3", MEDIA_TYPE_AUDIO},
-    {".wav", MEDIA_TYPE_AUDIO},
-    {".flac", MEDIA_TYPE_AUDIO},
-    {".aac", MEDIA_TYPE_AUDIO},
-    {".ogg", MEDIA_TYPE_AUDIO},
-    {".m4a", MEDIA_TYPE_AUDIO},
-    {".wma", MEDIA_TYPE_AUDIO},
-
-    {NULL, MEDIA_TYPE_UNKNOWN}
-};
-
-MediaProcessorOpts media_processor_opts_default(const char* output_dir) {
-    return (MediaProcessorOpts){
-        .output_dir = output_dir,
-        .image_quality = 80,
-        .image_max_width = 0,
-        .image_max_height = 0,
-        .video_segment_duration = 6,
-        .video_bitrate = 0,
-        .video_max_width = 0,
-        .video_max_height = 0,
-        .generate_thumbnail = true,
-    };
-}
-
-const char* media_get_extension(const char* filename) {
-    if (!filename) return NULL;
-    const char* dot = strrchr(filename, '.');
-    return dot ? dot : NULL;
-}
-
-MediaType media_detect_type(const char* filename) {
-    const char* ext = media_get_extension(filename);
-    if (!ext) return MEDIA_TYPE_UNKNOWN;
-
-    // Convert to lowercase for comparison
-    char ext_lower[16] = {0};
-    size_t len = strlen(ext);
-    if (len >= sizeof(ext_lower)) return MEDIA_TYPE_UNKNOWN;
-
-    for (size_t i = 0; i < len; i++) {
-        ext_lower[i] = (ext[i] >= 'A' && ext[i] <= 'Z') ? ext[i] + 32 : ext[i];
-    }
-
-    for (int i = 0; EXTENSION_MAP[i].ext != NULL; i++) {
-        if (strcmp(ext_lower, EXTENSION_MAP[i].ext) == 0) {
-            return EXTENSION_MAP[i].type;
-        }
-    }
-
-    return MEDIA_TYPE_UNKNOWN;
-}
-
-const char* media_type_to_mime(MediaType type) {
-    switch (type) {
-        case MEDIA_TYPE_IMAGE: return "image/*";
-        case MEDIA_TYPE_VIDEO: return "video/*";
-        case MEDIA_TYPE_AUDIO: return "audio/*";
-        default: return "application/octet-stream";
-    }
-}
-
-static MediaResult* create_result(void) {
-    MediaResult* result = calloc(1, sizeof(MediaResult));
-    result->status = MEDIA_STATUS_PENDING;
-    return result;
-}
-
-static void set_result_error(MediaResult* result, const char* fmt, ...) {
-    result->status = MEDIA_STATUS_FAILED;
-
-    va_list args;
-    va_start(args, fmt);
-
-    char buf[512];
-    vsnprintf(buf, sizeof(buf), fmt, args);
-    result->error_message = strdup(buf);
-
-    va_end(args);
-}
-
-void media_result_free(MediaResult* result) {
-    if (!result) return;
-    free(result->error_message);
-    free(result->original_path);
-    free(result->webp_path);
-    free(result->hls_playlist);
-    free(result->thumbnail_path);
-    free(result);
-}
-
-char* media_generate_id(void) {
-    static int counter = 0;
-    char buf[64];
-    snprintf(buf, sizeof(buf), "%ld_%d_%d",
-        (long)time(NULL), getpid(), counter++);
-    return strdup(buf);
-}
-
-bool media_path_is_safe(const char* path) {
-    if (!path) return false;
-    // Check for directory traversal
-    if (strstr(path, "..") != NULL) return false;
-    // Check for absolute paths (in uploaded filenames)
-    if (path[0] == '/') return false;
-    return true;
-}
-
-static bool ensure_directory(const char* path) {
-    struct stat st;
-    if (stat(path, &st) == 0) {
-        return S_ISDIR(st.st_mode);
-    }
-    return mkdir(path, 0755) == 0;
-}
-
-bool media_copy_file(const char* src, const char* dst) {
-    FILE* in = fopen(src, "rb");
-    if (!in) return false;
-
-    FILE* out = fopen(dst, "wb");
-    if (!out) {
-        fclose(in);
-        return false;
-    }
-
-    char buf[8192];
-    size_t n;
-    while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
-        if (fwrite(buf, 1, n, out) != n) {
-            fclose(in);
-            fclose(out);
-            return false;
-        }
-    }
-
-    fclose(in);
-    fclose(out);
-    return true;
-}
-
-MediaResult* media_process_image(const char* input_path, const char* id, MediaProcessorOpts* opts) {
-    MediaResult* result = create_result();
-    result->status = MEDIA_STATUS_PROCESSING;
-
-    if (!opts || !opts->output_dir) {
-        set_result_error(result, "Output directory not specified");
-        return result;
-    }
-
-    if (!ensure_directory(opts->output_dir)) {
-        set_result_error(result, "Failed to create output directory: %s", opts->output_dir);
-        return result;
-    }
-
-    // Create subdirectory for this media
-    char media_dir[1024];
-    snprintf(media_dir, sizeof(media_dir), "%s/%s", opts->output_dir, id);
-    if (!ensure_directory(media_dir)) {
-        set_result_error(result, "Failed to create media directory: %s", media_dir);
-        return result;
-    }
-
-    // Copy original
-    const char* ext = media_get_extension(input_path);
-    char original_path[1024];
-    snprintf(original_path, sizeof(original_path), "%s/original%s", media_dir, ext ? ext : "");
-
-    if (!media_copy_file(input_path, original_path)) {
-        set_result_error(result, "Failed to copy original file");
-        return result;
-    }
-    result->original_path = strdup(original_path);
-
-    // Convert to WebP
-    char webp_path[1024];
-    snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
-
-    ImageConvertOpts img_opts = ffmpeg_image_opts_default();
-    img_opts.quality = opts->image_quality;
-    img_opts.max_width = opts->image_max_width;
-    img_opts.max_height = opts->image_max_height;
-
-    FfmpegResult ffmpeg_result = ffmpeg_image_to_webp(input_path, webp_path, &img_opts);
-    if (ffmpeg_result != FFMPEG_OK) {
-        set_result_error(result, "FFmpeg conversion failed: %s", ffmpeg_last_error());
-        return result;
-    }
-
-    result->webp_path = strdup(webp_path);
-    result->status = MEDIA_STATUS_COMPLETED;
-    return result;
-}
-
-MediaResult* media_process_video(const char* input_path, const char* id, MediaProcessorOpts* opts) {
-    MediaResult* result = create_result();
-    result->status = MEDIA_STATUS_PROCESSING;
-
-    if (!opts || !opts->output_dir) {
-        set_result_error(result, "Output directory not specified");
-        return result;
-    }
-
-    if (!ensure_directory(opts->output_dir)) {
-        set_result_error(result, "Failed to create output directory: %s", opts->output_dir);
-        return result;
-    }
-
-    // Create subdirectory for this media
-    char media_dir[1024];
-    snprintf(media_dir, sizeof(media_dir), "%s/%s", opts->output_dir, id);
-    if (!ensure_directory(media_dir)) {
-        set_result_error(result, "Failed to create media directory: %s", media_dir);
-        return result;
-    }
-
-    // Copy original
-    const char* ext = media_get_extension(input_path);
-    char original_path[1024];
-    snprintf(original_path, sizeof(original_path), "%s/original%s", media_dir, ext ? ext : "");
-
-    if (!media_copy_file(input_path, original_path)) {
-        set_result_error(result, "Failed to copy original file");
-        return result;
-    }
-    result->original_path = strdup(original_path);
-
-    // Get duration
-    ffmpeg_get_duration(input_path, &result->duration);
-
-    // Convert to HLS
-    HlsConvertOpts hls_opts = ffmpeg_hls_opts_default();
-    hls_opts.segment_duration = opts->video_segment_duration;
-    hls_opts.video_bitrate = opts->video_bitrate;
-    hls_opts.max_width = opts->video_max_width;
-    hls_opts.max_height = opts->video_max_height;
-    hls_opts.generate_thumbnail = opts->generate_thumbnail;
-
-    FfmpegResult ffmpeg_result = ffmpeg_video_to_hls(input_path, media_dir, "video", &hls_opts);
-    if (ffmpeg_result != FFMPEG_OK) {
-        set_result_error(result, "FFmpeg HLS conversion failed: %s", ffmpeg_last_error());
-        return result;
-    }
-
-    char playlist_path[1024];
-    snprintf(playlist_path, sizeof(playlist_path), "%s/video.m3u8", media_dir);
-    result->hls_playlist = strdup(playlist_path);
-
-    if (opts->generate_thumbnail) {
-        char thumb_path[1024];
-        snprintf(thumb_path, sizeof(thumb_path), "%s/video_thumb.jpg", media_dir);
-        result->thumbnail_path = strdup(thumb_path);
-    }
-
-    result->status = MEDIA_STATUS_COMPLETED;
-    return result;
-}
-
-MediaResult* media_process_file(const char* input_path, const char* id, MediaProcessorOpts* opts) {
-    MediaType type = media_detect_type(input_path);
-
-    switch (type) {
-        case MEDIA_TYPE_IMAGE:
-            return media_process_image(input_path, id, opts);
-        case MEDIA_TYPE_VIDEO:
-            return media_process_video(input_path, id, opts);
-        default: {
-            MediaResult* result = create_result();
-            set_result_error(result, "Unsupported media type");
-            return result;
-        }
-    }
-}
--- a/media_processor/media_processor.h	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-#ifndef MEDIA_PROCESSOR_H
-#define MEDIA_PROCESSOR_H
-
-#include <stdbool.h>
-#include <stddef.h>
-
-// Media types
-typedef enum {
-    MEDIA_TYPE_UNKNOWN = 0,
-    MEDIA_TYPE_IMAGE,
-    MEDIA_TYPE_VIDEO,
-    MEDIA_TYPE_AUDIO,
-} MediaType;
-
-// Processing status
-typedef enum {
-    MEDIA_STATUS_PENDING = 0,
-    MEDIA_STATUS_PROCESSING,
-    MEDIA_STATUS_COMPLETED,
-    MEDIA_STATUS_FAILED,
-} MediaStatus;
-
-// Result of a processing operation
-typedef struct {
-    MediaStatus status;
-    char* error_message;
-
-    // For images
-    char* original_path;    // Path to original file
-    char* webp_path;        // Path to WebP version
-
-    // For videos
-    char* hls_playlist;     // Path to .m3u8 file
-    char* thumbnail_path;   // Path to thumbnail
-    double duration;        // Duration in seconds
-} MediaResult;
-
-// Processing options
-typedef struct {
-    // Output directory (required)
-    const char* output_dir;
-
-    // Image options
-    int image_quality;      // 0-100, default 80
-    int image_max_width;    // 0 = no limit
-    int image_max_height;   // 0 = no limit
-
-    // Video options
-    int video_segment_duration; // HLS segment duration, default 6
-    int video_bitrate;          // kbps, 0 = auto
-    int video_max_width;        // 0 = no limit
-    int video_max_height;       // 0 = no limit
-    bool generate_thumbnail;    // default true
-} MediaProcessorOpts;
-
-// Initialize processor options with defaults
-MediaProcessorOpts media_processor_opts_default(const char* output_dir);
-
-// Detect media type from file extension
-MediaType media_detect_type(const char* filename);
-
-// Get MIME type string
-const char* media_type_to_mime(MediaType type);
-
-// Process a single file (auto-detects type)
-// Returns a MediaResult that must be freed with media_result_free()
-MediaResult* media_process_file(const char* input_path, const char* id, MediaProcessorOpts* opts);
-
-// Process image specifically
-MediaResult* media_process_image(const char* input_path, const char* id, MediaProcessorOpts* opts);
-
-// Process video specifically
-MediaResult* media_process_video(const char* input_path, const char* id, MediaProcessorOpts* opts);
-
-// Free a MediaResult
-void media_result_free(MediaResult* result);
-
-// Generate a unique ID for a media file
-char* media_generate_id(void);
-
-// Utility: Copy file to destination
-bool media_copy_file(const char* src, const char* dst);
-
-// Utility: Get file extension (returns pointer into filename, do not free)
-const char* media_get_extension(const char* filename);
-
-// Utility: Check if path is safe (no directory traversal)
-bool media_path_is_safe(const char* path);
-
-#endif // MEDIA_PROCESSOR_H
--- a/media_processor/media_processor_test.c	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-#include "media_processor.h"
-#include <stdio.h>
-#include <assert.h>
-#include <string.h>
-
-void test_detect_type() {
-    printf("Testing media_detect_type...\n");
-
-    // Images
-    assert(media_detect_type("photo.jpg") == MEDIA_TYPE_IMAGE);
-    assert(media_detect_type("photo.JPEG") == MEDIA_TYPE_IMAGE);
-    assert(media_detect_type("image.png") == MEDIA_TYPE_IMAGE);
-    assert(media_detect_type("animation.gif") == MEDIA_TYPE_IMAGE);
-    assert(media_detect_type("modern.webp") == MEDIA_TYPE_IMAGE);
-    assert(media_detect_type("apple.heic") == MEDIA_TYPE_IMAGE);
-
-    // Videos
-    assert(media_detect_type("video.mp4") == MEDIA_TYPE_VIDEO);
-    assert(media_detect_type("movie.mov") == MEDIA_TYPE_VIDEO);
-    assert(media_detect_type("clip.avi") == MEDIA_TYPE_VIDEO);
-    assert(media_detect_type("stream.mkv") == MEDIA_TYPE_VIDEO);
-    assert(media_detect_type("web.webm") == MEDIA_TYPE_VIDEO);
-
-    // Audio
-    assert(media_detect_type("song.mp3") == MEDIA_TYPE_AUDIO);
-    assert(media_detect_type("audio.wav") == MEDIA_TYPE_AUDIO);
-    assert(media_detect_type("music.flac") == MEDIA_TYPE_AUDIO);
-
-    // Unknown
-    assert(media_detect_type("document.pdf") == MEDIA_TYPE_UNKNOWN);
-    assert(media_detect_type("archive.zip") == MEDIA_TYPE_UNKNOWN);
-    assert(media_detect_type("noextension") == MEDIA_TYPE_UNKNOWN);
-
-    printf("  PASSED\n");
-}
-
-void test_get_extension() {
-    printf("Testing media_get_extension...\n");
-
-    assert(strcmp(media_get_extension("file.jpg"), ".jpg") == 0);
-    assert(strcmp(media_get_extension("path/to/file.mp4"), ".mp4") == 0);
-    assert(strcmp(media_get_extension("multiple.dots.png"), ".png") == 0);
-    assert(media_get_extension("noextension") == NULL);
-    assert(media_get_extension(NULL) == NULL);
-
-    printf("  PASSED\n");
-}
-
-void test_path_safety() {
-    printf("Testing media_path_is_safe...\n");
-
-    // Safe paths
-    assert(media_path_is_safe("file.jpg") == true);
-    assert(media_path_is_safe("folder/file.jpg") == true);
-    assert(media_path_is_safe("a/b/c/file.jpg") == true);
-
-    // Unsafe paths
-    assert(media_path_is_safe("../file.jpg") == false);
-    assert(media_path_is_safe("folder/../file.jpg") == false);
-    assert(media_path_is_safe("/absolute/path.jpg") == false);
-    assert(media_path_is_safe(NULL) == false);
-
-    printf("  PASSED\n");
-}
-
-void test_generate_id() {
-    printf("Testing media_generate_id...\n");
-
-    char* id1 = media_generate_id();
-    char* id2 = media_generate_id();
-
-    assert(id1 != NULL);
-    assert(id2 != NULL);
-    assert(strlen(id1) > 0);
-    assert(strlen(id2) > 0);
-    assert(strcmp(id1, id2) != 0);  // Should be unique
-
-    free(id1);
-    free(id2);
-
-    printf("  PASSED\n");
-}
-
-void test_default_opts() {
-    printf("Testing media_processor_opts_default...\n");
-
-    MediaProcessorOpts opts = media_processor_opts_default("./output");
-
-    assert(opts.output_dir != NULL);
-    assert(strcmp(opts.output_dir, "./output") == 0);
-    assert(opts.image_quality == 80);
-    assert(opts.video_segment_duration == 6);
-    assert(opts.generate_thumbnail == true);
-
-    printf("  PASSED\n");
-}
-
-int main() {
-    printf("\n=== Media Processor Tests ===\n\n");
-
-    test_detect_type();
-    test_get_extension();
-    test_path_safety();
-    test_generate_id();
-    test_default_opts();
-
-    printf("\n=== All tests passed! ===\n\n");
-    return 0;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/.config.development	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,12 @@
+# MrJuneJune Server Configuration
+
+# Auth token for S3 upload API
+# Client must send this in Authorization header: Bearer <token>
+UPLOAD_AUTH_TOKEN=THIS
+
+# S3 Configuration
+S3_REGION=THIS
+S3_BUCKET=THIS
+
+# Presigned URL expiration (seconds)
+S3_URL_EXPIRES=SECONDS
--- a/mrjunejune/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -28,15 +28,37 @@
   dest = "src/public/highlight",
 )
 
+move_files_into_dir(
+  name = "rich_editor_js",
+  srcs = [
+    "//rich_editor:rich_editor",
+  ],
+  dest = "src/public/js",
+)
+
+move_files_into_dir(
+  name = "icons",
+  srcs = [
+    "//assets:icons",
+  ],
+  dest = "src/public/icons",
+)
+
 filegroup(
   name = "public_files",
-  srcs = glob(["src/public/**"]),
+  srcs = glob(["src/public/*"]),
+  visibility = ["//visibility:public"],
+)
+
+filegroup(
+  name = "public_fonts_files",
+  srcs = glob(["src/public/fonts/*"]),
   visibility = ["//visibility:public"],
 )
     
 filegroup(
   name = "src_files",
-  srcs = glob(["src/**"]) + [":react_pages", ":shared_js_non_public", "shared_js_file"],
+  srcs = glob(["src/**"]) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js", ":icons"],
   visibility = ["//mrjunejune/test:__pkg__"],
 )
 
@@ -45,10 +67,18 @@
   name = "mrjunejune_server",
   srcs = ["main.c"],
   deps = [
-    "//seobeo:seobeo_tcp_server_ws",
+    "//seobeo:seobeo",
     "//markdown_converter:markdown_to_html_c",
+    "//s3:s3",
+    "//deita:deita",
   ],
-  data = [":src_files"],
+  copts = ["-D_GNU_SOURCE"],
+  linkopts = ["-lpthread"],
+  data = [
+    ":src_files",
+    ":config_file",
+    "//:env_file",
+  ],
   visibility = ["//mrjunejune/test:__pkg__"],
 )
 
@@ -57,10 +87,30 @@
   name = "mrjunejune_server_debug",
   srcs = ["main.c"],
   deps = [
-    "//seobeo:seobeo_tcp_server_ws_debug", 
-    "//markdown_converter:markdown_to_html_c"
+    "//seobeo:seobeo_debug",
+    "//markdown_converter:markdown_to_html_c",
+    "//s3:s3",
+    "//deita:deita",
+  ],
+  copts = ["-D_GNU_SOURCE"],
+  linkopts = ["-lpthread"],
+  data = [
+    ":src_files",
+    ":config_file",
+    "//:env_file",
   ],
-  data = [":src_files"],
+)
+
+filegroup(
+  name = "config_file",
+  srcs = [".config"],
+  visibility = ["//visibility:public"],
+)
+
+filegroup(
+  name = "data_dir",
+  srcs = glob(["data/*"]),
+  visibility = ["//visibility:public"],
 )
 
 # Run this to create html files
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/PWA_SETUP.md	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,126 @@
+# PWA Setup Guide
+
+Your site is now configured as a Progressive Web App! 🎉
+
+## What's Been Added
+
+1. **manifest.json** - App configuration
+2. **sw.js** - Service worker for caching and offline support
+3. **pwa-register.js** - Service worker registration
+4. **offline.html** - Offline fallback page
+5. **Updated base_head.html** - Links to manifest and PWA script
+
+## Missing: App Icons
+
+You need to create PNG icons from your SVG. Run these commands:
+
+### Using ImageMagick (recommended):
+
+```bash
+cd src/public
+
+# 192x192 icon
+convert epi_all_colors.svg -resize 192x192 -background none icon-192.png
+
+# 512x512 icon
+convert epi_all_colors.svg -resize 512x512 -background none icon-512.png
+
+# Maskable icon (512x512 with safe zone)
+convert epi_all_colors.svg -resize 409x409 -background none -gravity center -extent 512x512 icon-maskable-512.png
+```
+
+### Or using Inkscape:
+
+```bash
+inkscape epi_all_colors.svg --export-type=png --export-filename=icon-192.png -w 192 -h 192
+inkscape epi_all_colors.svg --export-type=png --export-filename=icon-512.png -w 512 -h 512
+inkscape epi_all_colors.svg --export-type=png --export-filename=icon-maskable-512.png -w 512 -h 512
+```
+
+### Or use an online tool:
+- https://realfavicongenerator.net/
+- https://www.pwabuilder.com/
+
+## Optional: Screenshots (for better app install experience)
+
+Create screenshots of your site:
+
+```bash
+# Mobile screenshot (540x720)
+screenshot-mobile.png
+
+# Desktop screenshot (1280x720)
+screenshot-desktop.png
+```
+
+You can use browser DevTools to capture these, or remove the `screenshots` section from manifest.json if you don't want them.
+
+## Testing Your PWA
+
+1. **Serve over HTTPS** - PWAs require HTTPS (localhost works for testing)
+2. **Open Chrome DevTools** → Application tab → Manifest
+3. **Check Service Worker** → Application tab → Service Workers
+4. **Lighthouse Audit** → Run PWA audit to see score
+
+## Install Prompt
+
+When users visit your site, they'll see an "Install App" button in the bottom-right corner for 10 seconds. They can:
+- Click it to install immediately
+- Use browser menu: "Install MrJuneJune" or "Add to Home Screen"
+
+## Features
+
+✅ **Offline Support** - Caches pages, CSS, JS, fonts, images
+✅ **App Shortcuts** - Quick access to Blog and Notes
+✅ **Install Prompt** - Automatic install button
+✅ **Auto-updates** - Service worker updates on reload
+✅ **Fast Loading** - Cached resources load instantly
+
+## Customization
+
+Edit `manifest.json` to change:
+- `theme_color` - App theme color
+- `background_color` - Splash screen color
+- `display` - `standalone`, `fullscreen`, `minimal-ui`, or `browser`
+- `shortcuts` - App shortcut menu items
+
+Edit `sw.js` to change:
+- `CACHE_VERSION` - Increment to force cache refresh
+- `STATIC_CACHE` - Files to cache immediately
+- Caching strategy (currently: cache-first with network fallback)
+
+## Testing on Mobile
+
+### Android:
+1. Open Chrome
+2. Visit your site
+3. Tap menu → "Install app" or "Add to Home Screen"
+
+### iOS (Safari):
+1. Open Safari
+2. Tap Share button
+3. Tap "Add to Home Screen"
+
+Note: iOS has limited PWA support (no install prompt, limited background features)
+
+## Debugging
+
+Check console for:
+- `[PWA]` - Registration events
+- `[SW]` - Service worker caching events
+
+Clear cache:
+```js
+navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' });
+```
+
+## Next Steps
+
+1. Generate the PNG icons (see commands above)
+2. Test on HTTPS
+3. Run Lighthouse audit
+4. Deploy and test on mobile device
+5. Consider adding:
+   - Push notifications
+   - Background sync
+   - Share target API
--- a/mrjunejune/deploy.sh	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/deploy.sh	Sun Aug 02 16:50:48 2026 -0700
@@ -5,6 +5,9 @@
 sudo mkdir -p /opt/mrjunejune_server
 sudo cp -a bazel-bin/mrjunejune/mrjunejune_server_bundle /opt/mrjunejune_server_bundle_new
 sudo chown -R mrjunejune_server:zenbu_team /opt/mrjunejune_server_bundle_new
+# sqlite3 db
+sudo mkdir -p /opt/data/mrjunejune/
+sudo chown -R mrjunejune_server:zenbu_team /opt/data/mrjunejune/
 
 # Swap
 sudo rm -rf /opt/mrjunejune_server_bundle_active
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/generate-icons.sh	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Generate PWA icons from SVG
+# Requires ImageMagick: apt-get install imagemagick
+
+cd "$(dirname "$0")/src/public"
+
+if ! command -v convert &> /dev/null; then
+    echo "❌ ImageMagick not found. Please install it:"
+    echo "   Ubuntu/Debian: sudo apt-get install imagemagick"
+    echo "   macOS: brew install imagemagick"
+    echo ""
+    echo "Or use an online tool: https://realfavicongenerator.net/"
+    exit 1
+fi
+
+echo "🎨 Generating PWA icons from epi_all_colors.svg..."
+
+# 192x192 icon
+echo "  → Creating icon-192.png..."
+convert epi_all_colors.svg -resize 192x192 -background none icon-192.png
+
+# 512x512 icon
+echo "  → Creating icon-512.png..."
+convert epi_all_colors.svg -resize 512x512 -background none icon-512.png
+
+# Maskable icon (512x512 with safe zone - 80% of size)
+echo "  → Creating icon-maskable-512.png..."
+convert epi_all_colors.svg -resize 409x409 -background none -gravity center -extent 512x512 icon-maskable-512.png
+
+echo ""
+echo "✅ Icons generated successfully!"
+echo ""
+echo "Generated files:"
+echo "  - icon-192.png"
+echo "  - icon-512.png"
+echo "  - icon-maskable-512.png"
+echo ""
+echo "Next steps:"
+echo "  1. Verify icons look good: ls -lh src/public/icon-*.png"
+echo "  2. Test PWA: Open site in Chrome → DevTools → Application → Manifest"
+echo "  3. Run Lighthouse audit to check PWA score"
--- a/mrjunejune/main.c	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/main.c	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,11 @@
 #include "seobeo/seobeo.h"
 #include "markdown_converter/markdown_to_html.h"
+#include "s3/s3_uploader.h"
+#include "deita/deita.h"
 #include <time.h>
+#include <sys/stat.h>
+#include <stdarg.h>
+#include <pthread.h>
 
 // UUID + /tmp/ + format (max 4)
 #define TMP_FILE_LENGTH 47
@@ -9,6 +14,171 @@
 volatile sig_atomic_t stop_server = 0;
 static _Atomic uint32_t counter = 0;
 
+// Media Processing Context for background threads
+typedef struct {
+  int64    media_id;
+  char     s3_key_original[512];
+  char     s3_key_processed[512];
+  char     content_type[128];
+  char     access_token[256];
+  char     db_path[256];
+  S3_Config s3_config;
+} Media_Processing_Context;
+
+// Server configuration (loaded from .config)
+static char g_upload_auth_token[256] = {0};
+static char g_s3_region[64] = "us-west-2";
+static char g_s3_bucket[128] = "mrjunejune";
+static char g_s3_cloudfront_url[256] = {0};
+static char g_db_path[256] = "mrjunejune/data/mrjunejune.db";
+static int  g_s3_url_expires = 3600;
+static S3_Config g_s3_config = {0};
+static Deita_Connection *g_db_connection = NULL;
+
+static void load_config(const char *config_path)
+{
+  FILE *f = fopen(config_path, "r");
+  if (!f)
+  {
+    printf("[CONFIG] Warning: Could not open %s, using defaults\n", config_path);
+    return;
+  }
+
+  char line[512];
+  while (fgets(line, sizeof(line), f))
+  {
+    // Skip comments and empty lines
+    if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
+
+    char *eq = strchr(line, '=');
+    if (!eq) continue;
+
+    *eq = '\0';
+    char *key = line;
+    char *value = eq + 1;
+
+    // Trim newline from value
+    size_t vlen = strlen(value);
+    while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r'))
+      value[--vlen] = '\0';
+
+    if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0)
+    {
+      strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1);
+    }
+    else if (strcmp(key, "S3_REGION") == 0)
+    {
+      strncpy(g_s3_region, value, sizeof(g_s3_region) - 1);
+    }
+    else if (strcmp(key, "S3_BUCKET") == 0)
+    {
+      strncpy(g_s3_bucket, value, sizeof(g_s3_bucket) - 1);
+    }
+    else if (strcmp(key, "S3_URL_EXPIRES") == 0)
+    {
+      g_s3_url_expires = atoi(value);
+    }
+    else if (strcmp(key, "S3_CLOUDFRONT_URL") == 0)
+    {
+      strncpy(g_s3_cloudfront_url, value, sizeof(g_s3_cloudfront_url) - 1);
+    }
+    else if (strcmp(key, "DB_PATH") == 0)
+    {
+      strncpy(g_db_path, value, sizeof(g_db_path) - 1);
+    }
+  }
+  fclose(f);
+
+  printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d, cloudfront=%s, db=%s\n",
+         g_upload_auth_token[0] ? "***" : "(empty)",
+         g_s3_region, g_s3_bucket, g_s3_url_expires,
+         g_s3_cloudfront_url[0] ? g_s3_cloudfront_url : "(none)",
+         g_db_path);
+}
+
+static void init_database(void)
+{
+  // Create data directory if needed
+  char *last_slash = strrchr(g_db_path, '/');
+  if (last_slash)
+  {
+    char dir_path[256];
+    size_t dir_len = last_slash - g_db_path;
+    strncpy(dir_path, g_db_path, dir_len);
+    dir_path[dir_len] = '\0';
+    mkdir(dir_path, 0755);
+  }
+
+  g_db_connection = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, g_db_path);
+  if (!g_db_connection || !Deita_Connection_Is_Open(g_db_connection))
+  {
+    printf("[DB] ERROR: Failed to open database at %s\n", g_db_path);
+    return;
+  }
+
+  // Create editor_content table
+  const char *create_table =
+    "CREATE TABLE IF NOT EXISTS editor_content ("
+    "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
+    "  access_token TEXT NOT NULL,"
+    "  doc_id TEXT NOT NULL,"
+    "  content TEXT,"
+    "  created_at INTEGER DEFAULT (strftime('%s', 'now')),"
+    "  updated_at INTEGER DEFAULT (strftime('%s', 'now')),"
+    "  UNIQUE(access_token, doc_id)"
+    ")";
+
+  int32 result = Deita_Query_Execute_Update(g_db_connection, create_table);
+  if (result < 0)
+  {
+    printf("[DB] ERROR: Failed to create editor_content table\n");
+  }
+
+  // Create media_uploads table
+  const char *create_media_uploads =
+    "CREATE TABLE IF NOT EXISTS media_uploads ("
+    "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
+    "  access_token TEXT NOT NULL,"
+    "  original_filename TEXT NOT NULL,"
+    "  content_type TEXT NOT NULL,"
+    "  s3_key_original TEXT NOT NULL,"
+    "  s3_key_processed TEXT,"
+    "  file_size INTEGER,"
+    "  status TEXT NOT NULL DEFAULT 'pending',"
+    "  error_message TEXT,"
+    "  created_at INTEGER DEFAULT (strftime('%s', 'now')),"
+    "  updated_at INTEGER DEFAULT (strftime('%s', 'now'))"
+    ")";
+
+  result = Deita_Query_Execute_Update(g_db_connection, create_media_uploads);
+  if (result < 0)
+  {
+    printf("[DB] ERROR: Failed to create media_uploads table\n");
+  }
+
+  // Create indices for media_uploads
+  const char *create_status_idx =
+    "CREATE INDEX IF NOT EXISTS idx_media_uploads_status ON media_uploads(status)";
+  result = Deita_Query_Execute_Update(g_db_connection, create_status_idx);
+  if (result < 0)
+  {
+    printf("[DB] ERROR: Failed to create status index\n");
+  }
+
+  const char *create_token_status_idx =
+    "CREATE INDEX IF NOT EXISTS idx_media_uploads_token_status "
+    "ON media_uploads(access_token, status)";
+  result = Deita_Query_Execute_Update(g_db_connection, create_token_status_idx);
+  if (result < 0)
+  {
+    printf("[DB] ERROR: Failed to create token_status index\n");
+  }
+  else
+  {
+    printf("[DB] Initialized: %s\n", g_db_path);
+  }
+}
+
 void handle_sigint(int sig)
 {
   printf("Failed\n");
@@ -34,6 +204,8 @@
     char *end_tag = strstr(start_tag, "}}");
     if (!end_tag) break;
 
+    Seobeo_Log(SEOBEO_INFO, "[Curr] Life\n");
+
     size_t leading_len = start_tag - cursor;
     memcpy(final_body + current_offset, cursor, leading_len);
     current_offset += leading_len;
@@ -45,7 +217,8 @@
 
     size_t sub_file_size = 0;
     char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size);
-    Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content);
+    Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading include: '%s' -> %s (size=%zu)\n",
+              include_name, sub_content ? "OK" : "FAILED", sub_file_size);
     if (sub_content)
     {
       memcpy(final_body + current_offset, sub_content, sub_file_size);
@@ -58,15 +231,15 @@
   strcpy(final_body + current_offset, cursor);
 }
 
-
 void Seobeo_Render_Html_FilePath(
     char *final_body,
     char *path,
     Dowa_Arena *arena
 ) {
-  Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path);
+  Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading main template: '%s'\n", path);
   size_t html_size = 0;
   char *template = Seobeo_Web_LoadFile(path, &html_size);
+  Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Main template loaded: %s (size=%zu)\n", template ? "OK" : "FAILED", html_size);
   if (!template) return;
   Seobeo_Render_Html(final_body, template, arena);
 }
@@ -123,7 +296,7 @@
 
   if (!req)
   {
-    printf("ERROR: Request is NULL\n");
+    Seobeo_Log(SEOBEO_ERROR, "Request is NULL\n");
     char *error_msg = "Internal error: no request data";
     Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
     Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
@@ -136,7 +309,7 @@
 
   for (size_t i = 0; i < req_length; i++)
   {
-    printf("  Key[%zu]: '%s'\n", i, req[i].key);
+    Seobeo_Log(SEOBEO_INFO, "  Key[%zu]: '%s'\n", i, req[i].key);
   }
 
   void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
@@ -496,15 +669,1143 @@
   return resp;
 }
 
+Seobeo_Request_Entry *GetNotesLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
+  Seobeo_Render_Html_FilePath(final_body, "/notes/login.html", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  return resp;
+}
+
+Seobeo_Request_Entry *GetNotes(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
+  Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  return resp;
+}
+
+Seobeo_Request_Entry *GetNoteById(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
+  // Same template - JavaScript handles the note_id from URL
+  Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  return resp;
+}
+
 CREATE_REDIRECT_HANDLER(HomePage, "/")
 CREATE_REDIRECT_HANDLER(Resume, "/resume")
 CREATE_REDIRECT_HANDLER(Tools, "/tools")
 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html")
 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter")
 CREATE_REDIRECT_HANDLER(Talk, "/talk")
+CREATE_REDIRECT_HANDLER(Editor, "/editor")
+
+// S3 Upload URL API
+// POST /api/s3/upload-url
+// Headers: Authorization: Bearer <token>, Content-Type: application/json
+// Body: {"filename": "photo.png", "content_type": "image/png"}
+// Returns: {"upload_url": "https://...", "key": "uploads/..."}
+Seobeo_Request_Entry *GetS3UploadUrl(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+
+  // Check auth token
+  void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization");
+  if (!auth_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena);
+    return resp;
+  }
+
+  const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value;
+
+  // Expect "Bearer <token>"
+  if (strncmp(auth_header, "Bearer ", 7) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format, use Bearer token\"}", arena);
+    return resp;
+  }
+
+  const char *token = auth_header + 7;
+  if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "403", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena);
+    return resp;
+  }
+
+  // Parse request body for filename and content_type
+  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
+  if (!body_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena);
+    return resp;
+  }
+
+  const char *body = ((Seobeo_Request_Entry*)body_kv)->value;
+
+  // Simple JSON parsing for filename and content_type
+  char filename[256] = {0};
+  char content_type[128] = "application/octet-stream";
+
+  // Find "filename":"value"
+  const char *fn_key = strstr(body, "\"filename\"");
+  if (fn_key)
+  {
+    const char *fn_start = strchr(fn_key + 10, '"');
+    if (fn_start)
+    {
+      fn_start++;
+      const char *fn_end = strchr(fn_start, '"');
+      if (fn_end && (size_t)(fn_end - fn_start) < sizeof(filename))
+      {
+        memcpy(filename, fn_start, fn_end - fn_start);
+        filename[fn_end - fn_start] = '\0';
+      }
+    }
+  }
+
+  // Find "content_type":"value"
+  const char *ct_key = strstr(body, "\"content_type\"");
+  if (ct_key)
+  {
+    const char *ct_start = strchr(ct_key + 14, '"');
+    if (ct_start)
+    {
+      ct_start++;
+      const char *ct_end = strchr(ct_start, '"');
+      if (ct_end && (size_t)(ct_end - ct_start) < sizeof(content_type))
+      {
+        memcpy(content_type, ct_start, ct_end - ct_start);
+        content_type[ct_end - ct_start] = '\0';
+      }
+    }
+  }
+
+  if (strlen(filename) == 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing filename in request body\"}", arena);
+    return resp;
+  }
+
+  // Generate unique S3 key with timestamp
+  char s3_key[512];
+  char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN);
+  uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  Dowa_String_UUID(seed, uuid);
+  snprintf(s3_key, sizeof(s3_key), "uploads/%s/%s", uuid, filename);
+
+  // Generate presigned URL
+  S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key, content_type, g_s3_url_expires);
+
+  if (!presigned.success)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    char *error_body = Dowa_Arena_Allocate(arena, 256);
+    snprintf(error_body, 256, "{\"error\":\"Failed to generate upload URL: %s\"}",
+             presigned.error_message ? presigned.error_message : "unknown");
+    Dowa_HashMap_Push_Arena(resp, "body", error_body, arena);
+    S3_Presigned_URL_Destroy(&presigned);
+    return resp;
+  }
+
+  // Build public URL using CloudFront
+  char public_url[512];
+  if (g_s3_cloudfront_url[0])
+  {
+    snprintf(public_url, sizeof(public_url), "%s/%s", g_s3_cloudfront_url, s3_key);
+  }
+  else
+  {
+    snprintf(public_url, sizeof(public_url), "https://%s.s3.%s.amazonaws.com/%s",
+             g_s3_bucket, g_s3_region, s3_key);
+  }
+
+  // Build response
+  char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url));
+  snprintf(response_body, 4096 + strlen(presigned.url),
+           "{\"upload_url\":\"%s\",\"public_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}",
+           presigned.url, public_url, s3_key, g_s3_url_expires);
+
+  S3_Presigned_URL_Destroy(&presigned);
+
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", response_body, arena);
+
+  printf("[S3] Generated upload URL for: %s\n", s3_key);
+
+  return resp;
+}
+
+// Editor Content Save API
+// POST /api/editor/save
+// Headers: Authorization: Bearer <token>
+// Body: {"doc_id": "my-doc", "content": "<html content>"}
+Seobeo_Request_Entry *EditorSave(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+
+  // Check auth token
+  void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization");
+  if (!auth_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena);
+    return resp;
+  }
+
+  const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value;
+  if (strncmp(auth_header, "Bearer ", 7) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena);
+    return resp;
+  }
+
+  const char *token = auth_header + 7;
+  if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "403", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena);
+    return resp;
+  }
+
+  if (!g_db_connection)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena);
+    return resp;
+  }
+
+  // Parse request body
+  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
+  if (!body_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena);
+    return resp;
+  }
+
+  const char *body = ((Seobeo_Request_Entry*)body_kv)->value;
+
+  // Parse doc_id and content from JSON
+  char doc_id[256] = "default";
+  char *content = NULL;
+  size_t content_len = 0;
+
+  // Find "doc_id":"value"
+  const char *doc_key = strstr(body, "\"doc_id\"");
+  if (doc_key)
+  {
+    const char *doc_start = strchr(doc_key + 8, '"');
+    if (doc_start)
+    {
+      doc_start++;
+      const char *doc_end = strchr(doc_start, '"');
+      if (doc_end && (size_t)(doc_end - doc_start) < sizeof(doc_id))
+      {
+        memcpy(doc_id, doc_start, doc_end - doc_start);
+        doc_id[doc_end - doc_start] = '\0';
+      }
+    }
+  }
+
+  // Find "content":"value" - content can be large and contain escaped characters
+  const char *content_key = strstr(body, "\"content\"");
+  if (content_key)
+  {
+    const char *content_start = strchr(content_key + 9, '"');
+    if (content_start)
+    {
+      content_start++;
+      // Find closing quote (accounting for escaped quotes)
+      const char *p = content_start;
+      while (*p)
+      {
+        if (*p == '\\' && *(p+1))
+        {
+          p += 2;
+          continue;
+        }
+        if (*p == '"') break;
+        p++;
+      }
+      content_len = p - content_start;
+      content = Dowa_Arena_Allocate(arena, content_len + 1);
+      memcpy(content, content_start, content_len);
+      content[content_len] = '\0';
+    }
+  }
+
+  if (!content)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing content\"}", arena);
+    return resp;
+  }
+
+  // Upsert content
+  const char *upsert_query =
+    "INSERT INTO editor_content (access_token, doc_id, content, updated_at) "
+    "VALUES (?, ?, ?, strftime('%s', 'now')) "
+    "ON CONFLICT(access_token, doc_id) DO UPDATE SET "
+    "content = excluded.content, updated_at = strftime('%s', 'now')";
+
+  const char *params[] = { token, doc_id, content };
+  int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, upsert_query, 3, params);
+
+  if (result < 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to save\"}", arena);
+    return resp;
+  }
+
+  printf("[EDITOR] Saved doc_id=%s, content_len=%zu\n", doc_id, content_len);
+
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", "{\"success\":true}", arena);
+  return resp;
+}
+
+// Editor Content Load API
+// GET /api/editor/load/:doc_id
+// Headers: Authorization: Bearer <token>
+Seobeo_Request_Entry *EditorLoad(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+
+  // Check auth token
+  void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization");
+  if (!auth_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena);
+    return resp;
+  }
+
+  const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value;
+  if (strncmp(auth_header, "Bearer ", 7) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena);
+    return resp;
+  }
+
+  const char *token = auth_header + 7;
+  if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "403", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena);
+    return resp;
+  }
+
+  if (!g_db_connection)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena);
+    return resp;
+  }
+
+  // Get doc_id from URL parameter
+  void *doc_id_kv = Dowa_HashMap_Get_Ptr(req, ":doc_id");
+  const char *doc_id = "default";
+  if (doc_id_kv)
+  {
+    doc_id = ((Seobeo_Request_Entry*)doc_id_kv)->value;
+  }
+
+  // Query content
+  const char *select_query =
+    "SELECT content, updated_at FROM editor_content WHERE access_token = ? AND doc_id = ?";
+  const char *params[] = { token, doc_id };
+
+  Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, params, arena);
+
+  if (p_result && Deita_Result_Set_Next(p_result))
+  {
+    const char *content = Deita_Result_Set_Get_Text(p_result, 0);
+    int64 updated_at = Deita_Result_Set_Get_Integer(p_result, 1);
+
+    // Build JSON response - escape content
+    size_t content_len = content ? strlen(content) : 0;
+    char *response_body = Dowa_Arena_Allocate(arena, content_len + 256);
+    snprintf(response_body, content_len + 256,
+             "{\"doc_id\":\"%s\",\"content\":\"%s\",\"updated_at\":%lld}",
+             doc_id, content ? content : "", (long long)updated_at);
+
+    Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", response_body, arena);
+
+    printf("[EDITOR] Loaded doc_id=%s\n", doc_id);
+  }
+  else
+  {
+    // No content found, return empty
+    char *response_body = Dowa_Arena_Allocate(arena, 128);
+    snprintf(response_body, 128, "{\"doc_id\":\"%s\",\"content\":\"\",\"updated_at\":0}", doc_id);
+
+    Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", response_body, arena);
+  }
+
+  if (p_result) Deita_Result_Set_Free(p_result);
+  return resp;
+}
+
+// Media Upload API - Create media record
+// POST /api/media/create
+// Headers: Authorization: Bearer <token>, Content-Type: application/json
+// Body: {"filename": "photo.jpg", "content_type": "image/jpeg"}
+// Returns: {"media_id": 123, "upload_url": "https://...", "expires": 3600}
+Seobeo_Request_Entry *MediaCreate(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+
+  // Check auth token
+  void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization");
+  if (!auth_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena);
+    return resp;
+  }
+
+  const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value;
+  if (strncmp(auth_header, "Bearer ", 7) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena);
+    return resp;
+  }
+
+  const char *token = auth_header + 7;
+  if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "403", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena);
+    return resp;
+  }
+
+  if (!g_db_connection)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena);
+    return resp;
+  }
+
+  // Parse request body
+  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
+  if (!body_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing request body\"}", arena);
+    return resp;
+  }
+
+  const char *body = ((Seobeo_Request_Entry*)body_kv)->value;
+
+  // Parse filename and content_type
+  char filename[256] = {0};
+  char content_type[128] = "application/octet-stream";
+
+  // Find "filename":"value"
+  const char *fn_key = strstr(body, "\"filename\"");
+  if (fn_key)
+  {
+    const char *fn_start = strchr(fn_key + 10, '"');
+    if (fn_start)
+    {
+      fn_start++;
+      const char *fn_end = strchr(fn_start, '"');
+      if (fn_end && (size_t)(fn_end - fn_start) < sizeof(filename))
+      {
+        memcpy(filename, fn_start, fn_end - fn_start);
+        filename[fn_end - fn_start] = '\0';
+      }
+    }
+  }
+
+  // Find "content_type":"value"
+  const char *ct_key = strstr(body, "\"content_type\"");
+  if (ct_key)
+  {
+    const char *ct_start = strchr(ct_key + 14, '"');
+    if (ct_start)
+    {
+      ct_start++;
+      const char *ct_end = strchr(ct_start, '"');
+      if (ct_end && (size_t)(ct_end - ct_start) < sizeof(content_type))
+      {
+        memcpy(content_type, ct_start, ct_end - ct_start);
+        content_type[ct_end - ct_start] = '\0';
+      }
+    }
+  }
+
+  if (strlen(filename) == 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing filename\"}", arena);
+    return resp;
+  }
+
+  // Generate UUID for this upload
+  char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN);
+  uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  Dowa_String_UUID(seed, uuid);
+
+  // Generate S3 keys
+  char s3_key_original[512];
+  char s3_key_processed[512];
+  snprintf(s3_key_original, sizeof(s3_key_original), "uploads/%s/%s", uuid, filename);
+
+  // Only use .webp for images
+  int is_image = (strncmp(content_type, "image/", 6) == 0);
+  if (is_image)
+  {
+    snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid);
+  }
+  else
+  {
+    s3_key_processed[0] = '\0'; // No processed version for non-images
+  }
+
+  // Insert into database
+  const char *insert_query =
+    "INSERT INTO media_uploads (access_token, original_filename, content_type, s3_key_original, s3_key_processed, status) "
+    "VALUES (?, ?, ?, ?, ?, 'pending')";
+
+  const char *params[] = { token, filename, content_type, s3_key_original, s3_key_processed };
+  int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, insert_query, 5, params);
+
+  if (result < 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to create media record\"}", arena);
+    return resp;
+  }
+
+  // Get the inserted media_id using last_insert_rowid()
+  const char *last_id_query = "SELECT last_insert_rowid()";
+  Deita_Result_Set *id_result = Deita_Query_Execute(g_db_connection, last_id_query, arena);
+  int64 media_id = 0;
+  if (id_result && Deita_Result_Set_Next(id_result))
+  {
+    media_id = Deita_Result_Set_Get_Integer(id_result, 0);
+  }
+  if (id_result) Deita_Result_Set_Free(id_result);
+
+  // Generate presigned PUT URL
+  S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key_original, content_type, g_s3_url_expires);
+
+  if (!presigned.success)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    char *error_body = Dowa_Arena_Allocate(arena, 256);
+    snprintf(error_body, 256, "{\"error\":\"Failed to generate upload URL: %s\"}",
+             presigned.error_message ? presigned.error_message : "unknown");
+    Dowa_HashMap_Push_Arena(resp, "body", error_body, arena);
+    S3_Presigned_URL_Destroy(&presigned);
+    return resp;
+  }
+
+  // Build public URL using CloudFront or S3
+  char public_url[512];
+  if (g_s3_cloudfront_url[0])
+  {
+    snprintf(public_url, sizeof(public_url), "%s/%s", g_s3_cloudfront_url, s3_key_original);
+  }
+  else
+  {
+    snprintf(public_url, sizeof(public_url), "https://%s.s3.%s.amazonaws.com/%s",
+             g_s3_bucket, g_s3_region, s3_key_original);
+  }
+
+  // Build response
+  char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url) + strlen(public_url));
+  snprintf(response_body, 4096 + strlen(presigned.url) + strlen(public_url),
+           "{\"media_id\":%lld,\"upload_url\":\"%s\",\"public_url\":\"%s\",\"expires\":%d}",
+           (long long)media_id, presigned.url, public_url, g_s3_url_expires);
+
+  S3_Presigned_URL_Destroy(&presigned);
+
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", response_body, arena);
+
+  printf("[MEDIA] Created media_id=%lld, file=%s\n", (long long)media_id, filename);
+
+  return resp;
+}
+
+// Background thread function for media processing
+void *Media_Process_Background(void *arg)
+{
+  Media_Processing_Context *ctx = (Media_Processing_Context *)arg;
+
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Background thread started for media_id=%lld\n", (long long)ctx->media_id);
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] S3 key original: %s\n", ctx->s3_key_original);
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] S3 key processed: %s\n", ctx->s3_key_processed);
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] DB path: %s\n", ctx->db_path);
+
+  // Open thread-local DB connection
+  Deita_Connection *db_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, ctx->db_path);
+  if (!db_conn || !Deita_Connection_Is_Open(db_conn))
+  {
+    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] Thread ERROR: Failed to open database for media_id=%lld\n", (long long)ctx->media_id);
+    free(ctx);
+    return NULL;
+  }
+
+  // Update status to 'processing'
+  const char *update_processing =
+    "UPDATE media_uploads SET status='processing', updated_at=strftime('%s','now') WHERE id=?";
+  char media_id_str[32];
+  snprintf(media_id_str, sizeof(media_id_str), "%lld", (long long)ctx->media_id);
+  const char *params[] = { media_id_str };
+  int32 update_result = Deita_Query_Execute_Update_Prepared(db_conn, update_processing, 1, params);
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Updated status to 'processing' for media_id=%lld (result=%d)\n", (long long)ctx->media_id, update_result);
+
+  // Generate presigned GET URL for download (10 min expiry)
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Generating presigned GET URL for media_id=%lld\n", (long long)ctx->media_id);
+  S3_Presigned_URL download_url = S3_Presign_Get(&ctx->s3_config, ctx->s3_key_original, 600);
+  if (!download_url.success)
+  {
+    const char *error_msg = download_url.error_message ? download_url.error_message : "Failed to generate download URL";
+    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: Failed to generate download URL for media_id=%lld: %s\n",
+               (long long)ctx->media_id, error_msg);
+    const char *update_error =
+      "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?";
+    const char *error_params[] = { error_msg, media_id_str };
+    Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params);
+    S3_Presigned_URL_Destroy(&download_url);
+    Deita_Connection_Close(db_conn);
+    free(ctx);
+    return NULL;
+  }
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Generated presigned URL: %.100s...\n", download_url.url);
+
+  // Generate temp file paths
+  char tmp_input[256];
+  char tmp_output[256];
+  char *uuid_input = malloc(UUID_LEN);
+  char *uuid_output = malloc(UUID_LEN);
+  uint32 seed1 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  uint32 seed2 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  Dowa_String_UUID(seed1, uuid_input);
+  Dowa_String_UUID(seed2, uuid_output);
+  snprintf(tmp_input, sizeof(tmp_input), "/tmp/%s", uuid_input);
+  snprintf(tmp_output, sizeof(tmp_output), "/tmp/%s.webp", uuid_output);
+  free(uuid_input);
+  free(uuid_output);
+
+  // Download from S3
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Downloading from S3 to %s for media_id=%lld\n", tmp_input, (long long)ctx->media_id);
+  Seobeo_Client_Request *download_req = Seobeo_Client_Request_Create(download_url.url);
+  Seobeo_Client_Request_Set_Download_Path(download_req, tmp_input);
+  Seobeo_Client_Response *download_resp = Seobeo_Client_Request_Execute(download_req);
+
+  S3_Presigned_URL_Destroy(&download_url);
+
+  if (!download_resp || download_resp->status_code != 200)
+  {
+    int status = download_resp ? download_resp->status_code : 0;
+    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: Failed to download from S3 for media_id=%lld (status=%d)\n",
+               (long long)ctx->media_id, status);
+    const char *update_error =
+      "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?";
+    const char *error_params[] = { "Failed to download from S3", media_id_str };
+    Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params);
+    if (download_req) Seobeo_Client_Request_Destroy(download_req);
+    if (download_resp) Seobeo_Client_Response_Destroy(download_resp);
+    unlink(tmp_input);
+    Deita_Connection_Close(db_conn);
+    free(ctx);
+    return NULL;
+  }
+
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully downloaded file to %s\n", tmp_input);
+  Seobeo_Client_Request_Destroy(download_req);
+  Seobeo_Client_Response_Destroy(download_resp);
+
+  // Convert to webp using FFmpeg
+  char cmd[1024];
+  char log_file[256];
+  snprintf(log_file, sizeof(log_file), "/tmp/ffmpeg_%lld.log", (long long)ctx->media_id);
+  snprintf(cmd, sizeof(cmd), "ffmpeg -y -i %s -quality 80 %s 2>%s",
+           tmp_input, tmp_output, log_file);
+
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Running FFmpeg: %s\n", cmd);
+  int ffmpeg_result = system(cmd);
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] FFmpeg result: %d for media_id=%lld\n", ffmpeg_result, (long long)ctx->media_id);
+
+  if (ffmpeg_result != 0)
+  {
+    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: FFmpeg conversion failed for media_id=%lld (exit code %d). Check log: %s\n",
+               (long long)ctx->media_id, ffmpeg_result, log_file);
+    const char *update_error =
+      "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?";
+    const char *error_params[] = { "Image conversion failed", media_id_str };
+    Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params);
+    unlink(tmp_input);
+    unlink(tmp_output);
+    Deita_Connection_Close(db_conn);
+    free(ctx);
+    return NULL;
+  }
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully converted to webp: %s\n", tmp_output);
+
+  // Upload processed file to S3
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Uploading processed file to S3: %s -> %s\n", tmp_output, ctx->s3_key_processed);
+  S3_Result upload_result = S3_Upload_File_With_Content_Type(
+    &ctx->s3_config, tmp_output, ctx->s3_key_processed, "image/webp");
+
+  if (!upload_result.success)
+  {
+    const char *error_msg = upload_result.error_message ? upload_result.error_message : "Failed to upload processed file";
+    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: Failed to upload processed file for media_id=%lld: %s (HTTP status: %d)\n",
+               (long long)ctx->media_id, error_msg, upload_result.status_code);
+    const char *update_error =
+      "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?";
+    const char *error_params[] = { error_msg, media_id_str };
+    Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params);
+    unlink(tmp_input);
+    unlink(tmp_output);
+    Deita_Connection_Close(db_conn);
+    free(ctx);
+    return NULL;
+  }
+
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully uploaded processed file to S3\n");
+
+  // Update status to 'finished'
+  const char *update_finished =
+    "UPDATE media_uploads SET status='finished', updated_at=strftime('%s','now') WHERE id=?";
+  Deita_Query_Execute_Update_Prepared(db_conn, update_finished, 1, params);
+
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully processed media_id=%lld - COMPLETE\n", (long long)ctx->media_id);
+
+  // Cleanup
+  unlink(tmp_input);
+  unlink(tmp_output);
+  Deita_Connection_Close(db_conn);
+  free(ctx);
+
+  return NULL;
+}
+
+// Media Upload API - Mark uploaded
+// POST /api/media/:id/uploaded
+// Headers: Authorization: Bearer <token>
+Seobeo_Request_Entry *MediaUploaded(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+
+  // Check auth token
+  void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization");
+  if (!auth_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena);
+    return resp;
+  }
+
+  const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value;
+  if (strncmp(auth_header, "Bearer ", 7) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena);
+    return resp;
+  }
+
+  const char *token = auth_header + 7;
+  if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "403", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena);
+    return resp;
+  }
+
+  if (!g_db_connection)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena);
+    return resp;
+  }
+
+  // Extract media_id from URL params
+  void *id_kv = Dowa_HashMap_Get_Ptr(req, ":id");
+  if (!id_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing media ID\"}", arena);
+    return resp;
+  }
+
+  const char *media_id_str = ((Seobeo_Request_Entry*)id_kv)->value;
+  int64 media_id = atoll(media_id_str);
+
+  // Verify access_token matches and get content_type
+  const char *select_query =
+    "SELECT content_type, s3_key_original, s3_key_processed FROM media_uploads WHERE id = ? AND access_token = ?";
+  const char *select_params[] = { media_id_str, token };
+
+  Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, select_params, arena);
+
+  if (!p_result || !Deita_Result_Set_Next(p_result))
+  {
+    if (p_result) Deita_Result_Set_Free(p_result);
+    Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Media not found or access denied\"}", arena);
+    return resp;
+  }
+
+  const char *content_type = Deita_Result_Set_Get_Text(p_result, 0);
+  const char *s3_key_original = Deita_Result_Set_Get_Text(p_result, 1);
+  const char *s3_key_processed = Deita_Result_Set_Get_Text(p_result, 2);
+
+  // Copy values before freeing result set
+  char content_type_copy[128];
+  char s3_key_original_copy[512];
+  char s3_key_processed_copy[512];
+  strncpy(content_type_copy, content_type, sizeof(content_type_copy) - 1);
+  strncpy(s3_key_original_copy, s3_key_original, sizeof(s3_key_original_copy) - 1);
+  strncpy(s3_key_processed_copy, s3_key_processed, sizeof(s3_key_processed_copy) - 1);
+  content_type_copy[sizeof(content_type_copy) - 1] = '\0';
+  s3_key_original_copy[sizeof(s3_key_original_copy) - 1] = '\0';
+  s3_key_processed_copy[sizeof(s3_key_processed_copy) - 1] = '\0';
+
+  Deita_Result_Set_Free(p_result);
+
+  // Update status to 'uploaded'
+  const char *update_query =
+    "UPDATE media_uploads SET status='uploaded', updated_at=strftime('%s','now') WHERE id=?";
+  const char *update_params[] = { media_id_str };
+  int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, update_query, 1, update_params);
+
+  if (result < 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to update status\"}", arena);
+    return resp;
+  }
+
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Content type for media_id=%lld: '%s'\n", (long long)media_id, content_type_copy);
+
+  // If content_type starts with "image/", spawn background processing thread
+  if (strncmp(content_type_copy, "image/", 6) == 0)
+  {
+    Seobeo_Log(SEOBEO_INFO, "[MEDIA] Detected image type, preparing to spawn background thread for media_id=%lld\n", (long long)media_id);
+
+    // Create context for background thread (heap allocated)
+    Media_Processing_Context *ctx = malloc(sizeof(Media_Processing_Context));
+    ctx->media_id = media_id;
+    strncpy(ctx->s3_key_original, s3_key_original_copy, sizeof(ctx->s3_key_original) - 1);
+    strncpy(ctx->s3_key_processed, s3_key_processed_copy, sizeof(ctx->s3_key_processed) - 1);
+    strncpy(ctx->content_type, content_type_copy, sizeof(ctx->content_type) - 1);
+    strncpy(ctx->access_token, token, sizeof(ctx->access_token) - 1);
+    strncpy(ctx->db_path, g_db_path, sizeof(ctx->db_path) - 1);
+    ctx->s3_key_original[sizeof(ctx->s3_key_original) - 1] = '\0';
+    ctx->s3_key_processed[sizeof(ctx->s3_key_processed) - 1] = '\0';
+    ctx->content_type[sizeof(ctx->content_type) - 1] = '\0';
+    ctx->access_token[sizeof(ctx->access_token) - 1] = '\0';
+    ctx->db_path[sizeof(ctx->db_path) - 1] = '\0';
+    ctx->s3_config = g_s3_config;
+
+    Seobeo_Log(SEOBEO_INFO, "[MEDIA] Creating pthread for media_id=%lld\n", (long long)media_id);
+
+    // Spawn detached thread
+    pthread_t thread_id;
+    int thread_result = pthread_create(&thread_id, NULL, Media_Process_Background, ctx);
+
+    if (thread_result != 0)
+    {
+      Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: pthread_create failed with result=%d for media_id=%lld\n", thread_result, (long long)media_id);
+      free(ctx);
+    }
+    else
+    {
+      // Detach thread so it cleans up automatically when done
+      pthread_detach(thread_id);
+      Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully spawned and detached thread for media_id=%lld\n", (long long)media_id);
+    }
+  }
+  else
+  {
+    Seobeo_Log(SEOBEO_INFO, "[MEDIA] Non-image file, skipping background processing for media_id=%lld\n", (long long)media_id);
+  }
+
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", "{\"success\":true,\"status\":\"uploaded\"}", arena);
+
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Marked uploaded media_id=%lld\n", (long long)media_id);
+
+  return resp;
+}
+
+// Media Upload API - Get status
+// GET /api/media/:id/status
+// Headers: Authorization: Bearer <token>
+// Returns: {"id": 123, "status": "finished", "processed_url": "https://...", "error_message": null}
+Seobeo_Request_Entry *MediaStatus(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+
+  // Check auth token
+  void *auth_kv = Dowa_HashMap_Get_Ptr(req, "Authorization");
+  if (!auth_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing Authorization header\"}", arena);
+    return resp;
+  }
+
+  const char *auth_header = ((Seobeo_Request_Entry*)auth_kv)->value;
+  if (strncmp(auth_header, "Bearer ", 7) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "401", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid Authorization format\"}", arena);
+    return resp;
+  }
+
+  const char *token = auth_header + 7;
+  if (strlen(g_upload_auth_token) == 0 || strcmp(token, g_upload_auth_token) != 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "403", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Invalid token\"}", arena);
+    return resp;
+  }
+
+  if (!g_db_connection)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Database not available\"}", arena);
+    return resp;
+  }
+
+  // Extract media_id from URL params
+  void *id_kv = Dowa_HashMap_Get_Ptr(req, ":id");
+  if (!id_kv)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Missing media ID\"}", arena);
+    return resp;
+  }
+
+  const char *media_id_str = ((Seobeo_Request_Entry*)id_kv)->value;
+
+  // Query media status
+  const char *select_query =
+    "SELECT id, status, s3_key_original, s3_key_processed, error_message FROM media_uploads WHERE id = ? AND access_token = ?";
+  const char *select_params[] = { media_id_str, token };
+
+  Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(g_db_connection, select_query, 2, select_params, arena);
+
+  if (!p_result || !Deita_Result_Set_Next(p_result))
+  {
+    if (p_result) Deita_Result_Set_Free(p_result);
+    Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Media not found\"}", arena);
+    return resp;
+  }
+
+  int64 id = Deita_Result_Set_Get_Integer(p_result, 0);
+  const char *status = Deita_Result_Set_Get_Text(p_result, 1);
+  const char *s3_key_original = Deita_Result_Set_Get_Text(p_result, 2);
+  const char *s3_key_processed = Deita_Result_Set_Get_Text(p_result, 3);
+  const char *error_message = Deita_Result_Set_Get_Text(p_result, 4);
+
+  // Build CloudFront URL for processed file if status is 'finished'
+  char processed_url[1024] = {0};
+  if (strcmp(status, "finished") == 0 && s3_key_processed && strlen(s3_key_processed) > 0)
+  {
+    if (g_s3_cloudfront_url[0])
+    {
+      snprintf(processed_url, sizeof(processed_url), "%s/%s", g_s3_cloudfront_url, s3_key_processed);
+    }
+    else
+    {
+      snprintf(processed_url, sizeof(processed_url), "https://%s.s3.%s.amazonaws.com/%s",
+               g_s3_bucket, g_s3_region, s3_key_processed);
+    }
+  }
+
+  // Build CloudFront URL for original file (for non-images or before processing completes)
+  char original_url[1024] = {0};
+  if (s3_key_original && strlen(s3_key_original) > 0)
+  {
+    if (g_s3_cloudfront_url[0])
+    {
+      snprintf(original_url, sizeof(original_url), "%s/%s", g_s3_cloudfront_url, s3_key_original);
+    }
+    else
+    {
+      snprintf(original_url, sizeof(original_url), "https://%s.s3.%s.amazonaws.com/%s",
+               g_s3_bucket, g_s3_region, s3_key_original);
+    }
+  }
+
+  // Build JSON response with both processed_url and original_url
+  char *response_body = Dowa_Arena_Allocate(arena, 3072);
+
+  // Build the base response
+  int offset = snprintf(response_body, 3072,
+                        "{\"id\":%lld,\"status\":\"%s\",",
+                        (long long)id, status);
+
+  // Add processed_url
+  if (strlen(processed_url) > 0)
+  {
+    offset += snprintf(response_body + offset, 3072 - offset,
+                       "\"processed_url\":\"%s\",", processed_url);
+  }
+  else
+  {
+    offset += snprintf(response_body + offset, 3072 - offset,
+                       "\"processed_url\":null,");
+  }
+
+  // Add original_url
+  if (strlen(original_url) > 0)
+  {
+    offset += snprintf(response_body + offset, 3072 - offset,
+                       "\"original_url\":\"%s\",", original_url);
+  }
+  else
+  {
+    offset += snprintf(response_body + offset, 3072 - offset,
+                       "\"original_url\":null,");
+  }
+
+  // Add error_message
+  if (error_message && strlen(error_message) > 0)
+  {
+    snprintf(response_body + offset, 3072 - offset,
+             "\"error_message\":\"%s\"}", error_message);
+  }
+  else
+  {
+    snprintf(response_body + offset, 3072 - offset,
+             "\"error_message\":null}");
+  }
+
+  Deita_Result_Set_Free(p_result);
+
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", response_body, arena);
+
+  return resp;
+}
 
 int main(void)
 {
+  // Load server config
+  load_config("mrjunejune/.config");
+
+  // Load S3 credentials from .env
+  FILE *env_file = fopen(".env", "r");
+  static char s3_access_key[128] = {0};
+  static char s3_secret_key[128] = {0};
+
+  if (env_file)
+  {
+    char line[512];
+    while (fgets(line, sizeof(line), env_file))
+    {
+      if (strncmp(line, "AWS_MRJUNEJUNE_ACCESS_KEY=", 26) == 0)
+      {
+        char *val = line + 26;
+        size_t len = strlen(val);
+        while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0';
+        strncpy(s3_access_key, val, sizeof(s3_access_key) - 1);
+      }
+      else if (strncmp(line, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY=", 33) == 0)
+      {
+        char *val = line + 33;
+        size_t len = strlen(val);
+        while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0';
+        strncpy(s3_secret_key, val, sizeof(s3_secret_key) - 1);
+      }
+    }
+    fclose(env_file);
+  }
+
+  // Initialize S3 config
+  g_s3_config.access_key_id = s3_access_key;
+  g_s3_config.secret_access_key = s3_secret_key;
+  g_s3_config.region = g_s3_region;
+  g_s3_config.bucket = g_s3_bucket;
+  g_s3_config.endpoint = NULL;
+  g_s3_config.use_path_style = FALSE;
+
+  printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n",
+         g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)");
+
+  // Show current working directory
+  char cwd[1024];
+  if (getcwd(cwd, sizeof(cwd)) != NULL)
+  {
+    printf("[STARTUP] Current working directory: %s\n", cwd);
+    printf("[STARTUP] Database path (relative): %s\n", g_db_path);
+  }
+
+  // Initialize database
+  init_database();
+
   Seobeo_Router_Init();
 
   Seobeo_Router_Register("GET", "/", GetHomePage);
@@ -527,6 +1828,18 @@
   Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4);
   Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile);
 
+  // -- S3 Upload --/
+  Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl);
+
+  // -- Media Upload --/
+  Seobeo_Router_Register("POST", "/api/media/create", MediaCreate);
+  Seobeo_Router_Register("POST", "/api/media/:id/uploaded", MediaUploaded);
+  Seobeo_Router_Register("GET", "/api/media/:id/status", MediaStatus);
+
+  // -- Editor --/
+  Seobeo_Router_Register("POST", "/api/editor/save", EditorSave);
+  Seobeo_Router_Register("GET", "/api/editor/load/:doc_id", EditorLoad);
+
   // -- Blog --/
   Seobeo_Router_Register("GET", "/blog", RenderBlogList);
   Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog);
@@ -535,8 +1848,17 @@
   Seobeo_Router_Register("GET", "/talk", GetTalk);
   Seobeo_Router_Register("GET", "/talk/index.html", GetRedirectTalk);
 
+  // -- Notes --/
+  Seobeo_Router_Register("GET", "/notes", GetNotes);
+  Seobeo_Router_Register("GET", "/notes/", GetNotes);
+  Seobeo_Router_Register("GET", "/notes/index.html", GetNotes);
+  Seobeo_Router_Register("GET", "/notes/login", GetNotesLogin);
+  Seobeo_Router_Register("GET", "/notes/login/", GetNotesLogin);
+  Seobeo_Router_Register("GET", "/notes/:note_id", GetNoteById);
+
   Seobeo_WebSocket_Server_Init();
   Seobeo_WebSocket_Server_Register("/chat", Chat_Handler, NULL);
 
-  Seobeo_Web_Server_Start("mrjunejune/src", "6969", SEOBEO_MODE_EDGE, 3);
+  Seobeo_Log(SEOBEO_INFO, "WTF is going on\n");
+  Seobeo_Web_Server_Start("mrjunejune/src", "6969", SEOBEO_MODE_EDGE, 1);
 }
--- a/mrjunejune/src/base.css	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/src/base.css	Sun Aug 02 16:50:48 2026 -0700
@@ -359,3 +359,40 @@
 }
 
 
+/* pwa icon */
+#pwa-install-btn {
+  position: fixed;
+  bottom: 20px;
+  right: 20px;
+  padding: 12px 24px 12px 36px;
+  background: #000000;
+  color: #eeffee;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  font-size: 14px;
+  font-weight: bold;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+  z-index: 1000;
+  font-family: inherit;
+}
+
+#pwa-install-btn.with-icon::before {
+  content: "";
+  position: absolute;
+  left: 14px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 18px;
+  height: 18px;
+
+  /* Masked icon */
+  -webkit-mask: url('/public/paw.svg') no-repeat center;
+  mask: url('/public/paw.svg') no-repeat center;
+  mask-size: contain;
+  mask-repeat: no-repeat;
+
+  /* Icon color = button text color */
+  background-color: currentColor;
+}
+
--- a/mrjunejune/src/index.html	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/src/index.html	Sun Aug 02 16:50:48 2026 -0700
@@ -29,6 +29,41 @@
           margin-bottom: 0.75em;
         }
       }
+
+      #background {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        pointer-events: none;
+        z-index: 0;
+      }
+
+      #game {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        pointer-events: none;
+        z-index: 1;
+        cursor: default;
+      }
+
+      #game.active {
+        pointer-events: auto;
+        z-index: 100;
+      }
+
+      #gameUI {
+        position: fixed;
+        top: 10px;
+        right: 10px;
+        pointer-events: auto;
+        z-index: 101;
+        cursor: pointer;
+      }
     </style>
   </head>
   <body>
@@ -53,10 +88,14 @@
          <li><a href="/resume">Resume</a> - My professional experiences </li>
          <li><a href="/tools">Tools</a> - Things I made for myself </li>
        </ul> 
+       <canvas id="background"></canvas>
+       <canvas id="game"></canvas>
+       <canvas id="gameUI"></canvas>
      </main>
      {{/parts/footer.html}}
   </body>
   <script>
+    // Epi image
     let arr = Array.from({ length: 18 }, (_, i) => i+1);
     function setRandomImages() {
       const randomIndex = Math.floor(Math.random() * arr.length);
@@ -69,4 +108,5 @@
     }
     setRandomImages();
   </script>
+  <script src="/public/dog-game.js"></script>
 </html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/notes/index.html	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  {{/parts/base_head.html}}
+  <title>Notes</title>
+  <style>
+    .notes-page {
+      min-width: 70%;
+      margin: 0 auto;
+      padding: 20px;
+    }
+    .notes-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 20px;
+      flex-wrap: wrap;
+      gap: 12px;
+    }
+    .notes-header h1 {
+      margin: 0;
+      display: flex;
+      align-items: center;
+      gap: 12px;
+    }
+    .note-id-display {
+      font-size: 14px;
+      color: #666;
+      background: #f0f0f0;
+      padding: 4px 12px;
+      border-radius: 4px;
+      font-weight: normal;
+    }
+    .notes-actions {
+      display: flex;
+      gap: 8px;
+      align-items: center;
+    }
+    .notes-actions button, .notes-actions a {
+      padding: 8px 16px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      background: #fff;
+      cursor: pointer;
+      text-decoration: none;
+      color: #333;
+      font-size: 14px;
+    }
+    .notes-actions button:hover, .notes-actions a:hover {
+      background: #f5f5f5;
+    }
+    .notes-actions .btn-primary {
+      background: #0078ff;
+      color: white;
+      border-color: #0078ff;
+    }
+    .notes-actions .btn-primary:hover {
+      background: #0066dd;
+    }
+    .notes-actions .btn-danger {
+      color: #d32f2f;
+      border-color: #d32f2f;
+    }
+    .new-note-dialog {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background: rgba(0,0,0,0.5);
+      display: none;
+      align-items: center;
+      justify-content: center;
+      z-index: 100;
+    }
+    .new-note-dialog.show {
+      display: flex;
+    }
+    .new-note-form {
+      background: white;
+      padding: 24px;
+      border-radius: 8px;
+      width: 400px;
+      max-width: 90%;
+    }
+    .new-note-form h2 {
+      margin: 0 0 16px 0;
+    }
+    .new-note-form input {
+      width: 100%;
+      padding: 10px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      font-size: 16px;
+      box-sizing: border-box;
+      margin-bottom: 16px;
+    }
+    .new-note-form .form-actions {
+      display: flex;
+      gap: 8px;
+      justify-content: flex-end;
+    }
+    .new-note-form button {
+      padding: 8px 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      font-size: 14px;
+    }
+    .new-note-form .btn-cancel {
+      background: #f5f5f5;
+      border: 1px solid #ccc;
+    }
+    .new-note-form .btn-create {
+      background: #0078ff;
+      color: white;
+      border: none;
+    }
+    .note-hint {
+      font-size: 12px;
+      color: #666;
+      margin-top: -12px;
+      margin-bottom: 16px;
+    }
+  </style>
+</head>
+<body>
+  {{/parts/header.html}}
+
+  <main class="notes-page">
+    <div class="notes-header">
+      <h1>
+        Notes
+        <span class="note-id-display" id="note-id-display">index</span>
+      </h1>
+      <div class="notes-actions">
+        <button onclick="goHome()"> Home </button>
+        <button onclick="logout()" class="btn-danger">Logout</button>
+      </div>
+    </div>
+
+    <div id="editor-container"></div>
+  </main>
+
+  <!-- New Note Dialog -->
+  <div class="new-note-dialog" id="new-note-dialog">
+    <div class="new-note-form">
+      <h2>Create New Note</h2>
+      <input type="text" id="new-note-id" placeholder="note-id (e.g., my-ideas)">
+      <p class="note-hint">Use lowercase letters, numbers, and hyphens only</p>
+      <div class="form-actions">
+        <button class="btn-cancel" onclick="hideNewNoteDialog()">Cancel</button>
+        <button class="btn-create" onclick="createNewNote()">Create & Open</button>
+      </div>
+    </div>
+  </div>
+
+  {{/parts/footer.html}}
+
+  <script src="/public/js/rich_editor.js"></script>
+  <script src="/public/editor.js"></script>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/notes/login.html	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  {{/parts/base_head.html}}
+  <title>Login | Notes</title>
+  <style>
+    .login-page {
+      max-width: 400px;
+      margin: 100px auto;
+      padding: 20px;
+    }
+    .login-box {
+      background: #fff;
+      border: 1px solid #ddd;
+      border-radius: 8px;
+      padding: 32px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+    }
+    .login-box h1 {
+      margin: 0 0 24px 0;
+      font-size: 24px;
+      text-align: center;
+    }
+    .login-box label {
+      display: block;
+      margin-bottom: 8px;
+      font-weight: 500;
+    }
+    .login-box input {
+      width: 100%;
+      padding: 12px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      font-size: 16px;
+      box-sizing: border-box;
+    }
+    .login-box input:focus {
+      outline: none;
+      border-color: #0078ff;
+    }
+    .login-box button {
+      width: 100%;
+      padding: 12px;
+      margin-top: 16px;
+      background: #0078ff;
+      color: white;
+      border: none;
+      border-radius: 4px;
+      font-size: 16px;
+      cursor: pointer;
+    }
+    .login-box button:hover {
+      background: #0066dd;
+    }
+    .error-msg {
+      color: #d32f2f;
+      margin-top: 12px;
+      text-align: center;
+      display: none;
+    }
+  </style>
+</head>
+<body>
+  {{/parts/header.html}}
+
+  <main class="login-page">
+    <div class="login-box">
+      <h1>Notes Login</h1>
+      <form id="login-form">
+        <label for="token">Access Token</label>
+        <input type="password" id="token" placeholder="Enter your access token" required>
+        <button type="submit">Login</button>
+        <p class="error-msg" id="error-msg">Invalid token</p>
+      </form>
+    </div>
+  </main>
+
+  {{/parts/footer.html}}
+
+  <script>
+    // Check if already logged in
+    const savedToken = localStorage.getItem('notes-auth-token');
+    if (savedToken) {
+      window.location.href = '/notes/';
+    }
+
+    document.getElementById('login-form').addEventListener('submit', async function(e) {
+      e.preventDefault();
+
+      const token = document.getElementById('token').value.trim();
+      if (!token) return;
+
+      // Verify token by trying to load a document
+      try {
+        const response = await fetch('/api/editor/load/index', {
+          headers: { 'Authorization': 'Bearer ' + token }
+        });
+
+        if (response.ok) {
+          localStorage.setItem('notes-auth-token', token);
+          // Redirect to originally requested page or /notes/
+          const returnUrl = new URLSearchParams(window.location.search).get('return') || '/notes/';
+          window.location.href = returnUrl;
+        } else {
+          document.getElementById('error-msg').style.display = 'block';
+        }
+      } catch (err) {
+        document.getElementById('error-msg').style.display = 'block';
+      }
+    });
+  </script>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/offline.html	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,61 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Offline - MrJuneJune</title>
+    <style>
+      body {
+        font-family: system-ui, -apple-system, sans-serif;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        min-height: 100vh;
+        margin: 0;
+        padding: 20px;
+        background: linear-gradient(135deg, #544e43 0%, #3a3630 100%);
+        color: white;
+        text-align: center;
+      }
+
+      h1 {
+        font-size: 3em;
+        margin: 0 0 0.5em 0;
+      }
+
+      p {
+        font-size: 1.2em;
+        margin: 0 0 2em 0;
+        opacity: 0.9;
+      }
+
+      button {
+        padding: 12px 32px;
+        font-size: 1em;
+        background: white;
+        color: #544e43;
+        border: none;
+        border-radius: 8px;
+        cursor: pointer;
+        font-weight: bold;
+        transition: transform 0.2s;
+      }
+
+      button:hover {
+        transform: scale(1.05);
+      }
+
+      .icon {
+        font-size: 5em;
+        margin-bottom: 0.3em;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="icon">📡</div>
+    <h1>You're Offline</h1>
+    <p>It looks like you've lost your internet connection.<br>Don't worry, some cached pages might still work!</p>
+    <button onclick="window.location.reload()">Try Again</button>
+  </body>
+</html>
--- a/mrjunejune/src/parts/base_head.html	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/src/parts/base_head.html	Sun Aug 02 16:50:48 2026 -0700
@@ -1,6 +1,9 @@
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#544e43">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
+<link rel="manifest" href="/public/manifest.json">
+<link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
 
 <link rel="preload" href="/public/fonts/Roboto-Regular.ttf" as="font"  crossorigin>
 <link rel="preload" href="/public/fonts/Roboto-Thin.ttf"as="font" crossorigin>
@@ -16,3 +19,5 @@
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
 
+<script src="/public/pwa-register.js" defer></script>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/dog-game.js	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,295 @@
+// Dog frames
+const imagesSrc = [
+  "/public/sprite_shiba0.png",
+  "/public/sprite_shiba1.png",
+  "/public/sprite_shiba2.png",
+  "/public/sprite_shiba3.png",
+];
+const images =imagesSrc.map((src, index) => {
+  const img = new Image();
+  img.src = src;
+  img.onload = () => {
+    if (index == imagesSrc.length-1)
+      startAnimation();
+  };
+  return img
+});
+
+// Load treat image
+const treatImage = new Image();
+treatImage.src = "/public/dog-treat.png";
+
+// Load star images for background
+let starImages = [
+  { src: "/public/start-large.png", img: new Image() },
+  { src: "/public/start-small-1.png", img: new Image() },
+  { src: "/public/start-small-2.png", img: new Image() },
+  { src: "/public/start-small-3.png", img: new Image() }
+];
+starImages.forEach(star => star.img.src = star.src);
+
+const background = document.getElementById('background');
+const bgCtx = background.getContext("2d");
+const ctx = game.getContext("2d");
+const uiCtx = gameUI.getContext("2d");
+
+
+// Dog initial position and movement
+const initialDogVx = 2;
+let x = Math.random() * (game.width);
+let y = Math.random() * (game.height);
+let vx = initialDogVx; 
+let vy = 0; 
+const gravity = 0.5;
+const jumpForce = -12;
+let isFlipped = false;
+
+// Random jump interval
+// TODO: let's do it by frame?
+let nextJump = Math.random() * 2000 + 1000;
+
+// Treat system
+const originalTreatSize = 30;
+let treatSize = originalTreatSize;
+const treatMargin = 20;
+const reachDistance = 50;
+let treatRainActive = false;
+let treats = []; 
+let lastTreatSpawn = 0;
+const treatFallSpeed = 0.25;
+const treatGravity = 0.13;
+
+// Background stars
+let stars = [];
+function createStars() {
+  stars = [];
+  const starCount = window.innerWidth * 0.05;
+
+  for (let i = 0; i < starCount; i++) {
+    const starType = starImages[Math.floor(Math.random() * starImages.length)];
+    const scale = 0.5 + Math.random() * 0.8; // Random scale
+
+    stars.push({
+      x: Math.random() * background.width,
+      y: Math.random() * background.height,
+      vx: (Math.random() - 0.5) * 0.5,
+      vy: (Math.random() - 0.5) * 0.5,
+      rotation: Math.random() * Math.PI * 2,
+      rotationSpeed: (Math.random() - 0.5) * 0.02,
+      image: starType.img,
+      size: starType.img.width,
+      scale: scale,
+      opacity: 0.3 + Math.random() * 0.4 // Random opacity
+    });
+  }
+}
+
+function animateBackground() {
+  bgCtx.clearRect(0, 0, background.width, background.height);
+
+  stars.forEach(star => {
+    // Update position
+    star.x += star.vx;
+    star.y += star.vy;
+    star.rotation += star.rotationSpeed;
+
+    // Wrap around edges
+    if (star.x < -100) star.x = background.width + 100;
+    if (star.x > background.width + 100) star.x = -100;
+    if (star.y < -100) star.y = background.height + 100;
+    if (star.y > background.height + 100) star.y = -100;
+
+    // Draw star
+    bgCtx.save();
+    bgCtx.globalAlpha = star.opacity;
+    bgCtx.translate(star.x, star.y);
+    bgCtx.rotate(star.rotation);
+
+    const size = star.size * star.scale;
+    bgCtx.drawImage(star.image, -size / 2, -size / 2, size, size);
+
+    bgCtx.restore();
+  });
+
+  requestAnimationFrame(animateBackground);
+}
+
+function getMousePosition(event) {
+  const rect = game.getBoundingClientRect();
+  const x = event.clientX - rect.left;
+  const y = event.clientY - rect.top;
+  return { x: x, y: y };
+}
+
+// UI canvas click - toggle treat rain
+gameUI.addEventListener('click', (e) => {
+  treatRainActive = !treatRainActive;
+  if (treatRainActive)
+    game.classList.add('active');
+  else
+    game.classList.remove('active');
+});
+
+game.addEventListener('click', (e) => {
+  if (treatRainActive) {
+    const mousePos = getMousePosition(e);
+    treats.push({ x: mousePos.x, y: mousePos.y, vy: treatFallSpeed });
+  }
+});
+
+function startAnimation() {
+  requestAnimationFrame(animate);
+}
+
+
+let index = 0;
+let last = 0;
+const frameDuration = 120; // ms per frame
+function animate(ts) {
+  if (ts - last > frameDuration) {
+    index = (index + 1) % images.length;
+    last = ts;
+  }
+
+  // Get image dimensions
+  const imgWidth = images[0].width;
+  const imgHeight = images[0].height;
+
+  // Update falling treats
+  treats.forEach(treat => {
+    treat.vy += treatGravity;
+    treat.y += treat.vy;
+  });
+  treats = treats.filter(treat => treat.y < game.height + treatSize);
+
+  // Find closest treat for dog to chase
+  let targetTreat = null;
+  let closestDist = Infinity;
+  treats.forEach(treat => {
+    const dx = treat.x + treatSize / 2 - (x + imgWidth / 2);
+    const dy = treat.y + treatSize / 2 - (y + imgHeight / 2);
+    const dist = Math.sqrt(dx * dx + dy * dy);
+
+    if (dist < closestDist) {
+      closestDist = dist;
+      targetTreat = treat;
+    }
+  });
+
+  // Dog behavior
+  if (targetTreat) {
+    const dx = targetTreat.x + treatSize / 2 - (x + imgWidth / 2);
+    const dy = targetTreat.y + treatSize / 2 - (y + imgHeight / 2);
+    const distance = Math.sqrt(dx * dx + dy * dy);
+
+    // Dog eating
+    if (distance < reachDistance)
+    {
+      treats = treats.filter(t => t !== targetTreat);
+      vx = initialDogVx;
+    }
+    // Looking for it
+    else
+    {
+      const speed = initialDogVx * 1.5;
+      vx = dx > 0 ? speed : -speed;
+
+      // Update flip direction
+      if ((dx > 0 && isFlipped) || (dx < 0 && !isFlipped))
+      {
+        isFlipped = !isFlipped;
+      }
+
+      // Jump if treat is above and dog is on ground
+      const onGround = y + imgHeight >= game.height - 5;
+      if (dy < -50 && onGround && Math.abs(vy) < 1)
+      {
+        vy = jumpForce;
+      }
+    }
+  }
+
+  // --- Entity updates --- // 
+  vy += gravity;
+  x += vx;
+  y += vy;
+
+  // Bounce off walls
+  if (x <= 0 || x + imgWidth >= game.width)
+  {
+    if (!targetTreat)
+    {
+      vx = -vx;
+      isFlipped = !isFlipped;
+    }
+    x = x <= 0 ? 0 : game.width - imgWidth;
+  }
+
+  // Bounce off floor
+  if (y + imgHeight >= game.height)
+  {
+    y = game.height - imgHeight;
+    vy = -vy * 0.7; 
+    if (Math.abs(vy) < 2) vy = 0;
+  }
+
+  // Random jumps (only when not chasing treat)
+  if (!targetTreat && Date.now() > nextJump && Math.abs(vy) < 1)
+  {
+    vy = jumpForce;
+    nextJump = Date.now() + 1000 + Math.random() * 3000;
+  }
+
+  // --- Draw entity --- // 
+  ctx.clearRect(0, 0, game.width, game.height);
+  ctx.save();
+
+  if (isFlipped) {
+    ctx.translate(x + imgWidth, y);
+    ctx.scale(-1, 1);
+    ctx.drawImage(images[index], 0, 0);
+  } else {
+    ctx.drawImage(images[index], x, y);
+  }
+
+  ctx.restore();
+  treats.forEach(treat => {
+    ctx.drawImage(treatImage, treat.x, treat.y, treatSize, treatSize);
+  });
+
+  drawUI();
+  // repeat
+  requestAnimationFrame(animate);
+}
+
+// Draw UI elements (treat toggle button)
+function drawUI() {
+  uiCtx.drawImage(treatImage, 0, 0, treatSize, treatSize);
+}
+
+// --- Window functions --- //
+function resizeCanvas() {
+  background.width = window.innerWidth;
+  background.height = window.innerHeight;
+  game.width = window.innerWidth;
+  game.height = window.innerHeight;
+
+  // resize treats
+  treatSize = window.innerWidth < 700 ? originalTreatSize * 0.6 : originalTreatSize;
+
+  // Size UI canvas to fit treat button + text
+  gameUI.width = treatSize + 10;
+  gameUI.height = treatSize + 25;
+
+  createStars(); // Recreate stars on resize
+  drawUI(); // Redraw UI after resize
+}
+resizeCanvas();
+window.addEventListener('resize', resizeCanvas);
+
+window.onload = () => {
+  resizeCanvas();     // ensure canvas has real size
+  createStars();      // stars need correct canvas size
+  animateBackground(); // safe to start background animation
+};
+
Binary file mrjunejune/src/public/dog-treat.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/editor.js	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,218 @@
+let editor = null;
+let currentNoteId = 'index';
+
+function getAuthToken() {
+  return localStorage.getItem('notes-auth-token');
+}
+
+function requireAuth() {
+  if (!getAuthToken()) {
+    const returnUrl = encodeURIComponent(window.location.pathname);
+    window.location.href = '/notes/login?return=' + returnUrl;
+    return false;
+  }
+  return true;
+}
+
+function goHome() {
+  window.location.href = '/notes';
+}
+
+function logout() {
+  localStorage.removeItem('notes-auth-token');
+  window.location.href = '/notes/login';
+}
+
+function getNoteIdFromPath() {
+  const path = window.location.pathname;
+  const match = path.match(/^\/notes\/(.+)$/);
+  if (match && match[1] && match[1] !== 'login') {
+    return decodeURIComponent(match[1]);
+  }
+  return 'index';
+}
+
+function showNewNoteDialog() {
+  document.getElementById('new-note-dialog').classList.add('show');
+  document.getElementById('new-note-id').focus();
+}
+
+function hideNewNoteDialog() {
+  document.getElementById('new-note-dialog').classList.remove('show');
+  document.getElementById('new-note-id').value = '';
+}
+
+function createNewNote() {
+  let noteId = document.getElementById('new-note-id').value.trim();
+  if (!noteId) return;
+
+  // Sanitize note ID
+  noteId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
+
+  hideNewNoteDialog();
+  window.location.href = '/notes/' + encodeURIComponent(noteId);
+}
+
+// Handle Enter key in new note dialog
+document.getElementById('new-note-id').addEventListener('keydown', function(e) {
+  if (e.key === 'Enter') {
+    e.preventDefault();
+    createNewNote();
+  }
+  if (e.key === 'Escape') {
+    hideNewNoteDialog();
+  }
+});
+
+// Close dialog on backdrop click
+document.getElementById('new-note-dialog').addEventListener('click', function(e) {
+  if (e.target === this) {
+    hideNewNoteDialog();
+  }
+});
+
+async function uploadFile(file) {
+  const token = getAuthToken();
+  if (!token) {
+    throw new Error('Not authenticated');
+  }
+
+  // 1. Create media record
+  const createResponse = await fetch('/api/media/create', {
+    method: 'POST',
+    headers: {
+      'Authorization': 'Bearer ' + token,
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify({
+      filename: file.name,
+      content_type: file.type
+    })
+  });
+
+  if (!createResponse.ok) {
+    const error = await createResponse.json().catch(() => ({}));
+    throw new Error(error.error || 'Failed to create media record');
+  }
+
+  const data = await createResponse.json();
+
+  // 2. Upload file directly to S3
+  const uploadResponse = await fetch(data.upload_url, {
+    method: 'PUT',
+    headers: {
+      'Content-Type': file.type
+    },
+    body: file
+  });
+
+  if (!uploadResponse.ok) {
+    throw new Error('Failed to upload file to S3');
+  }
+
+  // 3. Mark as uploaded (triggers processing for images)
+  await fetch(`/api/media/${data.media_id}/uploaded`, {
+    method: 'POST',
+    headers: {
+      'Authorization': 'Bearer ' + token
+    }
+  });
+
+  // 4. Poll for images, return immediately for non-images
+  if (file.type.startsWith('image/')) {
+    return await pollForProcessedImage(data.media_id, token);
+  } else {
+    // For non-images, use the public URL from the server
+    return { url: data.public_url };
+  }
+}
+
+async function pollForProcessedImage(mediaId, token) {
+  const maxAttempts = 60; // 2 minutes max (60 * 2 seconds)
+
+  for (let i = 0; i < maxAttempts; i++) {
+    await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second interval
+
+    const statusResponse = await fetch(`/api/media/${mediaId}/status`, {
+      headers: {
+        'Authorization': 'Bearer ' + token
+      }
+    });
+
+    if (!statusResponse.ok) {
+      console.warn('Status check failed, retrying...');
+      continue;
+    }
+
+    const statusData = await statusResponse.json();
+
+    if (statusData.status === 'finished') {
+      return { url: statusData.processed_url };
+    } else if (statusData.status === 'error') {
+      throw new Error(statusData.error_message || 'Processing failed');
+    }
+    // Status is 'uploaded' or 'processing', continue polling
+  }
+
+  throw new Error('Processing timeout after 2 minutes');
+}
+
+async function saveContent(content) {
+  const token = getAuthToken();
+  if (!token) return;
+
+  const response = await fetch('/api/editor/save', {
+    method: 'POST',
+    headers: {
+      'Authorization': 'Bearer ' + token,
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify({
+      doc_id: currentNoteId,
+      content: content
+    })
+  });
+
+  if (!response.ok) {
+    throw new Error('Failed to save');
+  }
+}
+
+async function loadNote(noteId) {
+  const token = getAuthToken();
+  if (!token) return;
+
+  try {
+    const response = await fetch('/api/editor/load/' + encodeURIComponent(noteId), {
+      headers: { 'Authorization': 'Bearer ' + token }
+    });
+
+    if (response.ok) {
+      const data = await response.json();
+      editor.setContent(data.content || '');
+    }
+  } catch (error) {
+    console.error('Failed to load note:', error);
+  }
+}
+
+// Initialize
+document.addEventListener('DOMContentLoaded', function() {
+  if (!requireAuth()) return;
+
+  currentNoteId = getNoteIdFromPath();
+  document.getElementById('note-id-display').textContent = currentNoteId;
+
+  // Update page title
+  document.title = currentNoteId + ' | Notes';
+
+  editor = RichEditor.init('editor-container', {
+    uploadCallback: uploadFile,
+    saveCallback: saveContent,
+    debounceMs: 1500,
+    placeholder: 'Start writing... (paste images, drag files, or use /upload)\n\nTip: Click "+ New Note" to create linked notes.'
+  });
+
+  loadNote(currentNoteId);
+});
+
Binary file mrjunejune/src/public/icon-192.png has changed
Binary file mrjunejune/src/public/icon-512.png has changed
Binary file mrjunejune/src/public/icon-maskable-512.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/manifest.json	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,67 @@
+{
+  "name": "MrJuneJune",
+  "short_name": "MrJuneJune",
+  "description": "Personal website and blog of Juntae (June)",
+  "start_url": "/",
+  "display": "standalone",
+  "background_color": "#ffffff",
+  "theme_color": "#544e43",
+  "orientation": "portrait-primary",
+  "icons": [
+    {
+      "src": "/public/epi_all_colors.svg",
+      "sizes": "any",
+      "type": "image/svg+xml",
+      "purpose": "any maskable"
+    },
+    {
+      "src": "/public/icon-192.png",
+      "sizes": "192x192",
+      "type": "image/png",
+      "purpose": "any"
+    },
+    {
+      "src": "/public/icon-512.png",
+      "sizes": "512x512",
+      "type": "image/png",
+      "purpose": "any"
+    },
+    {
+      "src": "/public/icon-maskable-512.png",
+      "sizes": "512x512",
+      "type": "image/png",
+      "purpose": "maskable"
+    }
+  ],
+  "screenshots": [
+    {
+      "src": "/public/screenshot-mobile.png",
+      "sizes": "540x720",
+      "type": "image/png",
+      "form_factor": "narrow"
+    },
+    {
+      "src": "/public/screenshot-desktop.png",
+      "sizes": "1280x720",
+      "type": "image/png",
+      "form_factor": "wide"
+    }
+  ],
+  "categories": ["lifestyle", "productivity"],
+  "shortcuts": [
+    {
+      "name": "Blog",
+      "short_name": "Blog",
+      "description": "Read latest blog posts",
+      "url": "/blog",
+      "icons": [{ "src": "/public/epi_all_colors.svg", "sizes": "192x192" }]
+    },
+    {
+      "name": "Notes",
+      "short_name": "Notes",
+      "description": "Access your notes",
+      "url": "/notes",
+      "icons": [{ "src": "/public/epi_all_colors.svg", "sizes": "192x192" }]
+    }
+  ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/pwa-register.js	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,88 @@
+// PWA Service Worker Registration
+if ('serviceWorker' in navigator) {
+  window.addEventListener('load', () => {
+    navigator.serviceWorker
+      .register('/public/sw.js')
+      .then((registration) => {
+        // console.log('[PWA] Service Worker registered:', registration.scope);
+
+        // Check for updates periodically
+        setInterval(() => {
+          registration.update();
+        }, 60000); // Check every minute
+
+        // Handle updates
+        registration.addEventListener('updatefound', () => {
+          const newWorker = registration.installing;
+          newWorker.addEventListener('statechange', () => {
+            if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
+              // New content available, show update notification
+              if (confirm('New version available! Reload to update?')) {
+                newWorker.postMessage({ type: 'SKIP_WAITING' });
+                window.location.reload();
+              }
+            }
+          });
+        });
+      })
+      .catch((error) => {
+        console.error('[PWA] Service Worker registration failed:', error);
+      });
+
+    // Handle service worker updates
+    let refreshing = false;
+    navigator.serviceWorker.addEventListener('controllerchange', () => {
+      if (!refreshing) {
+        refreshing = true;
+        window.location.reload();
+      }
+    });
+  });
+}
+
+// Add install prompt handler
+let deferredPrompt;
+
+window.addEventListener('beforeinstallprompt', (e) => {
+  e.preventDefault();
+  deferredPrompt = e;
+
+  showInstallPromotion();
+});
+
+function showInstallPromotion() {
+  // Create an install button if it doesn't exist
+  if (document.getElementById('pwa-install-btn')) return;
+
+  const installBtn = document.createElement('button');
+  installBtn.classList.add('with-icon');
+  installBtn.id = 'pwa-install-btn';
+  installBtn.textContent = 'Install as App';
+
+  installBtn.addEventListener('click', async () => {
+    if (!deferredPrompt) return;
+
+    deferredPrompt.prompt();
+    const { outcome } = await deferredPrompt.userChoice;
+    console.log(`[PWA] User response: ${outcome}`);
+
+    deferredPrompt = null;
+    installBtn.remove();
+  });
+
+  document.body.appendChild(installBtn);
+
+  setTimeout(() => {
+    installBtn.style.opacity = '0';
+    installBtn.style.transition = 'opacity 0.3s';
+    setTimeout(() => installBtn.remove(), 300);
+  }, 5000);
+}
+
+window.addEventListener('appinstalled', () => {
+  console.log('[PWA] App installed successfully!');
+  deferredPrompt = null;
+
+  const installBtn = document.getElementById('pwa-install-btn');
+  if (installBtn) installBtn.remove();
+});
Binary file mrjunejune/src/public/shiba_open_sprites.zip has changed
Binary file mrjunejune/src/public/sprite_shiba0.png has changed
Binary file mrjunejune/src/public/sprite_shiba1.png has changed
Binary file mrjunejune/src/public/sprite_shiba2.png has changed
Binary file mrjunejune/src/public/sprite_shiba3.png has changed
Binary file mrjunejune/src/public/start-large.png has changed
Binary file mrjunejune/src/public/start-small-1.png has changed
Binary file mrjunejune/src/public/start-small-2.png has changed
Binary file mrjunejune/src/public/start-small-3.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/sw.js	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,138 @@
+// Service Worker for MrJuneJune PWA
+const CACHE_VERSION = 'v1';
+const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`;
+
+// Files to cache immediately on install
+const STATIC_CACHE = [
+  '/',
+  '/base.css',
+  '/public/epi_all_colors.svg',
+  '/public/fonts/Roboto-Regular.ttf',
+  '/public/fonts/Roboto-Thin.ttf',
+  '/public/fonts/more-sugar.regular.otf',
+  '/public/fonts/more-sugar.thin.otf',
+];
+
+// Install event - cache static assets
+self.addEventListener('install', (event) => {
+  console.log('[SW] Installing service worker...');
+
+  event.waitUntil(
+    caches.open(CACHE_NAME).then((cache) => {
+      console.log('[SW] Caching static assets');
+      return cache.addAll(STATIC_CACHE);
+    }).then(() => {
+      console.log('[SW] Skip waiting');
+      return self.skipWaiting();
+    })
+  );
+});
+
+// Activate event - clean up old caches
+self.addEventListener('activate', (event) => {
+  console.log('[SW] Activating service worker...');
+
+  event.waitUntil(
+    caches.keys().then((cacheNames) => {
+      return Promise.all(
+        cacheNames.map((cacheName) => {
+          if (cacheName !== CACHE_NAME) {
+            console.log('[SW] Deleting old cache:', cacheName);
+            return caches.delete(cacheName);
+          }
+        })
+      );
+    }).then(() => {
+      console.log('[SW] Claiming clients');
+      return self.clients.claim();
+    })
+  );
+});
+
+// Fetch event - serve from cache, fallback to network
+self.addEventListener('fetch', (event) => {
+  const { request } = event;
+  const url = new URL(request.url);
+
+  // Skip non-GET requests
+  if (request.method !== 'GET') {
+    return;
+  }
+
+  // Skip chrome-extension and other non-http(s) requests
+  if (!url.protocol.startsWith('http')) {
+    return;
+  }
+
+  // Skip API calls and media uploads (always go to network)
+  if (url.pathname.startsWith('/api/')) {
+    return;
+  }
+
+  event.respondWith(
+    caches.match(request).then((cachedResponse) => {
+      if (cachedResponse) {
+        console.log('[SW] Serving from cache:', url.pathname);
+        return cachedResponse;
+      }
+
+      // Not in cache, fetch from network
+      return fetch(request).then((networkResponse) => {
+        // Only cache successful responses
+        if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {
+          return networkResponse;
+        }
+
+        // Cache specific file types
+        const shouldCache =
+          url.pathname.endsWith('.css') ||
+          url.pathname.endsWith('.js') ||
+          url.pathname.endsWith('.svg') ||
+          url.pathname.endsWith('.png') ||
+          url.pathname.endsWith('.jpg') ||
+          url.pathname.endsWith('.webp') ||
+          url.pathname.endsWith('.woff') ||
+          url.pathname.endsWith('.woff2') ||
+          url.pathname.endsWith('.ttf') ||
+          url.pathname.endsWith('.otf') ||
+          url.pathname.startsWith('/blog/') ||
+          url.pathname.startsWith('/notes/') ||
+          url.pathname === '/';
+
+        if (shouldCache) {
+          const responseToCache = networkResponse.clone();
+          caches.open(CACHE_NAME).then((cache) => {
+            console.log('[SW] Caching new resource:', url.pathname);
+            cache.put(request, responseToCache);
+          });
+        }
+
+        return networkResponse;
+      }).catch((error) => {
+        console.log('[SW] Fetch failed:', error);
+
+        // Return offline page for HTML requests
+        if (request.headers.get('Accept').includes('text/html')) {
+          return caches.match('/offline.html');
+        }
+      });
+    })
+  );
+});
+
+// Handle messages from the client
+self.addEventListener('message', (event) => {
+  if (event.data && event.data.type === 'SKIP_WAITING') {
+    self.skipWaiting();
+  }
+
+  if (event.data && event.data.type === 'CLEAR_CACHE') {
+    event.waitUntil(
+      caches.keys().then((cacheNames) => {
+        return Promise.all(
+          cacheNames.map((cacheName) => caches.delete(cacheName))
+        );
+      })
+    );
+  }
+});
--- a/mrjunejune/src/resume/index.html	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/src/resume/index.html	Sun Aug 02 16:50:48 2026 -0700
@@ -27,7 +27,7 @@
             <a href="mailto:[email protected]">[email protected]</a>
           </div>
           <div>
-            <a href="https://www.linkedin.com/in/junepark"><svg width="12" height="12" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16">
+            <a href="https://www.linkedin.com/in/june-park-61721215b"><svg width="12" height="12" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16">
                <path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/>
              </svg> June Park
             </a>
--- a/mrjunejune/src/tools/index.html	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/src/tools/index.html	Sun Aug 02 16:50:48 2026 -0700
@@ -11,9 +11,9 @@
        <ul class="nav-list">
          <li><a href="/tools/markdown_to_html">MarkDown to HTML</a></li>
          <li><a href="/tools/file_converter">Images to Webp / Video to Mp4</a></li>
+         <li><a href="/notes">Personal Notes</a></li>
        </ul>
        <h3> TODOs </h3>
-       <p> Probably should add this... </p>
        <ul class="nav-list">
          <li>- Simple online LaTex editor.</li>
          <li>- Online HLS player.</li>
--- a/mrjunejune/src/tools/markdown_to_html/index.html	Sun Jan 25 10:44:04 2026 -0800
+++ b/mrjunejune/src/tools/markdown_to_html/index.html	Sun Aug 02 16:50:48 2026 -0700
@@ -54,7 +54,7 @@
 
 ### Images
 
-![Alt text](https://via.placeholder.com/150)
+![Alt text](https://mrjunejune.com/public/epi_favicon.svg)
 
 **Try editing this text!**</textarea>
         </div>
@@ -68,10 +68,16 @@
         </div>
     </div>
     {{/parts/footer.html}}
-    <script src="/markdown_to_html_bin.js"></script>
-    <script>
-        // Wait for WASM module to be ready
-        Module.onRuntimeInitialized = () => {
+    <script type="module">
+        import createModule from '/markdown_to_html_bin.js';
+
+        // Get DOM elements
+        const input = document.getElementById('input');
+        const output = document.getElementById('output');
+        const copy = document.getElementById('copy');
+
+        // Modern Emscripten exports a factory function that returns a Promise
+        createModule().then(Module => {
           // Get raw pointer so we can free it after use
           const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
           const markdownFree = Module.cwrap('markdown_free', null, ['number']);
@@ -108,7 +114,7 @@
               console.error('Failed to copy: ', err);
             });
           });
-        };
+        });
     </script>
 </body>
 </html>
--- a/react_games/public/base.css	Sun Jan 25 10:44:04 2026 -0800
+++ b/react_games/public/base.css	Sun Aug 02 16:50:48 2026 -0700
@@ -5,7 +5,6 @@
 }
 
 body {
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
   background: linear-gradient(135deg, #667eea 10%, #764ba2 100%);
   background-attachment: fixed;
   background-repeat: no-repeat;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rich_editor/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,5 @@
+filegroup(
+  name = "rich_editor",
+  srcs = ["rich_editor.js"],
+  visibility = ["//visibility:public"],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rich_editor/rich_editor.js	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,1062 @@
+/**
+ * Rich Editor
+ * -----------
+ * A vanilla JavaScript rich text editor with:
+ * - Text formatting (h1-h6, paragraphs, lists)
+ * - Image/file upload via paste, drop, click, or /upload command
+ * - Debounced auto-save
+ * - Generic callback functions for uploads and saves
+ *
+ * Usage:
+ *   const editor = RichEditor.init('editor-container', {
+ *     uploadCallback: async (file) => { return { url: '...', key: '...' }; },
+ *     saveCallback: async (content) => { console.log('Saved:', content); },
+ *     debounceMs: 1000,
+ *     placeholder: 'Start writing...'
+ *   });
+ */
+
+const RichEditor = (function() {
+  'use strict';
+
+  const DEFAULT_OPTIONS = {
+    uploadCallback: null,
+    saveCallback: null,
+    debounceMs: 1000,
+    placeholder: 'Start writing... (Use /upload to insert files)',
+    cloudFrontUrl: ''
+  };
+
+  function debounce(func, wait) {
+    let timeout;
+    return function executedFunction(...args) {
+      const later = () => {
+        clearTimeout(timeout);
+        func(...args);
+      };
+      clearTimeout(timeout);
+      timeout = setTimeout(later, wait);
+    };
+  }
+
+  function createToolbar(editor) {
+    const toolbar = document.createElement('div');
+    toolbar.className = 'rich-editor-toolbar';
+
+    const buttons = [
+      { cmd: 'h1', label: '', title: 'Heading 1', icons: "/public/icons/h1.png" },
+      { cmd: 'h2', label: '', title: 'Heading 2', icons: "/public/icons/h2.png" },
+      { cmd: 'h3', label: '', title: 'Heading 3', icons: "/public/icons/h3.png" },
+      { cmd: 'p', label: '¶', title: 'Paragraph' },
+      { cmd: 'ul', label: '•', title: 'Bullet List' },
+      { cmd: 'ol', label: '1.', title: 'Numbered List' },
+      { cmd: 'bold', label: '', title: 'Bold', icons: "/public/icons/bold.png" },
+      { cmd: 'separator' },
+      { cmd: 'link', label: '', title: 'Insert Link', icons: "/public/icons/link.png"  },
+      { cmd: 'notelink', label: '', title: 'Link to Note', icons: "/public/icons/binder.png"  },
+      { cmd: 'upload', label: '', title: 'Upload File', icons: "/public/icons/clipy.png"  },
+      { cmd: 'separator' },
+      { cmd: 'readonly', label: '', title: 'readonly', icons: "/public/icons/book.png" },
+    ];
+
+    buttons.forEach(btn => {
+      if (btn.cmd === 'separator') {
+        const sep = document.createElement('span');
+        sep.className = 'rich-editor-separator';
+        toolbar.appendChild(sep);
+        return;
+      }
+
+      const button = document.createElement('button');
+      button.type = 'button';
+      button.className = 'rich-editor-btn';
+      if (btn.icons)
+        button.style.backgroundImage = `url("${btn.icons}")`;
+      button.textContent = btn.label;
+      button.title = btn.title;
+      button.dataset.cmd = btn.cmd;
+
+      button.addEventListener('click', (e) => {
+        e.preventDefault();
+        editor.execCommand(btn.cmd);
+      });
+
+      toolbar.appendChild(button);
+    });
+
+    return toolbar;
+  }
+
+  function ensureViewportMeta() {
+    // Check if viewport meta tag exists
+    let viewport = document.querySelector('meta[name="viewport"]');
+    if (!viewport) {
+      viewport = document.createElement('meta');
+      viewport.name = 'viewport';
+      viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=5.0';
+      document.head.appendChild(viewport);
+    }
+  }
+
+  function createStyles() {
+    if (document.getElementById('rich-editor-styles')) return;
+
+    ensureViewportMeta();
+
+    const style = document.createElement('style');
+    style.id = 'rich-editor-styles';
+    style.textContent = `
+      .rich-editor-container {
+        border: 1px solid #ccc;
+        border-radius: 4px;
+        overflow: hidden;
+      }
+
+      .rich-editor-toolbar {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 4px;
+        padding: 8px;
+        background: #f5f5f5;
+        border-bottom: 1px solid #ccc;
+      }
+
+      .rich-editor-btn {
+        padding: 6px 12px;
+        border: 1px solid #ccc;
+        border-radius: 4px;
+        background: #fff;
+        cursor: pointer;
+        font-size: 14px;
+        min-width: 32px;
+        background-repeat: no-repeat;
+        background-position: center;
+        background-size: 24px 24px;
+        border: none;
+      }
+
+      .rich-editor-btn:hover {
+        background: #e9e9e9;
+        background-repeat: no-repeat;
+        background-position: center;
+        background-size: 24px 24px;
+      }
+
+      .rich-editor-btn:active {
+        background: #ddd;
+        background-repeat: no-repeat;
+        background-position: center;
+        background-size: 24px 24px;
+      }
+
+      .rich-editor-separator {
+        width: 1px;
+        background: #ccc;
+        margin: 0 4px;
+      }
+
+      .rich-editor-content {
+        min-height: 300px;
+        padding: 16px;
+        outline: none;
+        overflow-y: auto;
+      }
+
+      .rich-editor-content:empty:before {
+        content: attr(data-placeholder);
+        color: #999;
+        pointer-events: none;
+      }
+
+      .rich-editor-content img {
+        max-width: 100%;
+        height: auto;
+        border-radius: 4px;
+        margin: 8px 0;
+        cursor: pointer;
+      }
+
+      .rich-editor-content img.selected {
+        outline: 2px solid #0078ff;
+        outline-offset: 2px;
+      }
+
+      .rich-editor-resize-wrapper {
+        position: relative;
+        display: inline-block;
+        margin: 8px 0;
+      }
+
+      .rich-editor-resize-handle {
+        position: absolute;
+        width: 12px;
+        height: 12px;
+        background: #0078ff;
+        border: 2px solid white;
+        border-radius: 50%;
+        cursor: nwse-resize;
+        z-index: 10;
+      }
+
+      .rich-editor-resize-handle.nw {
+        top: -6px;
+        left: -6px;
+        cursor: nwse-resize;
+      }
+
+      .rich-editor-resize-handle.ne {
+        top: -6px;
+        right: -6px;
+        cursor: nesw-resize;
+      }
+
+      .rich-editor-resize-handle.sw {
+        bottom: -6px;
+        left: -6px;
+        cursor: nesw-resize;
+      }
+
+      .rich-editor-resize-handle.se {
+        bottom: -6px;
+        right: -6px;
+        cursor: nwse-resize;
+      }
+
+      .rich-editor-size-input {
+        position: absolute;
+        top: -35px;
+        left: 50%;
+        transform: translateX(-50%);
+        background: white;
+        border: 1px solid #ccc;
+        padding: 4px 8px;
+        border-radius: 4px;
+        font-size: 12px;
+        display: flex;
+        gap: 8px;
+        align-items: center;
+        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+        z-index: 11;
+      }
+
+      .rich-editor-size-input input {
+        width: 60px;
+        padding: 2px 4px;
+        border: 1px solid #ccc;
+        border-radius: 2px;
+        font-size: 12px;
+      }
+
+      .rich-editor-size-input label {
+        font-size: 11px;
+        color: #666;
+      }
+
+      .rich-editor-content a {
+        color: #0078ff;
+        text-decoration: none;
+      }
+
+      .rich-editor-content a:hover {
+        text-decoration: underline;
+      }
+
+      .rich-editor-content a.note-link {
+        background: #e8f4ff;
+        padding: 2px 6px;
+        border-radius: 3px;
+      }
+
+      .rich-editor-content a.note-link:hover {
+        background: #d0e8ff;
+      }
+
+      .rich-editor-content .upload-placeholder {
+        display: inline-block;
+        padding: 8px 16px;
+        background: #f0f0f0;
+        border-radius: 4px;
+        color: #666;
+        font-style: italic;
+      }
+
+      .rich-editor-content .upload-placeholder.loading {
+        padding: 8px 16px;
+        background: #fffacd;
+        border-radius: 4px;
+        animation: pulse 1.5s ease-in-out infinite;
+      }
+
+      @keyframes pulse {
+        0%, 100% { opacity: 1; }
+        50% { opacity: 0.6; }
+      }
+
+      .rich-editor-upload-overlay {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: rgba(0, 120, 255, 0.1);
+        border: 2px dashed #0078ff;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 18px;
+        color: #0078ff;
+        pointer-events: none;
+        z-index: 10;
+        padding: 20px;
+        text-align: center;
+      }
+
+      .rich-editor-status {
+        padding: 4px 8px;
+        font-size: 12px;
+        color: #666;
+        background: #f9f9f9;
+        border-top: 1px solid #eee;
+      }
+
+      .rich-editor-file-input {
+        display: none;
+      }
+
+      ol,ul {
+        padding: 16px;
+      }
+
+      /* Mobile optimizations */
+      @media (max-width: 720px) {
+        .rich-editor-toolbar {
+          padding: 6px;
+          gap: 3px;
+          overflow-x: auto;
+          -webkit-overflow-scrolling: touch;
+        }
+
+        .rich-editor-btn {
+          min-width: 44px;
+          min-height: 44px;
+          padding: 8px;
+          flex-shrink: 0;
+        }
+
+        .rich-editor-content {
+          min-height: 200px;
+          padding: 12px;
+          font-size: 16px; /* Prevents iOS zoom */
+          line-height: 1.6;
+        }
+
+        .rich-editor-content h1 {
+          font-size: 1.75em;
+        }
+
+        .rich-editor-content h2 {
+          font-size: 1.5em;
+        }
+
+        .rich-editor-content h3 {
+          font-size: 1.25em;
+        }
+
+        .rich-editor-status {
+          font-size: 11px;
+          padding: 6px 8px;
+        }
+
+        /* Mobile-friendly image resizing */
+        .rich-editor-resize-handle {
+          width: 20px;
+          height: 20px;
+          border-width: 3px;
+        }
+
+        .rich-editor-resize-handle.nw {
+          top: -10px;
+          left: -10px;
+        }
+
+        .rich-editor-resize-handle.ne {
+          top: -10px;
+          right: -10px;
+        }
+
+        .rich-editor-resize-handle.sw {
+          bottom: -10px;
+          left: -10px;
+        }
+
+        .rich-editor-resize-handle.se {
+          bottom: -10px;
+          right: -10px;
+        }
+
+        .rich-editor-size-input {
+          top: -40px;
+          padding: 6px 10px;
+        }
+
+        .rich-editor-size-input input {
+          width: 70px;
+          padding: 4px 6px;
+          font-size: 16px; /* Prevents iOS zoom */
+        }
+
+        /* Better touch targets for images */
+        .rich-editor-content img {
+          margin: 12px 0;
+        }
+
+        /* Hide drop overlay text on small screens */
+        .rich-editor-upload-overlay {
+          font-size: 16px;
+        }
+
+        /* Make link/note dialogs mobile-friendly */
+        .rich-editor-content a {
+          padding: 2px 0;
+        }
+      }
+
+      /* Extra small devices */
+      @media (max-width: 480px) {
+        .rich-editor-toolbar {
+          padding: 4px;
+          gap: 2px;
+        }
+
+        .rich-editor-btn {
+          min-width: 40px;
+          min-height: 40px;
+          padding: 6px;
+        }
+
+        .rich-editor-content {
+          padding: 10px;
+          font-size: 16px;
+        }
+
+        .rich-editor-size-input {
+          font-size: 11px;
+        }
+
+        .rich-editor-size-input input {
+          width: 60px;
+          font-size: 14px;
+        }
+      }
+    `;
+    document.head.appendChild(style);
+  }
+
+  class Editor {
+    constructor(elementId, options) {
+      this.options = { ...DEFAULT_OPTIONS, ...options };
+      this.container = document.getElementById(elementId);
+      this.state = {
+        readOnly: false,
+        selectedImage: null,
+        resizing: false
+      };
+
+      if (!this.container) {
+        throw new Error(`Element with id "${elementId}" not found`);
+      }
+
+      this.init();
+    }
+
+    init() {
+      createStyles();
+
+      // Create wrapper
+      this.wrapper = document.createElement('div');
+      this.wrapper.className = 'rich-editor-container';
+      this.wrapper.style.position = 'relative';
+
+      // Create toolbar
+      this.toolbar = createToolbar(this);
+      this.wrapper.appendChild(this.toolbar);
+
+      // Create content area
+      this.content = document.createElement('div');
+      this.content.className = 'rich-editor-content';
+      this.content.contentEditable = true;
+      this.content.dataset.placeholder = this.options.placeholder;
+      this.wrapper.appendChild(this.content);
+
+      // Create status bar
+      this.status = document.createElement('div');
+      this.status.className = 'rich-editor-status';
+      this.status.textContent = 'Ready';
+      this.wrapper.appendChild(this.status);
+
+      // Create hidden file input
+      this.fileInput = document.createElement('input');
+      this.fileInput.type = 'file';
+      this.fileInput.className = 'rich-editor-file-input';
+      this.fileInput.multiple = true;
+      this.fileInput.accept = 'image/*,application/pdf,.doc,.docx,.txt';
+      this.wrapper.appendChild(this.fileInput);
+
+      // Add to container
+      this.container.appendChild(this.wrapper);
+
+      // Setup event listeners
+      this.setupEvents();
+
+      // Setup debounced save
+      if (this.options.saveCallback) {
+        this.debouncedSave = debounce(() => {
+          this.save();
+        }, this.options.debounceMs);
+      }
+    }
+
+    setupEvents() {
+      // Input events for auto-save
+      this.content.addEventListener('input', () => {
+        if (this.debouncedSave) {
+          this.setStatus('Editing...');
+          this.debouncedSave();
+        }
+      });
+
+      // Handle paste
+      this.content.addEventListener('paste', (e) => {
+        const items = e.clipboardData?.items;
+        if (!items) return;
+
+        for (const item of items) {
+          if (item.type.startsWith('image/') || item.kind === 'file') {
+            e.preventDefault();
+            const file = item.getAsFile();
+            if (file) this.uploadFile(file);
+            return;
+          }
+        }
+      });
+
+      // Handle drop
+      this.content.addEventListener('dragover', (e) => {
+        e.preventDefault();
+        this.showDropOverlay();
+      });
+
+      this.content.addEventListener('dragleave', (e) => {
+        e.preventDefault();
+        this.hideDropOverlay();
+      });
+
+      this.content.addEventListener('drop', (e) => {
+        e.preventDefault();
+        this.hideDropOverlay();
+
+        const files = e.dataTransfer?.files;
+        if (files && files.length > 0) {
+          for (const file of files) {
+            this.uploadFile(file);
+          }
+        }
+      });
+
+      // Handle /upload command
+      this.content.addEventListener('keydown', (e) => {
+        if (e.key === 'Enter') {
+          const selection = window.getSelection();
+          const node = selection.anchorNode;
+          if (node && node.textContent) {
+            const text = node.textContent;
+            if (text.trim() === '/upload') {
+              e.preventDefault();
+              node.textContent = '';
+              this.triggerFileUpload();
+            }
+          }
+        }
+      });
+
+      // File input change
+      this.fileInput.addEventListener('change', (e) => {
+        const files = e.target.files;
+        if (files && files.length > 0) {
+          for (const file of files) {
+            this.uploadFile(file);
+          }
+        }
+        this.fileInput.value = '';
+      });
+
+      // Handle keyboard shortcuts
+      this.content.addEventListener('keydown', (e) => {
+        if (e.ctrlKey || e.metaKey) {
+          switch (e.key.toLowerCase()) {
+            case 'b':
+              e.preventDefault();
+              this.execCommand('bold');
+              break;
+            case 'i':
+              e.preventDefault();
+              this.execCommand('italic');
+              break;
+            case 's':
+              e.preventDefault();
+              this.save();
+              break;
+          }
+        }
+      });
+
+      // Handle image clicks for resizing
+      this.content.addEventListener('click', (e) => {
+        if (e.target.tagName === 'IMG') {
+          e.preventDefault();
+          this.selectImage(e.target);
+        } else if (!e.target.closest('.rich-editor-resize-wrapper')) {
+          this.deselectImage();
+        }
+      });
+
+      // Deselect image when clicking outside
+      document.addEventListener('click', (e) => {
+        if (!this.content.contains(e.target)) {
+          this.deselectImage();
+        }
+      });
+    }
+
+    showDropOverlay() {
+      if (this.dropOverlay) return;
+
+      this.dropOverlay = document.createElement('div');
+      this.dropOverlay.className = 'rich-editor-upload-overlay';
+      this.dropOverlay.textContent = 'Drop files here to upload';
+      this.wrapper.appendChild(this.dropOverlay);
+    }
+
+    hideDropOverlay() {
+      if (this.dropOverlay) {
+        this.dropOverlay.remove();
+        this.dropOverlay = null;
+      }
+    }
+
+    triggerFileUpload() {
+      this.fileInput.click();
+    }
+
+    readOnly() {
+      this.state.readOnly = !this.state.readOnly;
+
+      if (this.state.readOnly)
+      {
+        this.content.contentEditable = false;
+        this.setStatus('Read-only mode');
+        const buttons = this.toolbar.querySelectorAll('.rich-editor-btn');
+        buttons.forEach(btn => {
+          if (btn.dataset.cmd !== 'readonly') {
+            btn.disabled = true;
+          }
+        });
+      }
+      else
+      {
+        // Enable editing
+        this.content.contentEditable = true;
+        this.content.style.backgroundColor = '';
+        this.setStatus('Ready');
+
+        // Enable toolbar buttons
+        const buttons = this.toolbar.querySelectorAll('.rich-editor-btn');
+        buttons.forEach(btn => {
+          btn.disabled = false;
+        });
+      }
+    }
+
+    async uploadFile(file) {
+      if (!this.options.uploadCallback) {
+        console.warn('No upload callback configured');
+        return;
+      }
+
+      // Insert loading placeholder
+      const placeholder = document.createElement('div');
+      placeholder.className = 'upload-placeholder loading';
+      placeholder.textContent = file.type.startsWith('image/')
+        ? `Processing ${file.name}... ⏳`
+        : `Uploading ${file.name}...`;
+
+      this.insertAtCursor(placeholder);
+
+      this.setStatus(`Uploading ${file.name}...`);
+
+      try {
+        const result = await this.options.uploadCallback(file);
+
+        if (result && result.url) {
+          // Replace placeholder with actual content
+          if (file.type.startsWith('image/')) {
+            const img = document.createElement('img');
+            img.src = result.url;
+            img.alt = file.name;
+
+            // Store original dimensions when image loads
+            img.onload = () => {
+              img.dataset.originalWidth = img.naturalWidth;
+              img.dataset.originalHeight = img.naturalHeight;
+            };
+
+            placeholder.replaceWith(img);
+          } else {
+            const link = document.createElement('a');
+            link.href = result.url;
+            link.textContent = file.name;
+            link.target = '_blank';
+            placeholder.replaceWith(link);
+          }
+
+          this.setStatus(`Uploaded ${file.name}`);
+          if (this.debouncedSave) this.debouncedSave();
+        } else {
+          placeholder.textContent = `Failed to upload ${file.name}`;
+          placeholder.className = 'upload-placeholder';
+          this.setStatus('Upload failed');
+        }
+      } catch (error) {
+        console.error('Upload error:', error);
+        placeholder.textContent = `Error: ${error.message}`;
+        placeholder.className = 'upload-placeholder';
+        this.setStatus('Upload error');
+      }
+    }
+
+    insertAtCursor(element) {
+      const selection = window.getSelection();
+      if (selection.rangeCount > 0) {
+        const range = selection.getRangeAt(0);
+        range.deleteContents();
+        range.insertNode(element);
+        range.setStartAfter(element);
+        range.collapse(true);
+        selection.removeAllRanges();
+        selection.addRange(range);
+      } else {
+        this.content.appendChild(element);
+      }
+      this.content.focus();
+    }
+
+    execCommand(cmd) {
+      this.content.focus();
+
+      switch (cmd) {
+        case 'h1':
+        case 'h2':
+        case 'h3':
+        case 'h4':
+        case 'h5':
+        case 'h6':
+          document.execCommand('formatBlock', false, cmd);
+          break;
+        case 'p':
+          document.execCommand('formatBlock', false, 'p');
+          break;
+        case 'ul':
+          document.execCommand('insertUnorderedList', false, null);
+          break;
+        case 'ol':
+          document.execCommand('insertOrderedList', false, null);
+          break;
+        case 'bold':
+          document.execCommand('bold', false, null);
+          break;
+        case 'italic':
+          document.execCommand('italic', false, null);
+          break;
+        case 'link':
+          this.insertLink();
+          break;
+        case 'notelink':
+          this.insertNoteLink();
+          break;
+        case 'upload':
+          this.triggerFileUpload();
+          break;
+        case 'readonly':
+          this.readOnly();
+          break;
+      }
+
+      if (this.debouncedSave) this.debouncedSave();
+    }
+
+    insertLink() {
+      const url = prompt('Enter URL:');
+      if (!url) return;
+
+      const selection = window.getSelection();
+      const selectedText = selection.toString() || url;
+
+      const link = document.createElement('a');
+      link.href = url;
+      link.textContent = selectedText;
+      link.target = '_blank';
+
+      if (selection.rangeCount > 0) {
+        const range = selection.getRangeAt(0);
+        range.deleteContents();
+        range.insertNode(link);
+        range.setStartAfter(link);
+        range.collapse(true);
+        selection.removeAllRanges();
+        selection.addRange(range);
+      }
+    }
+
+    insertNoteLink() {
+      const noteId = prompt('Enter note ID (e.g., my-ideas):');
+      if (!noteId) return;
+
+      const sanitizedId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
+      const selection = window.getSelection();
+      const selectedText = selection.toString() || sanitizedId;
+
+      const link = document.createElement('a');
+      link.href = '/notes/' + encodeURIComponent(sanitizedId);
+      link.textContent = selectedText;
+      link.className = 'note-link';
+
+      if (selection.rangeCount > 0) {
+        const range = selection.getRangeAt(0);
+        range.deleteContents();
+        range.insertNode(link);
+        range.setStartAfter(link);
+        range.collapse(true);
+        selection.removeAllRanges();
+        selection.addRange(range);
+      }
+    }
+
+    setStatus(text) {
+      this.status.textContent = text;
+    }
+
+    async save() {
+      if (!this.options.saveCallback) return;
+
+      this.setStatus('Saving...');
+
+      try {
+        await this.options.saveCallback(this.getContent());
+        this.setStatus('Saved');
+      } catch (error) {
+        console.error('Save error:', error);
+        this.setStatus('Save failed');
+      }
+    }
+
+    getContent() {
+      return this.content.innerHTML;
+    }
+
+    setContent(html) {
+      this.content.innerHTML = html;
+    }
+
+    getText() {
+      return this.content.textContent;
+    }
+
+    clear() {
+      this.content.innerHTML = '';
+    }
+
+    focus() {
+      this.content.focus();
+    }
+
+    selectImage(img) {
+      if (this.state.readOnly) return;
+
+      // Deselect previous image
+      this.deselectImage();
+
+      this.state.selectedImage = img;
+      img.classList.add('selected');
+
+      // Wrap image in resize container if not already wrapped
+      if (!img.parentElement.classList.contains('rich-editor-resize-wrapper')) {
+        const wrapper = document.createElement('div');
+        wrapper.className = 'rich-editor-resize-wrapper';
+        wrapper.contentEditable = false;
+
+        // Store original dimensions if not set
+        if (!img.dataset.originalWidth) {
+          img.dataset.originalWidth = img.naturalWidth;
+          img.dataset.originalHeight = img.naturalHeight;
+        }
+
+        img.parentNode.insertBefore(wrapper, img);
+        wrapper.appendChild(img);
+      }
+
+      const wrapper = img.parentElement;
+
+      // Add resize handles
+      const corners = ['nw', 'ne', 'sw', 'se'];
+      corners.forEach(corner => {
+        const handle = document.createElement('div');
+        handle.className = `rich-editor-resize-handle ${corner}`;
+        handle.dataset.corner = corner;
+
+        // Mouse events
+        handle.addEventListener('mousedown', (e) => {
+          e.preventDefault();
+          e.stopPropagation();
+          this.startResize(e, img, corner);
+        });
+
+        // Touch events for mobile
+        handle.addEventListener('touchstart', (e) => {
+          e.preventDefault();
+          e.stopPropagation();
+          const touch = e.touches[0];
+          this.startResize(touch, img, corner, true);
+        });
+
+        wrapper.appendChild(handle);
+      });
+
+      // Add size input
+      const sizeInput = document.createElement('div');
+      sizeInput.className = 'rich-editor-size-input';
+      sizeInput.innerHTML = `
+        <label>Width:</label>
+        <input type="number" class="width-input" value="${img.width}" min="50" max="2000">
+        <label>px</label>
+      `;
+
+      const widthInput = sizeInput.querySelector('.width-input');
+      widthInput.addEventListener('input', (e) => {
+        const newWidth = parseInt(e.target.value);
+        if (newWidth && newWidth > 0) {
+          this.resizeImage(img, newWidth);
+        }
+      });
+
+      wrapper.appendChild(sizeInput);
+    }
+
+    deselectImage() {
+      if (!this.state.selectedImage) return;
+
+      const img = this.state.selectedImage;
+      img.classList.remove('selected');
+
+      const wrapper = img.parentElement;
+      if (wrapper && wrapper.classList.contains('rich-editor-resize-wrapper')) {
+        // Remove resize handles and size input
+        wrapper.querySelectorAll('.rich-editor-resize-handle, .rich-editor-size-input').forEach(el => el.remove());
+
+        // Unwrap image
+        wrapper.parentNode.insertBefore(img, wrapper);
+        wrapper.remove();
+      }
+
+      this.state.selectedImage = null;
+    }
+
+    startResize(e, img, corner, isTouch = false) {
+      this.state.resizing = true;
+
+      const startX = e.clientX;
+      const startY = e.clientY;
+      const startWidth = img.width;
+      const startHeight = img.height;
+      const aspectRatio = startWidth / startHeight;
+
+      const onMove = (e) => {
+        if (!this.state.resizing) return;
+
+        const clientX = isTouch ? e.touches[0].clientX : e.clientX;
+        const clientY = isTouch ? e.touches[0].clientY : e.clientY;
+
+        let deltaX = clientX - startX;
+        let deltaY = clientY - startY;
+
+        // Adjust delta based on corner
+        if (corner === 'nw' || corner === 'sw') {
+          deltaX = -deltaX;
+        }
+        if (corner === 'nw' || corner === 'ne') {
+          deltaY = -deltaY;
+        }
+
+        // Use the larger delta to maintain aspect ratio
+        const delta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY;
+        const newWidth = Math.max(50, startWidth + delta);
+
+        this.resizeImage(img, newWidth, aspectRatio);
+      };
+
+      const onEnd = () => {
+        this.state.resizing = false;
+
+        if (isTouch) {
+          document.removeEventListener('touchmove', onMove);
+          document.removeEventListener('touchend', onEnd);
+        } else {
+          document.removeEventListener('mousemove', onMove);
+          document.removeEventListener('mouseup', onEnd);
+        }
+
+        // Update width input
+        const wrapper = img.parentElement;
+        const widthInput = wrapper.querySelector('.width-input');
+        if (widthInput) {
+          widthInput.value = img.width;
+        }
+
+        if (this.debouncedSave) this.debouncedSave();
+      };
+
+      if (isTouch) {
+        document.addEventListener('touchmove', onMove, { passive: false });
+        document.addEventListener('touchend', onEnd);
+      } else {
+        document.addEventListener('mousemove', onMove);
+        document.addEventListener('mouseup', onEnd);
+      }
+    }
+
+    resizeImage(img, newWidth, aspectRatio = null) {
+      if (!aspectRatio) {
+        const originalWidth = parseInt(img.dataset.originalWidth) || img.naturalWidth;
+        const originalHeight = parseInt(img.dataset.originalHeight) || img.naturalHeight;
+        aspectRatio = originalWidth / originalHeight;
+      }
+
+      const newHeight = newWidth / aspectRatio;
+
+      img.width = newWidth;
+      img.height = newHeight;
+      img.style.width = newWidth + 'px';
+      img.style.height = newHeight + 'px';
+    }
+  }
+
+  return {
+    init: function(elementId, options) {
+      return new Editor(elementId, options);
+    }
+  };
+})();
+
+// Export for module systems
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = RichEditor;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/s3/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,56 @@
+load("@rules_cc//cc:cc_library.bzl", "cc_library")
+
+# File group
+filegroup(
+  name = "s3_hdrs",
+  srcs = ["s3_uploader.h"],
+  visibility = ["//visibility:public"],
+)
+
+# Main S3 uploader library (platform-aware)
+alias(
+  name = "s3",
+  actual = select({
+    "//config:macos":  ":s3_macos",
+    "//config:linux":  ":s3_linux",
+    "//conditions:default": ":s3_linux",
+  }),
+  visibility = ["//visibility:public"],
+)
+
+cc_library(
+  name = "s3_macos",
+  srcs = ["s3_uploader.c"],
+  hdrs = [":s3_hdrs"],
+  deps = [
+    "//dowa:dowa",
+    "//seobeo:seobeo",
+    "@openssl//:ssl",
+  ],
+  target_compatible_with = [
+    "@platforms//os:osx",
+  ],
+  visibility = ["//visibility:public"],
+)
+
+cc_library(
+  name = "s3_linux",
+  srcs = ["s3_uploader.c"],
+  hdrs = [":s3_hdrs"],
+  deps = [
+    "//dowa:dowa",
+    "//seobeo:seobeo",
+    "@openssl//:ssl",
+  ],
+  target_compatible_with = [
+    "@platforms//os:linux",
+  ],
+  visibility = ["//visibility:public"],
+)
+
+# Alias for convenience
+alias(
+  name = "s3_uploader",
+  actual = ":s3",
+  visibility = ["//visibility:public"],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/s3/s3_uploader.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,787 @@
+#include "s3_uploader.h"
+#include "seobeo/seobeo.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <ctype.h>
+
+#include <openssl/hmac.h>
+#include <openssl/sha.h>
+
+#define S3_ARENA_SIZE       (10 * ONE_MEGA_BYTE)
+#define S3_RESULT_ARENA_SIZE (64 * 1024)
+#define S3_SERVICE_NAME     "s3"
+#define S3_AWS4_REQUEST     "aws4_request"
+#define S3_ALGORITHM        "AWS4-HMAC-SHA256"
+
+// --- Internal Structures --- //
+
+typedef struct {
+  char date[9];       // YYYYMMDD
+  char datetime[17];  // YYYYMMDDTHHMMSSZ
+} S3_Timestamp;
+
+// --- Forward Declarations --- //
+
+static void     s3__get_timestamp(S3_Timestamp *p_ts);
+static void     s3__sha256_hex(const uint8 *data, size_t len, char *out);
+static void     s3__hmac_sha256(const uint8 *key, size_t key_len,
+                                const uint8 *data, size_t data_len,
+                                uint8 *out);
+static void     s3__hex_encode(const uint8 *data, size_t len, char *out);
+static char    *s3__uri_encode(const char *str, Dowa_Arena *p_arena);
+static char    *s3__build_canonical_request(const char *method,
+                                            const char *uri,
+                                            const char *query,
+                                            const char *headers,
+                                            const char *signed_headers,
+                                            const char *payload_hash,
+                                            Dowa_Arena *p_arena);
+static char    *s3__build_string_to_sign(const char *datetime,
+                                         const char *date,
+                                         const char *region,
+                                         const char *canonical_request,
+                                         Dowa_Arena *p_arena);
+static void     s3__calculate_signing_key(const char *secret_key,
+                                          const char *date,
+                                          const char *region,
+                                          uint8 *out);
+static char    *s3__build_authorization_header(const char *access_key,
+                                               const char *date,
+                                               const char *region,
+                                               const char *signed_headers,
+                                               const uint8 *signing_key,
+                                               const char *string_to_sign,
+                                               Dowa_Arena *p_arena);
+static uint8   *s3__load_file(const char *path, size_t *p_size);
+
+// --- Public API Implementation --- //
+
+S3_Result S3_Upload_File(const S3_Config *p_config,
+                         const char *local_path,
+                         const char *s3_key)
+{
+  const char *content_type = S3_Guess_Content_Type(local_path);
+  return S3_Upload_File_With_Content_Type(p_config, local_path, s3_key, content_type);
+}
+
+S3_Result S3_Upload_File_With_Content_Type(const S3_Config *p_config,
+                                            const char *local_path,
+                                            const char *s3_key,
+                                            const char *content_type)
+{
+  S3_Result result = {0};
+  result.p_arena = Dowa_Arena_Create(S3_RESULT_ARENA_SIZE);
+
+  size_t file_size = 0;
+  uint8 *file_data = s3__load_file(local_path, &file_size);
+  if (!file_data)
+  {
+    result.success = FALSE;
+    result.status_code = 0;
+    result.error_message = Dowa_String_Copy_Arena("Failed to read file", result.p_arena);
+    return result;
+  }
+
+  result = S3_Upload_Data(p_config, file_data, file_size, s3_key, content_type);
+  free(file_data);
+
+  return result;
+}
+
+S3_Result S3_Upload_Data(const S3_Config *p_config,
+                         const uint8 *data,
+                         size_t data_length,
+                         const char *s3_key,
+                         const char *content_type)
+{
+  S3_Result result = {0};
+  result.p_arena = Dowa_Arena_Create(S3_RESULT_ARENA_SIZE);
+
+  if (!p_config || !data || !s3_key)
+  {
+    result.success = FALSE;
+    result.error_message = Dowa_String_Copy_Arena("Invalid parameters", result.p_arena);
+    return result;
+  }
+
+  Dowa_Arena *p_arena = Dowa_Arena_Create(S3_ARENA_SIZE);
+
+  // Get timestamp
+  S3_Timestamp ts;
+  s3__get_timestamp(&ts);
+
+  // Calculate payload hash
+  char payload_hash[65];
+  s3__sha256_hex(data, data_length, payload_hash);
+
+  // Build host
+  char *host;
+  if (p_config->endpoint)
+  {
+    host = Dowa_String_Copy_Arena((char *)p_config->endpoint, p_arena);
+  }
+  else if (p_config->use_path_style)
+  {
+    size_t host_len = strlen("s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
+    host = Dowa_Arena_Allocate(p_arena, host_len);
+    snprintf(host, host_len, "s3.%s.amazonaws.com", p_config->region);
+  }
+  else
+  {
+    size_t host_len = strlen(p_config->bucket) + strlen(".s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
+    host = Dowa_Arena_Allocate(p_arena, host_len);
+    snprintf(host, host_len, "%s.s3.%s.amazonaws.com", p_config->bucket, p_config->region);
+  }
+
+  // Build URI path
+  char *uri_path;
+  char *encoded_key = s3__uri_encode(s3_key, p_arena);
+  if (p_config->use_path_style)
+  {
+    size_t uri_len = strlen("/") + strlen(p_config->bucket) + strlen("/") + strlen(encoded_key) + 1;
+    uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
+    snprintf(uri_path, uri_len, "/%s/%s", p_config->bucket, encoded_key);
+  }
+  else
+  {
+    size_t uri_len = strlen("/") + strlen(encoded_key) + 1;
+    uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
+    snprintf(uri_path, uri_len, "/%s", encoded_key);
+  }
+
+  // Build canonical headers (must be sorted alphabetically)
+  // Note: Content-Length is NOT signed for S3 PUT requests (it's sent but not in signature)
+  size_t headers_len = 512 + strlen(host) + strlen(content_type);
+  char *canonical_headers = Dowa_Arena_Allocate(p_arena, headers_len);
+  snprintf(canonical_headers, headers_len,
+           "content-type:%s\n"
+           "host:%s\n"
+           "x-amz-content-sha256:%s\n"
+           "x-amz-date:%s\n",
+           content_type,
+           host,
+           payload_hash,
+           ts.datetime);
+
+  const char *signed_headers = "content-type;host;x-amz-content-sha256;x-amz-date";
+
+  // Build canonical request
+  char *canonical_request = s3__build_canonical_request("PUT",
+                                                         uri_path,
+                                                         "",  // No query string
+                                                         canonical_headers,
+                                                         signed_headers,
+                                                         payload_hash,
+                                                         p_arena);
+
+  // Build string to sign
+  char *string_to_sign = s3__build_string_to_sign(ts.datetime,
+                                                   ts.date,
+                                                   p_config->region,
+                                                   canonical_request,
+                                                   p_arena);
+
+  // Calculate signing key
+  uint8 signing_key[32];
+  s3__calculate_signing_key(p_config->secret_access_key,
+                            ts.date,
+                            p_config->region,
+                            signing_key);
+
+  // Build authorization header
+  char *auth_header = s3__build_authorization_header(p_config->access_key_id,
+                                                      ts.date,
+                                                      p_config->region,
+                                                      signed_headers,
+                                                      signing_key,
+                                                      string_to_sign,
+                                                      p_arena);
+
+  // Build URL
+  size_t url_len = strlen("https://") + strlen(host) + strlen(uri_path) + 1;
+  char *url = Dowa_Arena_Allocate(p_arena, url_len);
+  snprintf(url, url_len, "https://%s%s", host, uri_path);
+
+  // Execute request using seobeo
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(url);
+  Seobeo_Client_Request_Set_Method(p_req, "PUT");
+  Seobeo_Client_Request_Set_Body(p_req, (const char *)data, data_length);
+  Seobeo_Client_Request_Add_Header_Map(p_req, "Connection", "keep-alive");
+  Seobeo_Client_Request_Add_Header_Map(p_req, "Content-Type", content_type);
+  Seobeo_Client_Request_Add_Header_Map(p_req, "x-amz-date", ts.datetime);
+  Seobeo_Client_Request_Add_Header_Map(p_req, "x-amz-content-sha256", payload_hash);
+  Seobeo_Client_Request_Add_Header_Map(p_req, "Authorization", auth_header);
+
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] Uploading %zu bytes to %s\n", data_length, url);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] Content-Type: %s\n", content_type);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] x-amz-date: %s\n", ts.datetime);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] x-amz-content-sha256: %s\n", payload_hash);
+  Seobeo_Log(SEOBEO_DEBUG, "[S3] Authorization: %.80s...\n", auth_header);
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+
+  if (p_resp)
+  {
+    result.status_code = p_resp->status_code;
+    if (p_resp->status_code >= 200 && p_resp->status_code < 300)
+    {
+      result.success = TRUE;
+
+      // Extract ETag from response headers
+      if (p_resp->headers)
+      {
+        char *etag = Dowa_HashMap_Get(p_resp->headers, "etag");
+        if (!etag) etag = Dowa_HashMap_Get(p_resp->headers, "ETag");
+        if (etag)
+        {
+          result.etag = Dowa_String_Copy_Arena(etag, result.p_arena);
+        }
+      }
+    }
+    else
+    {
+      result.success = FALSE;
+      if (p_resp->body && p_resp->body_length > 0)
+      {
+        result.error_message = Dowa_String_Copy_Arena(p_resp->body, result.p_arena);
+      }
+      else
+      {
+        result.error_message = Dowa_String_Copy_Arena("Upload failed", result.p_arena);
+      }
+    }
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    result.success = FALSE;
+    result.error_message = Dowa_String_Copy_Arena("Failed to execute request", result.p_arena);
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+  Dowa_Arena_Free(p_arena);
+
+  return result;
+}
+
+void S3_Result_Destroy(S3_Result *p_result)
+{
+  if (p_result && p_result->p_arena)
+  {
+    Dowa_Arena_Free(p_result->p_arena);
+    p_result->p_arena = NULL;
+    p_result->error_message = NULL;
+    p_result->etag = NULL;
+  }
+}
+
+// --- Presigned URL Implementation --- //
+
+static char *s3__uri_encode_strict(const char *str, Dowa_Arena *p_arena)
+{
+  // Stricter encoding for query string values (no forward slash allowed)
+  if (!str) return NULL;
+
+  size_t len = strlen(str);
+  size_t max_len = len * 3 + 1;
+  char *out = Dowa_Arena_Allocate(p_arena, max_len);
+  char *p = out;
+
+  for (size_t i = 0; i < len; i++)
+  {
+    char c = str[i];
+    if ((c >= 'A' && c <= 'Z') ||
+        (c >= 'a' && c <= 'z') ||
+        (c >= '0' && c <= '9') ||
+        c == '-' || c == '_' || c == '.' || c == '~')
+    {
+      *p++ = c;
+    }
+    else
+    {
+      sprintf(p, "%%%02X", (unsigned char)c);
+      p += 3;
+    }
+  }
+  *p = '\0';
+  return out;
+}
+
+static S3_Presigned_URL s3__presign_url(const S3_Config *p_config,
+                                         const char *s3_key,
+                                         const char *method,
+                                         const char *content_type,
+                                         int32 expires_seconds)
+{
+  S3_Presigned_URL result = {0};
+  result.p_arena = Dowa_Arena_Create(S3_RESULT_ARENA_SIZE);
+
+  if (!p_config || !s3_key)
+  {
+    result.success = FALSE;
+    result.error_message = Dowa_String_Copy_Arena("Invalid parameters", result.p_arena);
+    return result;
+  }
+
+  Dowa_Arena *p_arena = Dowa_Arena_Create(S3_ARENA_SIZE);
+
+  // Get timestamp
+  S3_Timestamp ts;
+  s3__get_timestamp(&ts);
+
+  // Build host
+  char *host;
+  if (p_config->endpoint)
+  {
+    host = Dowa_String_Copy_Arena((char *)p_config->endpoint, p_arena);
+  }
+  else if (p_config->use_path_style)
+  {
+    size_t host_len = strlen("s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
+    host = Dowa_Arena_Allocate(p_arena, host_len);
+    snprintf(host, host_len, "s3.%s.amazonaws.com", p_config->region);
+  }
+  else
+  {
+    size_t host_len = strlen(p_config->bucket) + strlen(".s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
+    host = Dowa_Arena_Allocate(p_arena, host_len);
+    snprintf(host, host_len, "%s.s3.%s.amazonaws.com", p_config->bucket, p_config->region);
+  }
+
+  // Build URI path
+  char *uri_path;
+  char *encoded_key = s3__uri_encode(s3_key, p_arena);
+  if (p_config->use_path_style)
+  {
+    size_t uri_len = strlen("/") + strlen(p_config->bucket) + strlen("/") + strlen(encoded_key) + 1;
+    uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
+    snprintf(uri_path, uri_len, "/%s/%s", p_config->bucket, encoded_key);
+  }
+  else
+  {
+    size_t uri_len = strlen("/") + strlen(encoded_key) + 1;
+    uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
+    snprintf(uri_path, uri_len, "/%s", encoded_key);
+  }
+
+  // Build credential scope
+  size_t scope_len = strlen(p_config->access_key_id) + strlen(ts.date) + strlen(p_config->region) +
+                     strlen(S3_SERVICE_NAME) + strlen(S3_AWS4_REQUEST) + 10;
+  char *credential = Dowa_Arena_Allocate(p_arena, scope_len);
+  snprintf(credential, scope_len, "%s/%s/%s/%s/%s",
+           p_config->access_key_id, ts.date, p_config->region, S3_SERVICE_NAME, S3_AWS4_REQUEST);
+
+  char *encoded_credential = s3__uri_encode_strict(credential, p_arena);
+
+  // Build signed headers (host is required, content-type for PUT)
+  const char *signed_headers;
+  if (content_type && strcmp(method, "PUT") == 0)
+  {
+    signed_headers = "content-type;host";
+  }
+  else
+  {
+    signed_headers = "host";
+  }
+
+  // Build canonical query string (must be sorted alphabetically)
+  char expires_str[16];
+  snprintf(expires_str, sizeof(expires_str), "%d", expires_seconds);
+
+  // URL-encode signed headers (semicolon becomes %3B)
+  char *encoded_signed_headers = s3__uri_encode_strict(signed_headers, p_arena);
+
+  size_t query_len = 1024 + strlen(encoded_credential);
+  char *canonical_query = Dowa_Arena_Allocate(p_arena, query_len);
+
+  snprintf(canonical_query, query_len,
+           "X-Amz-Algorithm=%s"
+           "&X-Amz-Credential=%s"
+           "&X-Amz-Date=%s"
+           "&X-Amz-Expires=%s"
+           "&X-Amz-SignedHeaders=%s",
+           S3_ALGORITHM,
+           encoded_credential,
+           ts.datetime,
+           expires_str,
+           encoded_signed_headers);
+
+  // Build canonical headers
+  size_t headers_len = 256 + strlen(host) + (content_type ? strlen(content_type) : 0);
+  char *canonical_headers = Dowa_Arena_Allocate(p_arena, headers_len);
+
+  if (content_type && strcmp(method, "PUT") == 0)
+  {
+    snprintf(canonical_headers, headers_len,
+             "content-type:%s\nhost:%s\n",
+             content_type, host);
+  }
+  else
+  {
+    snprintf(canonical_headers, headers_len, "host:%s\n", host);
+  }
+
+  // For presigned URLs, payload is UNSIGNED-PAYLOAD
+  const char *payload_hash = "UNSIGNED-PAYLOAD";
+
+  // Build canonical request
+  char *canonical_request = s3__build_canonical_request(method,
+                                                         uri_path,
+                                                         canonical_query,
+                                                         canonical_headers,
+                                                         signed_headers,
+                                                         payload_hash,
+                                                         p_arena);
+
+  // Build string to sign
+  char *string_to_sign = s3__build_string_to_sign(ts.datetime,
+                                                   ts.date,
+                                                   p_config->region,
+                                                   canonical_request,
+                                                   p_arena);
+
+  // Calculate signing key
+  uint8 signing_key[32];
+  s3__calculate_signing_key(p_config->secret_access_key,
+                            ts.date,
+                            p_config->region,
+                            signing_key);
+
+  // Calculate signature
+  uint8 signature[32];
+  s3__hmac_sha256(signing_key, 32,
+                  (const uint8 *)string_to_sign, strlen(string_to_sign),
+                  signature);
+
+  char signature_hex[65];
+  s3__hex_encode(signature, 32, signature_hex);
+
+  // Build final presigned URL
+  size_t url_len = strlen("https://") + strlen(host) + strlen(uri_path) + 1 +
+                   strlen(canonical_query) + strlen("&X-Amz-Signature=") + 64 + 1;
+  char *url = Dowa_Arena_Allocate(result.p_arena, url_len);
+  snprintf(url, url_len, "https://%s%s?%s&X-Amz-Signature=%s",
+           host, uri_path, canonical_query, signature_hex);
+
+  result.success = TRUE;
+  result.url = url;
+
+  Dowa_Arena_Free(p_arena);
+  return result;
+}
+
+S3_Presigned_URL S3_Presign_Put(const S3_Config *p_config,
+                                 const char *s3_key,
+                                 const char *content_type,
+                                 int32 expires_seconds)
+{
+  return s3__presign_url(p_config, s3_key, "PUT", content_type, expires_seconds);
+}
+
+S3_Presigned_URL S3_Presign_Get(const S3_Config *p_config,
+                                 const char *s3_key,
+                                 int32 expires_seconds)
+{
+  return s3__presign_url(p_config, s3_key, "GET", NULL, expires_seconds);
+}
+
+void S3_Presigned_URL_Destroy(S3_Presigned_URL *p_url)
+{
+  if (p_url && p_url->p_arena)
+  {
+    Dowa_Arena_Free(p_url->p_arena);
+    p_url->p_arena = NULL;
+    p_url->url = NULL;
+    p_url->error_message = NULL;
+  }
+}
+
+const char *S3_Guess_Content_Type(const char *filename)
+{
+  if (!filename) return "application/octet-stream";
+
+  const char *dot = strrchr(filename, '.');
+  if (!dot) return "application/octet-stream";
+
+  dot++;  // Skip the dot
+
+  // Common content types
+  if (strcasecmp(dot, "html") == 0 || strcasecmp(dot, "htm") == 0)
+    return "text/html";
+  if (strcasecmp(dot, "css") == 0)
+    return "text/css";
+  if (strcasecmp(dot, "js") == 0)
+    return "application/javascript";
+  if (strcasecmp(dot, "json") == 0)
+    return "application/json";
+  if (strcasecmp(dot, "xml") == 0)
+    return "application/xml";
+  if (strcasecmp(dot, "txt") == 0)
+    return "text/plain";
+  if (strcasecmp(dot, "csv") == 0)
+    return "text/csv";
+
+  // Images
+  if (strcasecmp(dot, "png") == 0)
+    return "image/png";
+  if (strcasecmp(dot, "jpg") == 0 || strcasecmp(dot, "jpeg") == 0)
+    return "image/jpeg";
+  if (strcasecmp(dot, "gif") == 0)
+    return "image/gif";
+  if (strcasecmp(dot, "svg") == 0)
+    return "image/svg+xml";
+  if (strcasecmp(dot, "webp") == 0)
+    return "image/webp";
+  if (strcasecmp(dot, "ico") == 0)
+    return "image/x-icon";
+
+  // Audio/Video
+  if (strcasecmp(dot, "mp3") == 0)
+    return "audio/mpeg";
+  if (strcasecmp(dot, "mp4") == 0)
+    return "video/mp4";
+  if (strcasecmp(dot, "webm") == 0)
+    return "video/webm";
+  if (strcasecmp(dot, "ogg") == 0)
+    return "audio/ogg";
+  if (strcasecmp(dot, "wav") == 0)
+    return "audio/wav";
+
+  // Documents
+  if (strcasecmp(dot, "pdf") == 0)
+    return "application/pdf";
+  if (strcasecmp(dot, "zip") == 0)
+    return "application/zip";
+  if (strcasecmp(dot, "gz") == 0 || strcasecmp(dot, "gzip") == 0)
+    return "application/gzip";
+  if (strcasecmp(dot, "tar") == 0)
+    return "application/x-tar";
+
+  // Fonts
+  if (strcasecmp(dot, "woff") == 0)
+    return "font/woff";
+  if (strcasecmp(dot, "woff2") == 0)
+    return "font/woff2";
+  if (strcasecmp(dot, "ttf") == 0)
+    return "font/ttf";
+  if (strcasecmp(dot, "otf") == 0)
+    return "font/otf";
+
+  return "application/octet-stream";
+}
+
+// --- Internal Implementation --- //
+
+static void s3__get_timestamp(S3_Timestamp *p_ts)
+{
+  time_t now = time(NULL);
+  struct tm *utc = gmtime(&now);
+
+  strftime(p_ts->date, sizeof(p_ts->date), "%Y%m%d", utc);
+  strftime(p_ts->datetime, sizeof(p_ts->datetime), "%Y%m%dT%H%M%SZ", utc);
+}
+
+static void s3__hex_encode(const uint8 *data, size_t len, char *out)
+{
+  static const char hex[] = "0123456789abcdef";
+  for (size_t i = 0; i < len; i++)
+  {
+    out[i * 2]     = hex[(data[i] >> 4) & 0x0F];
+    out[i * 2 + 1] = hex[data[i] & 0x0F];
+  }
+  out[len * 2] = '\0';
+}
+
+static void s3__sha256_hex(const uint8 *data, size_t len, char *out)
+{
+  uint8 hash[SHA256_DIGEST_LENGTH];
+  SHA256(data, len, hash);
+  s3__hex_encode(hash, SHA256_DIGEST_LENGTH, out);
+}
+
+static void s3__hmac_sha256(const uint8 *key, size_t key_len,
+                            const uint8 *data, size_t data_len,
+                            uint8 *out)
+{
+  unsigned int out_len = 0;
+  HMAC(EVP_sha256(), key, (int)key_len, data, data_len, out, &out_len);
+}
+
+static char *s3__uri_encode(const char *str, Dowa_Arena *p_arena)
+{
+  if (!str) return NULL;
+
+  size_t len = strlen(str);
+  // Worst case: every char becomes %XX (3 chars)
+  size_t max_len = len * 3 + 1;
+  char *out = Dowa_Arena_Allocate(p_arena, max_len);
+  char *p = out;
+
+  for (size_t i = 0; i < len; i++)
+  {
+    char c = str[i];
+    // Unreserved characters per RFC 3986
+    if ((c >= 'A' && c <= 'Z') ||
+        (c >= 'a' && c <= 'z') ||
+        (c >= '0' && c <= '9') ||
+        c == '-' || c == '_' || c == '.' || c == '~' || c == '/')
+    {
+      *p++ = c;
+    }
+    else
+    {
+      sprintf(p, "%%%02X", (unsigned char)c);
+      p += 3;
+    }
+  }
+  *p = '\0';
+  return out;
+}
+
+static char *s3__build_canonical_request(const char *method,
+                                          const char *uri,
+                                          const char *query,
+                                          const char *headers,
+                                          const char *signed_headers,
+                                          const char *payload_hash,
+                                          Dowa_Arena *p_arena)
+{
+  size_t len = strlen(method) + strlen(uri) + strlen(query) +
+               strlen(headers) + strlen(signed_headers) + strlen(payload_hash) + 10;
+  char *out = Dowa_Arena_Allocate(p_arena, len);
+  snprintf(out, len, "%s\n%s\n%s\n%s\n%s\n%s",
+           method, uri, query, headers, signed_headers, payload_hash);
+  return out;
+}
+
+static char *s3__build_string_to_sign(const char *datetime,
+                                       const char *date,
+                                       const char *region,
+                                       const char *canonical_request,
+                                       Dowa_Arena *p_arena)
+{
+  // Hash the canonical request
+  char canonical_hash[65];
+  s3__sha256_hex((const uint8 *)canonical_request, strlen(canonical_request), canonical_hash);
+
+  // Build credential scope
+  size_t scope_len = strlen(date) + strlen(region) + strlen(S3_SERVICE_NAME) + strlen(S3_AWS4_REQUEST) + 4;
+  char *scope = Dowa_Arena_Allocate(p_arena, scope_len);
+  snprintf(scope, scope_len, "%s/%s/%s/%s", date, region, S3_SERVICE_NAME, S3_AWS4_REQUEST);
+
+  // Build string to sign
+  size_t len = strlen(S3_ALGORITHM) + strlen(datetime) + strlen(scope) + strlen(canonical_hash) + 10;
+  char *out = Dowa_Arena_Allocate(p_arena, len);
+  snprintf(out, len, "%s\n%s\n%s\n%s", S3_ALGORITHM, datetime, scope, canonical_hash);
+
+  return out;
+}
+
+static void s3__calculate_signing_key(const char *secret_key,
+                                       const char *date,
+                                       const char *region,
+                                       uint8 *out)
+{
+  // AWS4 + SecretAccessKey
+  size_t key_len = 4 + strlen(secret_key) + 1;
+  char *k_secret = malloc(key_len);
+  snprintf(k_secret, key_len, "AWS4%s", secret_key);
+
+  uint8 k_date[32];
+  uint8 k_region[32];
+  uint8 k_service[32];
+
+  s3__hmac_sha256((const uint8 *)k_secret, strlen(k_secret),
+                  (const uint8 *)date, strlen(date), k_date);
+
+  s3__hmac_sha256(k_date, 32,
+                  (const uint8 *)region, strlen(region), k_region);
+
+  s3__hmac_sha256(k_region, 32,
+                  (const uint8 *)S3_SERVICE_NAME, strlen(S3_SERVICE_NAME), k_service);
+
+  s3__hmac_sha256(k_service, 32,
+                  (const uint8 *)S3_AWS4_REQUEST, strlen(S3_AWS4_REQUEST), out);
+
+  free(k_secret);
+}
+
+static char *s3__build_authorization_header(const char *access_key,
+                                             const char *date,
+                                             const char *region,
+                                             const char *signed_headers,
+                                             const uint8 *signing_key,
+                                             const char *string_to_sign,
+                                             Dowa_Arena *p_arena)
+{
+  // Calculate signature
+  uint8 signature[32];
+  s3__hmac_sha256(signing_key, 32,
+                  (const uint8 *)string_to_sign, strlen(string_to_sign),
+                  signature);
+
+  char signature_hex[65];
+  s3__hex_encode(signature, 32, signature_hex);
+
+  // Build credential
+  size_t cred_len = strlen(access_key) + strlen(date) + strlen(region) +
+                    strlen(S3_SERVICE_NAME) + strlen(S3_AWS4_REQUEST) + 10;
+  char *credential = Dowa_Arena_Allocate(p_arena, cred_len);
+  snprintf(credential, cred_len, "%s/%s/%s/%s/%s",
+           access_key, date, region, S3_SERVICE_NAME, S3_AWS4_REQUEST);
+
+  // Build full authorization header
+  size_t auth_len = strlen(S3_ALGORITHM) + strlen(credential) +
+                    strlen(signed_headers) + strlen(signature_hex) + 64;
+  char *auth = Dowa_Arena_Allocate(p_arena, auth_len);
+  snprintf(auth, auth_len,
+           "%s Credential=%s, SignedHeaders=%s, Signature=%s",
+           S3_ALGORITHM, credential, signed_headers, signature_hex);
+
+  return auth;
+}
+
+static uint8 *s3__load_file(const char *path, size_t *p_size)
+{
+  FILE *f = fopen(path, "rb");
+  if (!f)
+  {
+    *p_size = 0;
+    return NULL;
+  }
+
+  fseek(f, 0, SEEK_END);
+  long size = ftell(f);
+  fseek(f, 0, SEEK_SET);
+
+  if (size <= 0)
+  {
+    fclose(f);
+    *p_size = 0;
+    return NULL;
+  }
+
+  uint8 *data = malloc((size_t)size);
+  if (!data)
+  {
+    fclose(f);
+    *p_size = 0;
+    return NULL;
+  }
+
+  size_t read = fread(data, 1, (size_t)size, f);
+  fclose(f);
+
+  if (read != (size_t)size)
+  {
+    free(data);
+    *p_size = 0;
+    return NULL;
+  }
+
+  *p_size = (size_t)size;
+  return data;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/s3/s3_uploader.h	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,150 @@
+#ifndef S3_UPLOADER_H
+#define S3_UPLOADER_H
+
+/**
+ * S3 Uploader
+ * -----------
+ *
+ * Generic S3 bucket uploader using AWS Signature Version 4.
+ * Built on top of seobeo HTTP client.
+ *
+ * ## Examples
+ *
+ * ### 1. Upload a file from disk
+ *
+ * ```c
+ * S3_Config config = {
+ *   .access_key_id     = "AKIAIOSFODNN7EXAMPLE",
+ *   .secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
+ *   .region            = "us-east-1",
+ *   .bucket            = "my-bucket"
+ * };
+ *
+ * S3_Result result = S3_Upload_File(&config, "/path/to/local/file.txt", "uploads/file.txt");
+ * if (result.success)
+ * {
+ *   printf("Uploaded successfully!\n");
+ * }
+ * else
+ * {
+ *   printf("Error: %s\n", result.error_message);
+ * }
+ * S3_Result_Destroy(&result);
+ * ```
+ *
+ * ### 2. Upload raw data from memory
+ *
+ * ```c
+ * const char *data = "Hello, S3!";
+ * S3_Result result = S3_Upload_Data(&config, (const uint8 *)data, strlen(data),
+ *                                   "hello.txt", "text/plain");
+ * S3_Result_Destroy(&result);
+ * ```
+ *
+ * ### 3. Upload with custom content type
+ *
+ * ```c
+ * S3_Result result = S3_Upload_File_With_Content_Type(&config,
+ *                                                      "/path/to/image.png",
+ *                                                      "images/photo.png",
+ *                                                      "image/png");
+ * S3_Result_Destroy(&result);
+ * ```
+ */
+
+#include <stddef.h>
+
+#include "dowa/dowa.h"
+
+// S3 configuration
+typedef struct {
+  const char *access_key_id;      // AWS access key ID
+  const char *secret_access_key;  // AWS secret access key
+  const char *region;             // AWS region (e.g., "us-east-1")
+  const char *bucket;             // S3 bucket name
+  const char *endpoint;           // Optional custom endpoint (NULL for default AWS)
+  boolean     use_path_style;     // Use path-style URLs (bucket in path, not subdomain)
+} S3_Config;
+
+// Upload result
+typedef struct {
+  boolean     success;        // TRUE if upload succeeded
+  int32       status_code;    // HTTP status code
+  char       *error_message;  // Error message (NULL on success)
+  char       *etag;           // ETag of uploaded object (on success)
+  Dowa_Arena *p_arena;        // Internal arena (do not modify)
+} S3_Result;
+
+// --- Core API --- //
+
+/* Upload file from disk to S3. Content-Type is auto-detected. */
+extern S3_Result S3_Upload_File(const S3_Config *p_config,
+                                const char *local_path,
+                                const char *s3_key);
+
+/* Upload file with explicit content type. */
+extern S3_Result S3_Upload_File_With_Content_Type(const S3_Config *p_config,
+                                                   const char *local_path,
+                                                   const char *s3_key,
+                                                   const char *content_type);
+
+/* Upload raw data from memory to S3. */
+extern S3_Result S3_Upload_Data(const S3_Config *p_config,
+                                const uint8 *data,
+                                size_t data_length,
+                                const char *s3_key,
+                                const char *content_type);
+
+/* Free resources associated with result. */
+extern void S3_Result_Destroy(S3_Result *p_result);
+
+// --- Presigned URLs --- //
+
+// Presigned URL result
+typedef struct {
+  boolean     success;        // TRUE if generation succeeded
+  char       *url;            // The presigned URL (on success)
+  char       *error_message;  // Error message (on failure)
+  Dowa_Arena *p_arena;        // Internal arena (do not modify)
+} S3_Presigned_URL;
+
+/* Generate a presigned URL for uploading (PUT) a file.
+ *
+ * The client can use this URL to upload directly to S3 without credentials.
+ *
+ * Example:
+ *   S3_Presigned_URL url = S3_Presign_Put(&config, "uploads/file.txt", "image/png", 3600);
+ *   if (url.success) {
+ *     printf("Upload URL: %s\n", url.url);
+ *     // Client does: curl -X PUT -H "Content-Type: image/png" --data-binary @file.png "$URL"
+ *   }
+ *   S3_Presigned_URL_Destroy(&url);
+ */
+extern S3_Presigned_URL S3_Presign_Put(const S3_Config *p_config,
+                                        const char *s3_key,
+                                        const char *content_type,
+                                        int32 expires_seconds);
+
+/* Generate a presigned URL for downloading (GET) a file.
+ *
+ * Example:
+ *   S3_Presigned_URL url = S3_Presign_Get(&config, "uploads/file.txt", 3600);
+ *   if (url.success) {
+ *     printf("Download URL: %s\n", url.url);
+ *     // Client does: curl "$URL" -o file.txt
+ *   }
+ *   S3_Presigned_URL_Destroy(&url);
+ */
+extern S3_Presigned_URL S3_Presign_Get(const S3_Config *p_config,
+                                        const char *s3_key,
+                                        int32 expires_seconds);
+
+/* Free resources associated with presigned URL result. */
+extern void S3_Presigned_URL_Destroy(S3_Presigned_URL *p_url);
+
+// --- Utility Functions --- //
+
+/* Guess content type from file extension. Returns "application/octet-stream" if unknown. */
+extern const char *S3_Guess_Content_Type(const char *filename);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/s3/tests/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,23 @@
+load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
+load("@rules_cc//cc:cc_test.bzl", "cc_test")
+
+cc_test(
+  name = "s3_uploader_test",
+  srcs = ["s3_uploader_test.c"],
+  deps = [
+    "//s3:s3",
+  ],
+  data = [
+    "//:env_file",
+  ],
+  visibility = ["//visibility:public"],
+)
+
+cc_binary(
+  name = "s3_uploader_bin",
+  srcs = ["s3_uploader_test.c"],
+  deps = [
+    "//s3:s3",
+  ],
+  visibility = ["//visibility:public"],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/s3/tests/s3_uploader_test.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,192 @@
+#include "s3/s3_uploader.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+// Simple .env parser
+static char *parse_env_value(const char *env_content, const char *key)
+{
+  const char *pos = strstr(env_content, key);
+  if (!pos) return NULL;
+
+  pos += strlen(key);
+  if (*pos != '=') return NULL;
+  pos++;
+
+  const char *end = pos;
+  while (*end && *end != '\n' && *end != '\r') end++;
+
+  size_t len = end - pos;
+  char *value = malloc(len + 1);
+  memcpy(value, pos, len);
+  value[len] = '\0';
+  return value;
+}
+
+static char *load_file(const char *path)
+{
+  FILE *f = fopen(path, "r");
+  if (!f) return NULL;
+
+  fseek(f, 0, SEEK_END);
+  long size = ftell(f);
+  fseek(f, 0, SEEK_SET);
+
+  char *content = malloc(size + 1);
+  fread(content, 1, size, f);
+  content[size] = '\0';
+  fclose(f);
+  return content;
+}
+
+int main(int argc, char **argv)
+{
+  // Try to load .env file - check multiple locations for bazel runfiles
+  char *env_content = load_file(".env");
+  if (!env_content)
+  {
+    env_content = load_file("zenbu/.env");  // bazel runfiles path
+  }
+  if (!env_content)
+  {
+    env_content = load_file("../.env");
+  }
+  if (!env_content)
+  {
+    env_content = load_file("../../.env");
+  }
+
+  char *access_key = NULL;
+  char *secret_key = NULL;
+
+  if (env_content)
+  {
+    access_key = parse_env_value(env_content, "AWS_MRJUNEJUNE_ACCESS_KEY");
+    secret_key = parse_env_value(env_content, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY");
+    free(env_content);
+  }
+
+  // Fall back to environment variables
+  if (!access_key) access_key = getenv("AWS_ACCESS_KEY_ID") ? strdup(getenv("AWS_ACCESS_KEY_ID")) : NULL;
+  if (!secret_key) secret_key = getenv("AWS_SECRET_ACCESS_KEY") ? strdup(getenv("AWS_SECRET_ACCESS_KEY")) : NULL;
+
+  if (!access_key || !secret_key)
+  {
+    printf("Missing credentials. Set in .env file or environment:\n");
+    printf("  AWS_MRJUNEJUNE_ACCESS_KEY / AWS_ACCESS_KEY_ID\n");
+    printf("  AWS_MRJUNEJUNE_SECRET_ACCESS_KEY / AWS_SECRET_ACCESS_KEY\n");
+    return 1;
+  }
+
+  S3_Config config = {
+    .access_key_id     = access_key,
+    .secret_access_key = secret_key,
+    .region            = "us-west-2",
+    .bucket            = "mrjunejune",
+    .endpoint          = NULL,
+    .use_path_style    = FALSE
+  };
+
+  printf("S3 Uploader Test\n");
+  printf("================\n");
+  printf("Region: %s\n", config.region);
+  printf("Bucket: %s\n", config.bucket);
+  printf("\n");
+
+  // Test 1: Upload string data
+  printf("Test 1: Uploading string data...\n");
+  const char *test_data = "Hello from S3 uploader test!";
+  S3_Result result = S3_Upload_Data(&config,
+                                     (const uint8 *)test_data,
+                                     strlen(test_data),
+                                     "test/hello.txt",
+                                     "text/plain");
+  if (result.success)
+  {
+    printf("  SUCCESS! ETag: %s\n", result.etag ? result.etag : "(none)");
+  }
+  else
+  {
+    printf("  FAILED! Status: %d, Error: %s\n",
+           result.status_code,
+           result.error_message ? result.error_message : "(unknown)");
+  }
+  S3_Result_Destroy(&result);
+
+  // Test 2: Content type detection
+  printf("\nTest 2: Content type detection...\n");
+  printf("  .html -> %s\n", S3_Guess_Content_Type("page.html"));
+  printf("  .png  -> %s\n", S3_Guess_Content_Type("image.png"));
+  printf("  .json -> %s\n", S3_Guess_Content_Type("data.json"));
+  printf("  .xyz  -> %s\n", S3_Guess_Content_Type("unknown.xyz"));
+
+  // Test 3: Generate presigned PUT URL
+  printf("\nTest 3: Generating presigned PUT URL...\n");
+  S3_Presigned_URL put_url = S3_Presign_Put(&config,
+                                             "uploads/client-upload.png",
+                                             "image/png",
+                                             3600);  // 1 hour
+  if (put_url.success)
+  {
+    printf("  SUCCESS!\n");
+    printf("  URL: %s\n", put_url.url);
+    printf("\n  Client can upload with:\n");
+    printf("    curl -X PUT -H \"Content-Type: image/png\" --data-binary @file.png \"$URL\"\n");
+  }
+  else
+  {
+    printf("  FAILED! Error: %s\n", put_url.error_message ? put_url.error_message : "(unknown)");
+  }
+  S3_Presigned_URL_Destroy(&put_url);
+
+  // Test 4: Generate presigned GET URL
+  printf("\nTest 4: Generating presigned GET URL...\n");
+  S3_Presigned_URL get_url = S3_Presign_Get(&config,
+                                             "uploads/client-upload.png",
+                                             3600);  // 1 hour
+  if (get_url.success)
+  {
+    printf("  SUCCESS!\n");
+    printf("  URL: %s\n", get_url.url);
+    printf("\n  Client can download with:\n");
+    printf("    curl \"$URL\" -o file.png\n");
+  }
+  else
+  {
+    printf("  FAILED! Error: %s\n", get_url.error_message ? get_url.error_message : "(unknown)");
+  }
+  S3_Presigned_URL_Destroy(&get_url);
+
+  // Test 5: Upload file (if provided as argument)
+  if (argc >= 3)
+  {
+    printf("\nTest 5: Uploading file...\n");
+    printf("  Local: %s\n", argv[1]);
+    printf("  S3 Key: %s\n", argv[2]);
+
+    result = S3_Upload_File(&config, argv[1], argv[2]);
+    if (result.success)
+    {
+      printf("  SUCCESS! ETag: %s\n", result.etag ? result.etag : "(none)");
+    }
+    else
+    {
+      printf("  FAILED! Status: %d, Error: %s\n",
+             result.status_code,
+             result.error_message ? result.error_message : "(unknown)");
+    }
+    S3_Result_Destroy(&result);
+  }
+  else
+  {
+    printf("\nTest 5: Skipped (no file path provided)\n");
+    printf("  Usage: %s <local_file> <s3_key>\n", argv[0]);
+  }
+
+  printf("\nDone!\n");
+
+  free(access_key);
+  free(secret_key);
+  return 0;
+}
--- a/seobeo/s_http_client.c	Sun Jan 25 10:44:04 2026 -0800
+++ b/seobeo/s_http_client.c	Sun Aug 02 16:50:48 2026 -0700
@@ -1,5 +1,46 @@
 #include "seobeo/seobeo.h"
 #include <ctype.h>
+#include <time.h>
+
+static int64_t Seobeo_Client_Monotonic_Milliseconds(void)
+{
+  struct timespec now;
+  clock_gettime(CLOCK_MONOTONIC, &now);
+  return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
+}
+
+static boolean Seobeo_Client_Read_Timed_Out(int32 timeout_ms, int64_t last_progress_ms)
+{
+  return timeout_ms > 0 &&
+         Seobeo_Client_Monotonic_Milliseconds() - last_progress_ms >= timeout_ms;
+}
+
+static size_t Seobeo_Client_Find_Header_Length(const uint8 *buffer, size_t length)
+{
+  if (!buffer || length < 4)
+    return 0;
+  for (size_t i = 0; i + 3 < length; i++)
+  {
+    if (buffer[i] == '\r' && buffer[i + 1] == '\n' &&
+        buffer[i + 2] == '\r' && buffer[i + 3] == '\n')
+      return i + 4;
+  }
+  return 0;
+}
+
+static const char *Seobeo_Client_Header_Value(
+    Seobeo_Request_Entry *headers,
+    const char *name)
+{
+  if (!headers || !name)
+    return NULL;
+  for (size_t i = 0; i < Dowa_Array_Length(headers); i++)
+  {
+    if (headers[i].key && strcasecmp(headers[i].key, name) == 0)
+      return headers[i].value;
+  }
+  return NULL;
+}
 
 static void Seobeo_Client_Parse_Url(const char *url, char **p_host, 
     char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena)
@@ -87,6 +128,7 @@
 
   p_req->follow_redirects = FALSE;
   p_req->max_redirects = 10;
+  p_req->timeout_ms = 0;
 
   return p_req;
 }
@@ -149,6 +191,13 @@
   p_req->max_redirects = max_redirects > 0 ? max_redirects : 10;
 }
 
+void Seobeo_Client_Request_Set_Timeout_Milliseconds(Seobeo_Client_Request *p_req, int32 timeout_ms)
+{
+  if (!p_req)
+    return;
+  p_req->timeout_ms = timeout_ms > 0 ? timeout_ms : 0;
+}
+
 void Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path)
 {
   if (!p_req || !path)
@@ -223,7 +272,10 @@
   return offset;
 }
 
-static Seobeo_Client_Response *Seobeo_Client_Parse_Response(Seobeo_Handle *p_handle, const char *download_path)
+static Seobeo_Client_Response *Seobeo_Client_Parse_Response(
+    Seobeo_Handle *p_handle,
+    const char *download_path,
+    int32 timeout_ms)
 {
   Seobeo_Client_Response *p_resp = malloc(sizeof(Seobeo_Client_Response));
   if (!p_resp)
@@ -238,27 +290,49 @@
     return NULL;
   }
 
+  int64_t last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
   while (TRUE)
   {
     int r = Seobeo_Handle_Read(p_handle);
-    if (r < 0)
-      return p_resp;
     if (r == -2)
       break;
+    if (r < 0)
+    {
+      Seobeo_Client_Response_Destroy(p_resp);
+      return NULL;
+    }
+    if (r > 0)
+      last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
 
-    if (p_handle->read_buffer_len >= 4 && strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL)
+    if (Seobeo_Client_Find_Header_Length(
+            p_handle->read_buffer, p_handle->read_buffer_len) > 0)
       break;
 
     if (r == 0)
+    {
+      if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
+      {
+        Seobeo_Log(SEOBEO_ERROR, "HTTP response header timed out\n");
+        Seobeo_Client_Response_Destroy(p_resp);
+        return NULL;
+      }
+      usleep(1000);
       continue;
+    }
   }
 
-  char *buf = (char*)p_handle->read_buffer;
-  char *hdr_end = strstr(buf, "\r\n\r\n");
-  if (!hdr_end)
-    return p_resp;
+  size_t hdr_len = Seobeo_Client_Find_Header_Length(
+      p_handle->read_buffer, p_handle->read_buffer_len);
+  if (hdr_len == 0)
+  {
+    Seobeo_Client_Response_Destroy(p_resp);
+    return NULL;
+  }
 
-  size_t hdr_len = hdr_end - buf + 4;
+  char *buf = Dowa_Arena_Allocate(p_resp->p_arena, hdr_len + 1);
+  memcpy(buf, p_handle->read_buffer, hdr_len);
+  buf[hdr_len] = '\0';
+  char *hdr_end = buf + hdr_len - 4;
 
   char version[16];
   int status_code;
@@ -328,12 +402,12 @@
 
   Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);
 
-  void *p_cl_kv = Dowa_HashMap_Get_Ptr(p_resp->headers, "Content-Length");
   size_t body_len = 0;
-  if (p_cl_kv)
+  const char *content_length = Seobeo_Client_Header_Value(
+      p_resp->headers, "Content-Length");
+  if (content_length)
   {
-    const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value;
-    body_len = atoi(content_length_str);
+    body_len = (size_t)strtoull(content_length, NULL, 10);
   }
 
   FILE *p_file = NULL;
@@ -366,15 +440,32 @@
 
         total_read += to_copy;
         Seobeo_Handle_Consume(p_handle, (uint32)to_copy);
+        last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
       }
 
       if (total_read < body_len)
       {
         int r = Seobeo_Handle_Read(p_handle);
-        if (r < 0 || r == -2)
-          break;
+        if (r == -2 || r < 0)
+        {
+          if (p_file) fclose(p_file);
+          Seobeo_Client_Response_Destroy(p_resp);
+          return NULL;
+        }
+        if (r > 0)
+          last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
         if (r == 0)
+        {
+          if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
+          {
+            Seobeo_Log(SEOBEO_ERROR, "HTTP response body timed out\n");
+            if (p_file) fclose(p_file);
+            Seobeo_Client_Response_Destroy(p_resp);
+            return NULL;
+          }
+          usleep(1000);
           continue;
+        }
       }
     }
 
@@ -397,9 +488,12 @@
 
     while (1)
     {
-      int n = Seobeo_Handle_Read(p_handle);
+      int n = p_handle->read_buffer_len > 0
+          ? (int)p_handle->read_buffer_len
+          : Seobeo_Handle_Read(p_handle);
       if (n > 0)
       {
+        last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
         if (download_path)
         {
           fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file);
@@ -420,9 +514,23 @@
       else if (n == -2)
         break;
       else if (n == 0)
+      {
+        if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
+        {
+          Seobeo_Log(SEOBEO_ERROR, "HTTP response body timed out\n");
+          if (p_file) fclose(p_file);
+          Seobeo_Client_Response_Destroy(p_resp);
+          return NULL;
+        }
+        usleep(1000);
         continue;
+      }
       else
-        break;
+      {
+        if (p_file) fclose(p_file);
+        Seobeo_Client_Response_Destroy(p_resp);
+        return NULL;
+      }
     }
 
     if (!download_path)
@@ -468,7 +576,8 @@
     return NULL;
   }
 
-  Seobeo_Client_Response *p_resp = Seobeo_Client_Parse_Response(p_handle, p_req->download_path);
+  Seobeo_Client_Response *p_resp =
+      Seobeo_Client_Parse_Response(p_handle, p_req->download_path, p_req->timeout_ms);
 
   Seobeo_Handle_Destroy(p_handle);
 
--- a/seobeo/s_network.c	Sun Jan 25 10:44:04 2026 -0800
+++ b/seobeo/s_network.c	Sun Aug 02 16:50:48 2026 -0700
@@ -1,8 +1,37 @@
 #include "seobeo/seobeo.h"
 
+static pthread_once_t g_sigpipe_once = PTHREAD_ONCE_INIT;
+
+static void Seobeo_Process_Ignore_Sigpipe(void)
+{
+  signal(SIGPIPE, SIG_IGN);
+}
+
+static void Seobeo_Socket_Disable_Sigpipe(int socket_fd)
+{
+#ifdef SO_NOSIGPIPE
+  int enabled = 1;
+  setsockopt(socket_fd, SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof(enabled));
+#else
+  (void)socket_fd;
+#endif
+}
+
+static ssize_t Seobeo_Socket_Write(
+    int socket_fd,
+    const void *buffer,
+    size_t length)
+{
+#ifdef MSG_NOSIGNAL
+  return send(socket_fd, buffer, length, MSG_NOSIGNAL);
+#else
+  return send(socket_fd, buffer, length, 0);
+#endif
+}
 
 Seobeo_Handle *Seobeo_Stream_Handle_Server_Create(const char *host,  const char* port)
 {
+  pthread_once(&g_sigpipe_once, Seobeo_Process_Ignore_Sigpipe);
   Seobeo_Handle *p_handle;
   struct addrinfo hints, *server_infos, *free_server_info;
   int32 socket_fd, yes = 1;  // Need this for setsockopt 
@@ -26,6 +55,7 @@
     if((socket_fd = socket(free_server_info->ai_family,
                         free_server_info->ai_socktype, free_server_info->ai_protocol)) == -1)
     { perror("socket"); continue; }
+    Seobeo_Socket_Disable_Sigpipe(socket_fd);
 
      if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1)
      { perror("setsockopt SO_REUSEADDR"); continue; }
@@ -81,6 +111,7 @@
 
 Seobeo_Handle *Seobeo_Stream_Handle_Client_Create(const char *host,  const char* port, boolean use_tls)
 {
+  pthread_once(&g_sigpipe_once, Seobeo_Process_Ignore_Sigpipe);
   Seobeo_Handle *p_handle;
   p_handle = malloc(sizeof(*p_handle));
 
@@ -98,6 +129,7 @@
   if((socket_fd = socket(server_infos->ai_family,
                       server_infos->ai_socktype, server_infos->ai_protocol)) == -1)
   { perror("socket"); return NULL; }
+  Seobeo_Socket_Disable_Sigpipe(socket_fd);
 
   if (connect(socket_fd, server_infos->ai_addr, server_infos->ai_addrlen) != 0)
   { perror("connect"); return NULL; }
@@ -154,6 +186,7 @@
       Seobeo_Get_IP4_Or_IP6((struct sockaddr *)&addr),
       client_inet_addr, sizeof client_inet_addr);
   if (client_fd == -1) return NULL;
+  Seobeo_Socket_Disable_Sigpipe(client_fd);
 
   // Set non blocking...
   int flags = fcntl(client_fd, F_GETFL, 0);
@@ -236,7 +269,7 @@
     }else
     {
       Seobeo_Log(SEOBEO_DEBUG, "Flushing socket: %d\n", p_handle->socket);
-      ssize_t n = write(
+      ssize_t n = Seobeo_Socket_Write(
         p_handle->socket,
         p_handle->write_buffer + sent,
         total - sent
@@ -268,9 +301,19 @@
     uint32 offset = 0;
     while (offset < data_size)
     {
-      ssize_t n = write(p_handle->socket,
-                data + offset,
-                data_size - offset);
+      ssize_t n;
+      if (p_handle->ssl)
+      {
+        n = Seobeo_SSL_Write(p_handle, data + offset, data_size - offset);
+      }
+      else
+      {
+        n = Seobeo_Socket_Write(
+            p_handle->socket,
+            data + offset,
+            data_size - offset);
+      }
+
       if (n==0)
       {
         // DEBUG
--- a/seobeo/s_web.c	Sun Jan 25 10:44:04 2026 -0800
+++ b/seobeo/s_web.c	Sun Aug 02 16:50:48 2026 -0700
@@ -51,6 +51,7 @@
   {
     case HTTP_OK: status_text = "OK"; break;
     case HTTP_CREATED: status_text = "Created"; break;
+    case HTTP_NO_CONTENT: status_text = "No Content"; break;
     case HTTP_MOVED_PERMANENTLY: status_text = "Moved Permanently"; break;
     case HTTP_FOUND: status_text = "Found"; break;
     case HTTP_BAD_REQUEST: status_text = "Bad Request"; break;
@@ -58,11 +59,13 @@
     case HTTP_FORBIDDEN: status_text = "Forbidden"; break;
     case HTTP_NOT_FOUND: status_text = "Not Found"; break;
     case HTTP_INTERNAL_ERROR: status_text = "Internal Server Error"; break;
+    case 502: status_text = "Bad Gateway"; break;
+    case 504: status_text = "Gateway Timeout"; break;
     default: status_text = "Unknown"; break;
   }
 
-  sprintf(
-    buffer,
+  snprintf(
+    (char*)buffer, 1024,
     "HTTP/1.1 %d %s\r\n"
     "Content-Type: %s\r\n"
     "Content-Length: %d\r\n"
@@ -774,6 +777,11 @@
   Seobeo_Router_Send_Response_KeepAlive(p_handle, p_response_map, p_arena, FALSE);
 }
 
+static boolean Seobeo_Response_Header_Value_Is_Safe(const char *value)
+{
+  return value && strchr(value, '\r') == NULL && strchr(value, '\n') == NULL;
+}
+
 void Seobeo_Router_Send_Response_KeepAlive(
     Seobeo_Handle *p_handle,
     Seobeo_Request_Entry *p_response_map,
@@ -819,26 +827,63 @@
   else
     body_length = strlen(body);
 
-  char *header = Dowa_Arena_Allocate(p_arena, 4096);
-  Seobeo_Web_Header_Generate_KeepAlive(header, status, content_type, body_length, keep_alive);
+  size_t header_capacity = 1024;
   for (int i = 0; i < Dowa_Array_Length(p_response_map); i++)
   {
+    const char *key = p_response_map[i].key;
+    const char *value = p_response_map[i].value;
     if (
-      strstr(p_response_map[i].key, "status") ||
-      strstr(p_response_map[i].key, "body") ||
-      strstr(p_response_map[i].key, "content-type") ||
-      strstr(p_response_map[i].key, "content-length")
+      strcasecmp(key, "status") == 0 ||
+      strcasecmp(key, "body") == 0 ||
+      strcasecmp(key, "content-type") == 0 ||
+      strcasecmp(key, "content-length") == 0
+    )
+      continue;
+    if (Seobeo_Response_Header_Value_Is_Safe(key) &&
+        Seobeo_Response_Header_Value_Is_Safe(value))
+      header_capacity += strlen(key) + strlen(value) + 4;
+  }
+
+  char *header = Dowa_Arena_Allocate(p_arena, header_capacity);
+  Seobeo_Web_Header_Generate_KeepAlive(header, status, content_type, body_length, keep_alive);
+  size_t header_length = strlen(header);
+  if (header_length < 2)
+    return;
+  header_length -= 2;
+
+  for (int i = 0; i < Dowa_Array_Length(p_response_map); i++)
+  {
+    const char *key = p_response_map[i].key;
+    const char *value = p_response_map[i].value;
+    if (
+      strcasecmp(key, "status") == 0 ||
+      strcasecmp(key, "body") == 0 ||
+      strcasecmp(key, "content-type") == 0 ||
+      strcasecmp(key, "content-length") == 0
     )
       continue;
 
-    int32 current_header_len = strlen(header);
-    char *temp = malloc(sizeof(char) * 1024);
-    sprintf(temp, "%s: %s\r\n\r\n", p_response_map[i].key, p_response_map[i].value);
-    memcpy(&header[current_header_len - 2 /* \r\n */], temp, strlen(temp));
-    free(temp);
+    if (!Seobeo_Response_Header_Value_Is_Safe(key) ||
+        !Seobeo_Response_Header_Value_Is_Safe(value))
+    {
+      Seobeo_Log(SEOBEO_WARNING, "Skipping unsafe response header\n");
+      continue;
+    }
+
+    int written = snprintf(
+        header + header_length,
+        header_capacity - header_length,
+        "%s: %s\r\n",
+        key,
+        value);
+    if (written < 0 || (size_t)written >= header_capacity - header_length)
+    {
+      Seobeo_Log(SEOBEO_ERROR, "Response header exceeded allocated capacity\n");
+      return;
+    }
+    header_length += (size_t)written;
   }
-
-  printf("hEADER %s\n", header);
+  memcpy(header + header_length, "\r\n", 3);
 
   Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
   Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length);
--- a/seobeo/seobeo.h	Sun Jan 25 10:44:04 2026 -0800
+++ b/seobeo/seobeo.h	Sun Aug 02 16:50:48 2026 -0700
@@ -30,6 +30,7 @@
 // HTTP STATUS CODE
 #define HTTP_OK 200
 #define HTTP_CREATED 201
+#define HTTP_NO_CONTENT 204
 #define HTTP_MOVED_PERMANENTLY 301
 #define HTTP_FOUND 302
 #define HTTP_BAD_REQUEST 400
@@ -82,6 +83,8 @@
 extern void                    Seobeo_Client_Request_Set_Body(Seobeo_Client_Request *p_req, const char *body, size_t length);
 /* Enable/disable following redirects with max redirect count. Default is FALSE with 10 max. */
 extern void                    Seobeo_Client_Request_Set_Follow_Redirects(Seobeo_Client_Request *p_req, boolean follow, int32 max_redirects);
+/* Set the maximum idle time for HTTP response reads. Zero disables the timeout. */
+extern void                    Seobeo_Client_Request_Set_Timeout_Milliseconds(Seobeo_Client_Request *p_req, int32 timeout_ms);
 /* Set download path to save response body to file instead of memory. */
 extern void                    Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path);
 /* Execute the HTTP request and return response. */
--- a/seobeo/seobeo_internal.h	Sun Jan 25 10:44:04 2026 -0800
+++ b/seobeo/seobeo_internal.h	Sun Aug 02 16:50:48 2026 -0700
@@ -131,6 +131,7 @@
 
   boolean  follow_redirects;
   int32    max_redirects;
+  int32    timeout_ms;
 
   char    *download_path;
 
@@ -158,6 +159,7 @@
 extern void                    Seobeo_Client_Request_Add_Header_Array(Seobeo_Client_Request *p_req, const char *header);
 extern void                    Seobeo_Client_Request_Set_Body(Seobeo_Client_Request *p_req, const char *body, size_t length);
 extern void                    Seobeo_Client_Request_Set_Follow_Redirects(Seobeo_Client_Request *p_req, boolean follow, int32 max_redirects);
+extern void                    Seobeo_Client_Request_Set_Timeout_Milliseconds(Seobeo_Client_Request *p_req, int32 timeout_ms);
 extern void                    Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path);
 extern Seobeo_Client_Response *Seobeo_Client_Request_Execute(Seobeo_Client_Request *p_req);
 extern void                    Seobeo_Client_Request_Destroy(Seobeo_Client_Request *p_req);
--- a/seobeo/tests/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ b/seobeo/tests/BUILD	Sun Aug 02 16:50:48 2026 -0700
@@ -28,3 +28,48 @@
   args = ["$(location //seobeo/examples:websocket_server_example)"],
   visibility = ["//visibility:public"],
 )
+
+cc_test(
+  name = "seobeo_response_test",
+  srcs = ["seobeo_response_test.c"],
+  deps = ["//seobeo:seobeo"],
+  size = "small",
+  timeout = "short",
+  visibility = ["//visibility:public"],
+)
+
+cc_test(
+  name = "seobeo_sigpipe_test",
+  srcs = ["seobeo_sigpipe_test.c"],
+  deps = ["//seobeo:seobeo"],
+  size = "small",
+  timeout = "short",
+  visibility = ["//visibility:public"],
+)
+
+cc_test(
+  name = "seobeo_http_timeout_test",
+  srcs = ["seobeo_http_timeout_test.c"],
+  deps = ["//seobeo:seobeo"],
+  size = "small",
+  timeout = "short",
+  visibility = ["//visibility:public"],
+)
+
+cc_test(
+  name = "seobeo_http_framing_test",
+  srcs = ["seobeo_http_framing_test.c"],
+  deps = ["//seobeo:seobeo"],
+  size = "small",
+  timeout = "short",
+  visibility = ["//visibility:public"],
+)
+
+cc_test(
+  name = "seobeo_http_content_length_test",
+  srcs = ["seobeo_http_content_length_test.c"],
+  deps = ["//seobeo:seobeo"],
+  size = "small",
+  timeout = "short",
+  visibility = ["//visibility:public"],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/tests/seobeo_http_content_length_test.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,91 @@
+#include "seobeo/seobeo.h"
+
+#include <arpa/inet.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+typedef struct {
+  int listener;
+} Content_Length_Server;
+
+static int64_t monotonic_milliseconds(void)
+{
+  struct timespec now;
+  clock_gettime(CLOCK_MONOTONIC, &now);
+  return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
+}
+
+static void *serve_lowercase_content_length(void *argument)
+{
+  Content_Length_Server *server = argument;
+  int client = accept(server->listener, NULL, NULL);
+  if (client >= 0) {
+    char request[1024];
+    (void)read(client, request, sizeof(request));
+    const char response[] =
+        "HTTP/1.1 200 OK\r\n"
+        "content-length: 4\r\n"
+        "Connection: keep-alive\r\n"
+        "\r\n"
+        "done";
+    (void)write(client, response, sizeof(response) - 1);
+    usleep(500000);
+    close(client);
+  }
+  return NULL;
+}
+
+int main(void)
+{
+  int listener = socket(AF_INET, SOCK_STREAM, 0);
+  if (listener < 0)
+    return 1;
+
+  struct sockaddr_in address = {
+    .sin_family = AF_INET,
+    .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+    .sin_port = 0,
+  };
+  if (bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 ||
+      listen(listener, 1) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  socklen_t address_length = sizeof(address);
+  if (getsockname(listener, (struct sockaddr *)&address, &address_length) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  Content_Length_Server server = {.listener = listener};
+  pthread_t thread;
+  if (pthread_create(&thread, NULL, serve_lowercase_content_length, &server) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  char url[128];
+  snprintf(url, sizeof(url), "http://127.0.0.1:%u/", ntohs(address.sin_port));
+  Seobeo_Client_Request *request = Seobeo_Client_Request_Create(url);
+  Seobeo_Client_Request_Set_Timeout_Milliseconds(request, 1000);
+  int64_t started = monotonic_milliseconds();
+  Seobeo_Client_Response *response = Seobeo_Client_Request_Execute(request);
+  int64_t elapsed = monotonic_milliseconds() - started;
+
+  int failed = !response ||
+               response->body_length != 4 ||
+               memcmp(response->body, "done", 4) != 0 ||
+               elapsed > 300;
+  if (failed)
+    fprintf(stderr, "Case-insensitive Content-Length was not honored (%lldms)\n",
+            (long long)elapsed);
+
+  Seobeo_Client_Response_Destroy(response);
+  Seobeo_Client_Request_Destroy(request);
+  pthread_join(thread, NULL);
+  close(listener);
+  return failed;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/tests/seobeo_http_framing_test.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,79 @@
+#include "seobeo/seobeo.h"
+
+#include <arpa/inet.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+
+typedef struct {
+  int listener;
+} Framing_Server;
+
+static void *serve_close_delimited_response(void *argument)
+{
+  Framing_Server *server = argument;
+  int client = accept(server->listener, NULL, NULL);
+  if (client >= 0) {
+    char request[1024];
+    (void)read(client, request, sizeof(request));
+    const char response[] =
+        "HTTP/1.1 200 OK\r\n"
+        "Content-Type: text/plain\r\n"
+        "Connection: close\r\n"
+        "\r\n"
+        "buffered-body";
+    (void)write(client, response, sizeof(response) - 1);
+    close(client);
+  }
+  return NULL;
+}
+
+int main(void)
+{
+  int listener = socket(AF_INET, SOCK_STREAM, 0);
+  if (listener < 0)
+    return 1;
+
+  struct sockaddr_in address = {
+    .sin_family = AF_INET,
+    .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+    .sin_port = 0,
+  };
+  if (bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 ||
+      listen(listener, 1) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  socklen_t address_length = sizeof(address);
+  if (getsockname(listener, (struct sockaddr *)&address, &address_length) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  Framing_Server server = {.listener = listener};
+  pthread_t thread;
+  if (pthread_create(&thread, NULL, serve_close_delimited_response, &server) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  char url[128];
+  snprintf(url, sizeof(url), "http://127.0.0.1:%u/", ntohs(address.sin_port));
+  Seobeo_Client_Request *request = Seobeo_Client_Request_Create(url);
+  Seobeo_Client_Request_Set_Timeout_Milliseconds(request, 1000);
+  Seobeo_Client_Response *response = Seobeo_Client_Request_Execute(request);
+
+  int failed = !response ||
+               response->status_code != 200 ||
+               response->body_length != strlen("buffered-body") ||
+               memcmp(response->body, "buffered-body", strlen("buffered-body")) != 0;
+  if (failed)
+    fprintf(stderr, "Close-delimited response body was not preserved\n");
+
+  Seobeo_Client_Response_Destroy(response);
+  Seobeo_Client_Request_Destroy(request);
+  pthread_join(thread, NULL);
+  close(listener);
+  return failed;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/tests/seobeo_http_timeout_test.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,82 @@
+#include "seobeo/seobeo.h"
+
+#include <arpa/inet.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+typedef struct {
+  int listener;
+} Timeout_Server;
+
+static int64_t monotonic_milliseconds(void)
+{
+  struct timespec now;
+  clock_gettime(CLOCK_MONOTONIC, &now);
+  return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
+}
+
+static void *serve_stalled_response(void *argument)
+{
+  Timeout_Server *server = argument;
+  int client = accept(server->listener, NULL, NULL);
+  if (client >= 0) {
+    char request[1024];
+    (void)read(client, request, sizeof(request));
+    usleep(250000);
+    close(client);
+  }
+  return NULL;
+}
+
+int main(void)
+{
+  int listener = socket(AF_INET, SOCK_STREAM, 0);
+  if (listener < 0)
+    return 1;
+
+  struct sockaddr_in address = {
+    .sin_family = AF_INET,
+    .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+    .sin_port = 0,
+  };
+  if (bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 ||
+      listen(listener, 1) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  socklen_t address_length = sizeof(address);
+  if (getsockname(listener, (struct sockaddr *)&address, &address_length) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  Timeout_Server server = {.listener = listener};
+  pthread_t thread;
+  if (pthread_create(&thread, NULL, serve_stalled_response, &server) != 0) {
+    close(listener);
+    return 1;
+  }
+
+  char url[128];
+  snprintf(url, sizeof(url), "http://127.0.0.1:%u/", ntohs(address.sin_port));
+  Seobeo_Client_Request *request = Seobeo_Client_Request_Create(url);
+  Seobeo_Client_Request_Set_Timeout_Milliseconds(request, 50);
+
+  int64_t started = monotonic_milliseconds();
+  Seobeo_Client_Response *response = Seobeo_Client_Request_Execute(request);
+  int64_t elapsed = monotonic_milliseconds() - started;
+
+  int failed = response != NULL || elapsed < 40 || elapsed > 2000;
+  if (failed)
+    fprintf(stderr, "Expected a bounded timeout; response=%p elapsed=%lldms\n",
+            (void *)response, (long long)elapsed);
+
+  Seobeo_Client_Response_Destroy(response);
+  Seobeo_Client_Request_Destroy(request);
+  pthread_join(thread, NULL);
+  close(listener);
+  return failed;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/tests/seobeo_response_test.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,79 @@
+#include "seobeo/seobeo.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+
+int main(void)
+{
+  int sockets[2];
+  if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0)
+    return 1;
+
+  Seobeo_Handle handle = {0};
+  handle.socket = sockets[0];
+  handle.write_buffer_capacity = 4096;
+  handle.write_buffer = malloc(handle.write_buffer_capacity);
+
+  Dowa_Arena *arena = Dowa_Arena_Create(4096);
+  Seobeo_Request_Entry *response = NULL;
+  Dowa_HashMap_Push_Arena(response, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(response, "content-type", "text/plain", arena);
+  Dowa_HashMap_Push_Arena(response, "body", "hello", arena);
+  Dowa_HashMap_Push_Arena(response, "X-Test", "present", arena);
+  Dowa_HashMap_Push_Arena(response, "X-Unsafe", "bad\r\nInjected: yes", arena);
+
+  Seobeo_Router_Send_Response(&handle, response, arena);
+
+  char received[4096] = {0};
+  ssize_t length = read(sockets[1], received, sizeof(received) - 1);
+  int failed = 0;
+  if (length <= 0)
+    failed = 1;
+  if (!strstr(received, "Content-Length: 5\r\n"))
+    failed = 1;
+  if (!strstr(received, "X-Test: present\r\n\r\nhello"))
+    failed = 1;
+  if (strstr(received, "Injected: yes"))
+    failed = 1;
+
+  if (failed)
+    fprintf(stderr, "Unexpected response:\n%s\n", received);
+
+  close(sockets[0]);
+  close(sockets[1]);
+  free(handle.write_buffer);
+  Dowa_Arena_Free(arena);
+
+  if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0)
+    return 1;
+
+  memset(&handle, 0, sizeof(handle));
+  handle.socket = sockets[0];
+  handle.write_buffer_capacity = 4096;
+  handle.write_buffer = malloc(handle.write_buffer_capacity);
+  arena = Dowa_Arena_Create(4096);
+  response = NULL;
+  Dowa_HashMap_Push_Arena(response, "status", "204", arena);
+  Dowa_HashMap_Push_Arena(response, "content-type", "text/plain", arena);
+  Dowa_HashMap_Push_Arena(response, "body", "", arena);
+  Seobeo_Router_Send_Response(&handle, response, arena);
+
+  memset(received, 0, sizeof(received));
+  length = read(sockets[1], received, sizeof(received) - 1);
+  if (length <= 0 ||
+      !strstr(received, "HTTP/1.1 204 No Content\r\n") ||
+      !strstr(received, "Content-Length: 0\r\n") ||
+      !strstr(received, "\r\n\r\n"))
+    failed = 1;
+
+  if (failed)
+    fprintf(stderr, "Unexpected no-content response:\n%s\n", received);
+
+  close(sockets[0]);
+  close(sockets[1]);
+  free(handle.write_buffer);
+  Dowa_Arena_Free(arena);
+  return failed;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/tests/seobeo_sigpipe_test.c	Sun Aug 02 16:50:48 2026 -0700
@@ -0,0 +1,36 @@
+#include "seobeo/seobeo.h"
+
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+
+int main(void)
+{
+  Seobeo_Handle *server =
+      Seobeo_Stream_Handle_Server_Create("127.0.0.1", "0");
+  if (!server)
+    return 1;
+
+  raise(SIGPIPE);
+  Seobeo_Handle_Destroy(server);
+
+  int sockets[2];
+  if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0)
+    return 1;
+
+  Seobeo_Handle handle = {0};
+  handle.socket = sockets[0];
+  handle.write_buffer_capacity = 64;
+  handle.write_buffer = malloc(handle.write_buffer_capacity);
+  if (!handle.write_buffer)
+    return 1;
+
+  close(sockets[1]);
+  if (Seobeo_Handle_Queue(&handle, (const uint8 *)"closed", 6) != 0)
+    return 1;
+
+  int result = Seobeo_Handle_Flush(&handle);
+  close(sockets[0]);
+  free(handle.write_buffer);
+  return result < 0 ? 0 : 1;
+}
--- a/third_party/README.md	Sun Jan 25 10:44:04 2026 -0800
+++ b/third_party/README.md	Sun Aug 02 16:50:48 2026 -0700
@@ -8,7 +8,6 @@
 |-----------|-------------|
 | `bun/` | Bun JavaScript runtime |
 | `emsdk/` | Emscripten SDK for WASM compilation |
-| `ffmpeg/` | FFmpeg CLI wrapper for media processing |
 | `highlight/` | highlight.js for syntax highlighting |
 | `libuv/` | Async I/O library |
 | `luajit/` | LuaJIT interpreter |
@@ -24,5 +23,4 @@
 deps = ["//third_party/sqlite3:sqlite3"]
 deps = ["//third_party/raylib:raylib"]
 deps = ["//third_party/libuv:libuv"]
-deps = ["//third_party/ffmpeg:ffmpeg_cli"]
 ```
--- a/third_party/ffmpeg/BUILD	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-# Assumes ffmpeg is installed on the system (apt install ffmpeg / brew install ffmpeg)
-load("@rules_cc//cc:cc_library.bzl", "cc_library")
-load("@rules_cc//cc:cc_test.bzl", "cc_test")
-
-cc_library(
-    name = "ffmpeg",
-    hdrs = ["ffmpeg.h"],
-    srcs = ["ffmpeg_wrapper.c"],
-    linkopts = select({
-        "@platforms//os:linux": [
-            "-lavcodec",
-            "-lavformat",
-            "-lavutil",
-            "-lswscale",
-            "-lswresample",
-        ],
-        "@platforms//os:macos": [
-            "-lavcodec",
-            "-lavformat",
-            "-lavutil",
-            "-lswscale",
-            "-lswresample",
-        ],
-        "//conditions:default": [],
-    }),
-    copts = select({
-        "@platforms//os:linux": ["-I/usr/include/ffmpeg"],
-        "@platforms//os:macos": ["-I/opt/homebrew/include", "-I/usr/local/include"],
-        "//conditions:default": [],
-    }),
-    visibility = ["//visibility:public"],
-)
-
-# For projects that just need to shell out to ffmpeg CLI
-cc_library(
-    name = "ffmpeg_cli",
-    hdrs = ["ffmpeg_cli.h"],
-    srcs = ["ffmpeg_cli.c"],
-    visibility = ["//visibility:public"],
-)
--- a/third_party/ffmpeg/README.md	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-# ffmpeg
-
-FFmpeg wrapper for Bazel projects.
-
-## Prerequisites
-
-FFmpeg must be installed on the system:
-
-```bash
-# Ubuntu/Debian
-sudo apt install ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
-
-# macOS
-brew install ffmpeg
-
-# Arch Linux
-sudo pacman -S ffmpeg
-```
-
-## Libraries
-
-### ffmpeg_cli
-
-Shells out to the `ffmpeg` and `ffprobe` command-line tools. This is the recommended approach for most projects as it's simpler and doesn't require linking against FFmpeg libraries.
-
-```starlark
-deps = ["//third_party/ffmpeg:ffmpeg_cli"]
-```
-
-```c
-#include "third_party/ffmpeg/ffmpeg_cli.h"
-
-// Convert image to WebP
-ImageConvertOpts opts = ffmpeg_image_opts_default();
-opts.quality = 85;
-ffmpeg_image_to_webp("input.jpg", "output.webp", &opts);
-
-// Convert video to HLS
-HlsConvertOpts hls_opts = ffmpeg_hls_opts_default();
-ffmpeg_video_to_hls("input.mp4", "./output", "video", &hls_opts);
-```
-
-### ffmpeg (library)
-
-Links against FFmpeg libraries directly. Use this when you need low-level access to FFmpeg's encoding/decoding APIs.
-
-```starlark
-deps = ["//third_party/ffmpeg:ffmpeg"]
-```
-
-## API
-
-### Image Conversion
-
-```c
-FfmpegResult ffmpeg_image_to_webp(const char* input, const char* output, ImageConvertOpts* opts);
-```
-
-### Video to HLS
-
-```c
-FfmpegResult ffmpeg_video_to_hls(const char* input, const char* output_dir, const char* name, HlsConvertOpts* opts);
-```
-
-### Video Thumbnail
-
-```c
-FfmpegResult ffmpeg_video_thumbnail(const char* input, const char* output, int timestamp_seconds);
-```
-
-### Duration
-
-```c
-FfmpegResult ffmpeg_get_duration(const char* input, double* duration_out);
-```
--- a/third_party/ffmpeg/ffmpeg.h	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-// FFmpeg library wrapper header
-// This is a placeholder for projects that want to use libavcodec/libavformat directly
-// Most projects should use ffmpeg_cli.h instead which shells out to the ffmpeg binary
-
-#ifndef FFMPEG_WRAPPER_H
-#define FFMPEG_WRAPPER_H
-
-// Include FFmpeg headers when using the library directly
-// Uncomment these when building with FFmpeg libraries linked
-/*
-#include <libavcodec/avcodec.h>
-#include <libavformat/avformat.h>
-#include <libavutil/avutil.h>
-#include <libswscale/swscale.h>
-#include <libswresample/swresample.h>
-*/
-
-#endif // FFMPEG_WRAPPER_H
--- a/third_party/ffmpeg/ffmpeg_cli.c	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,260 +0,0 @@
-#include "ffmpeg_cli.h"
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <stdarg.h>
-#include <sys/stat.h>
-#include <sys/wait.h>
-#include <errno.h>
-
-#define MAX_CMD_LEN 4096
-#define MAX_ERROR_LEN 512
-
-static char last_error[MAX_ERROR_LEN] = {0};
-
-static void set_error(const char* fmt, ...) {
-    va_list args;
-    va_start(args, fmt);
-    vsnprintf(last_error, MAX_ERROR_LEN, fmt, args);
-    va_end(args);
-}
-
-static bool file_exists(const char* path) {
-    struct stat st;
-    return stat(path, &st) == 0;
-}
-
-static bool ensure_dir(const char* path) {
-    struct stat st;
-    if (stat(path, &st) == 0) {
-        return S_ISDIR(st.st_mode);
-    }
-    return mkdir(path, 0755) == 0;
-}
-
-static int run_command(const char* cmd) {
-    int ret = system(cmd);
-    if (ret == -1) {
-        set_error("Failed to execute command: %s", strerror(errno));
-        return -1;
-    }
-    return WEXITSTATUS(ret);
-}
-
-ImageConvertOpts ffmpeg_image_opts_default(void) {
-    return (ImageConvertOpts){
-        .quality = 80,
-        .max_width = 0,
-        .max_height = 0,
-        .preserve_aspect = true,
-    };
-}
-
-HlsConvertOpts ffmpeg_hls_opts_default(void) {
-    return (HlsConvertOpts){
-        .segment_duration = 6,
-        .video_bitrate = 0,
-        .audio_bitrate = 128,
-        .max_width = 0,
-        .max_height = 0,
-        .generate_thumbnail = true,
-    };
-}
-
-bool ffmpeg_is_available(void) {
-    return system("ffmpeg -version > /dev/null 2>&1") == 0;
-}
-
-const char* ffmpeg_last_error(void) {
-    return last_error;
-}
-
-FfmpegResult ffmpeg_image_to_webp(const char* input_path, const char* output_path, ImageConvertOpts* opts) {
-    if (!input_path || !output_path) {
-        set_error("Invalid arguments: input_path and output_path required");
-        return FFMPEG_ERR_INVALID_ARGS;
-    }
-
-    if (!file_exists(input_path)) {
-        set_error("Input file not found: %s", input_path);
-        return FFMPEG_ERR_INPUT_NOT_FOUND;
-    }
-
-    ImageConvertOpts default_opts = ffmpeg_image_opts_default();
-    if (!opts) opts = &default_opts;
-
-    char cmd[MAX_CMD_LEN];
-    char scale_filter[256] = "";
-
-    // Build scale filter if resizing
-    if (opts->max_width > 0 || opts->max_height > 0) {
-        int w = opts->max_width > 0 ? opts->max_width : -1;
-        int h = opts->max_height > 0 ? opts->max_height : -1;
-
-        if (opts->preserve_aspect) {
-            if (w > 0 && h > 0) {
-                snprintf(scale_filter, sizeof(scale_filter),
-                    "-vf \"scale='min(%d,iw)':min'(%d,ih)':force_original_aspect_ratio=decrease\"", w, h);
-            } else if (w > 0) {
-                snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale='min(%d,iw)':-1\"", w);
-            } else {
-                snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=-1:'min(%d,ih)'\"", h);
-            }
-        } else {
-            snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h);
-        }
-    }
-
-    snprintf(cmd, sizeof(cmd),
-        "ffmpeg -y -i \"%s\" %s -quality %d \"%s\" 2>/dev/null",
-        input_path, scale_filter, opts->quality, output_path);
-
-    int ret = run_command(cmd);
-    if (ret != 0) {
-        set_error("FFmpeg conversion failed with code %d", ret);
-        return FFMPEG_ERR_PROCESS_FAILED;
-    }
-
-    if (!file_exists(output_path)) {
-        set_error("Output file was not created");
-        return FFMPEG_ERR_OUTPUT_FAILED;
-    }
-
-    return FFMPEG_OK;
-}
-
-FfmpegResult ffmpeg_video_to_hls(const char* input_path, const char* output_dir, const char* name, HlsConvertOpts* opts) {
-    if (!input_path || !output_dir || !name) {
-        set_error("Invalid arguments: input_path, output_dir, and name required");
-        return FFMPEG_ERR_INVALID_ARGS;
-    }
-
-    if (!file_exists(input_path)) {
-        set_error("Input file not found: %s", input_path);
-        return FFMPEG_ERR_INPUT_NOT_FOUND;
-    }
-
-    if (!ensure_dir(output_dir)) {
-        set_error("Failed to create output directory: %s", output_dir);
-        return FFMPEG_ERR_OUTPUT_FAILED;
-    }
-
-    HlsConvertOpts default_opts = ffmpeg_hls_opts_default();
-    if (!opts) opts = &default_opts;
-
-    char cmd[MAX_CMD_LEN];
-    char scale_filter[256] = "";
-    char video_bitrate[64] = "";
-    char audio_bitrate[64] = "";
-
-    // Build scale filter if resizing
-    if (opts->max_width > 0 || opts->max_height > 0) {
-        int w = opts->max_width > 0 ? opts->max_width : -2;
-        int h = opts->max_height > 0 ? opts->max_height : -2;
-        snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h);
-    }
-
-    // Video bitrate
-    if (opts->video_bitrate > 0) {
-        snprintf(video_bitrate, sizeof(video_bitrate), "-b:v %dk", opts->video_bitrate);
-    }
-
-    // Audio bitrate
-    snprintf(audio_bitrate, sizeof(audio_bitrate), "-b:a %dk",
-        opts->audio_bitrate > 0 ? opts->audio_bitrate : 128);
-
-    char playlist_path[1024];
-    char segment_pattern[1024];
-    snprintf(playlist_path, sizeof(playlist_path), "%s/%s.m3u8", output_dir, name);
-    snprintf(segment_pattern, sizeof(segment_pattern), "%s/%s_%%03d.ts", output_dir, name);
-
-    // Main HLS conversion command
-    snprintf(cmd, sizeof(cmd),
-        "ffmpeg -y -i \"%s\" "
-        "-c:v libx264 -preset fast -crf 22 %s %s "
-        "-c:a aac %s "
-        "-f hls "
-        "-hls_time %d "
-        "-hls_list_size 0 "
-        "-hls_segment_filename \"%s\" "
-        "\"%s\" 2>/dev/null",
-        input_path,
-        scale_filter, video_bitrate,
-        audio_bitrate,
-        opts->segment_duration,
-        segment_pattern,
-        playlist_path);
-
-    int ret = run_command(cmd);
-    if (ret != 0) {
-        set_error("FFmpeg HLS conversion failed with code %d", ret);
-        return FFMPEG_ERR_PROCESS_FAILED;
-    }
-
-    // Generate thumbnail if requested
-    if (opts->generate_thumbnail) {
-        char thumb_path[1024];
-        snprintf(thumb_path, sizeof(thumb_path), "%s/%s_thumb.jpg", output_dir, name);
-        ffmpeg_video_thumbnail(input_path, thumb_path, 1);
-    }
-
-    return FFMPEG_OK;
-}
-
-FfmpegResult ffmpeg_video_thumbnail(const char* input_path, const char* output_path, int timestamp_seconds) {
-    if (!input_path || !output_path) {
-        set_error("Invalid arguments: input_path and output_path required");
-        return FFMPEG_ERR_INVALID_ARGS;
-    }
-
-    if (!file_exists(input_path)) {
-        set_error("Input file not found: %s", input_path);
-        return FFMPEG_ERR_INPUT_NOT_FOUND;
-    }
-
-    char cmd[MAX_CMD_LEN];
-    snprintf(cmd, sizeof(cmd),
-        "ffmpeg -y -i \"%s\" -ss %d -vframes 1 -q:v 2 \"%s\" 2>/dev/null",
-        input_path, timestamp_seconds, output_path);
-
-    int ret = run_command(cmd);
-    if (ret != 0) {
-        set_error("FFmpeg thumbnail generation failed with code %d", ret);
-        return FFMPEG_ERR_PROCESS_FAILED;
-    }
-
-    return FFMPEG_OK;
-}
-
-FfmpegResult ffmpeg_get_duration(const char* input_path, double* duration_out) {
-    if (!input_path || !duration_out) {
-        set_error("Invalid arguments");
-        return FFMPEG_ERR_INVALID_ARGS;
-    }
-
-    if (!file_exists(input_path)) {
-        set_error("Input file not found: %s", input_path);
-        return FFMPEG_ERR_INPUT_NOT_FOUND;
-    }
-
-    char cmd[MAX_CMD_LEN];
-    snprintf(cmd, sizeof(cmd),
-        "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"%s\" 2>/dev/null",
-        input_path);
-
-    FILE* fp = popen(cmd, "r");
-    if (!fp) {
-        set_error("Failed to run ffprobe");
-        return FFMPEG_ERR_PROCESS_FAILED;
-    }
-
-    char output[64];
-    if (fgets(output, sizeof(output), fp) != NULL) {
-        *duration_out = atof(output);
-    } else {
-        *duration_out = 0;
-    }
-
-    pclose(fp);
-    return FFMPEG_OK;
-}
--- a/third_party/ffmpeg/ffmpeg_cli.h	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-#ifndef FFMPEG_CLI_H
-#define FFMPEG_CLI_H
-
-#include <stdbool.h>
-
-// Result codes
-typedef enum {
-    FFMPEG_OK = 0,
-    FFMPEG_ERR_INPUT_NOT_FOUND = 1,
-    FFMPEG_ERR_OUTPUT_FAILED = 2,
-    FFMPEG_ERR_INVALID_ARGS = 3,
-    FFMPEG_ERR_PROCESS_FAILED = 4,
-} FfmpegResult;
-
-// Image conversion options
-typedef struct {
-    int quality;        // 0-100, default 80
-    int max_width;      // 0 = no resize
-    int max_height;     // 0 = no resize
-    bool preserve_aspect;
-} ImageConvertOpts;
-
-// Video conversion options for HLS
-typedef struct {
-    int segment_duration;   // seconds, default 6
-    int video_bitrate;      // kbps, 0 = auto
-    int audio_bitrate;      // kbps, 0 = 128
-    int max_width;          // 0 = no resize
-    int max_height;         // 0 = no resize
-    bool generate_thumbnail;
-} HlsConvertOpts;
-
-// Initialize default options
-ImageConvertOpts ffmpeg_image_opts_default(void);
-HlsConvertOpts ffmpeg_hls_opts_default(void);
-
-// Convert image to WebP format
-// output_path should end with .webp
-FfmpegResult ffmpeg_image_to_webp(const char* input_path, const char* output_path, ImageConvertOpts* opts);
-
-// Convert video to HLS format
-// output_dir is the directory where .m3u8 and .ts files will be created
-FfmpegResult ffmpeg_video_to_hls(const char* input_path, const char* output_dir, const char* name, HlsConvertOpts* opts);
-
-// Generate video thumbnail
-FfmpegResult ffmpeg_video_thumbnail(const char* input_path, const char* output_path, int timestamp_seconds);
-
-// Get media duration in seconds (works for both audio and video)
-FfmpegResult ffmpeg_get_duration(const char* input_path, double* duration_out);
-
-// Check if ffmpeg is available on the system
-bool ffmpeg_is_available(void);
-
-// Get last error message
-const char* ffmpeg_last_error(void);
-
-#endif // FFMPEG_CLI_H
--- a/third_party/ffmpeg/ffmpeg_wrapper.c	Sun Jan 25 10:44:04 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-// FFmpeg library wrapper
-// Placeholder implementation for direct FFmpeg library usage
-// Most projects should use ffmpeg_cli.c instead
-
-#include "ffmpeg.h"
-
-// Add FFmpeg library wrapper functions here when needed