changeset 201:6cdee35a7ba9

[MrJuneJune] notes
author MrJuneJune <me@mrjunejune.com>
date Sun, 15 Feb 2026 07:07:50 -0800
parents 90dfcef375fb
children b9b184b3303c
files .hgignore mrjunejune/BUILD mrjunejune/main.c mrjunejune/src/editor/index.html mrjunejune/src/notes/editor.js mrjunejune/src/notes/index.html mrjunejune/src/notes/login.html rich_editor/BUILD rich_editor/rich_editor.js s3/s3_uploader.c
diffstat 10 files changed, 2517 insertions(+), 44 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sat Feb 14 16:32:24 2026 -0800
+++ b/.hgignore	Sun Feb 15 07:07:50 2026 -0800
@@ -45,3 +45,5 @@
 .env
 # Server config
 .config
+# DB
+mrjunejune.db
--- a/mrjunejune/BUILD	Sat Feb 14 16:32:24 2026 -0800
+++ b/mrjunejune/BUILD	Sun Feb 15 07:07:50 2026 -0800
@@ -28,6 +28,14 @@
   dest = "src/public/highlight",
 )
 
+move_files_into_dir(
+  name = "rich_editor_js",
+  srcs = [
+    "//rich_editor:rich_editor",
+  ],
+  dest = "src/public/js",
+)
+
 filegroup(
   name = "public_files",
   srcs = glob(["src/public/*"]),
@@ -42,7 +50,7 @@
     
 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"],
   visibility = ["//mrjunejune/test:__pkg__"],
 )
 
@@ -54,7 +62,10 @@
     "//seobeo:seobeo",
     "//markdown_converter:markdown_to_html_c",
     "//s3:s3",
+    "//deita:deita",
   ],
+  copts = ["-D_GNU_SOURCE"],
+  linkopts = ["-lpthread"],
   data = [
     ":src_files",
     ":config_file",
@@ -71,7 +82,10 @@
     "//seobeo:seobeo_debug",
     "//markdown_converter:markdown_to_html_c",
     "//s3:s3",
+    "//deita:deita",
   ],
+  copts = ["-D_GNU_SOURCE"],
+  linkopts = ["-lpthread"],
   data = [
     ":src_files",
     ":config_file",
--- a/mrjunejune/main.c	Sat Feb 14 16:32:24 2026 -0800
+++ b/mrjunejune/main.c	Sun Feb 15 07:07:50 2026 -0800
@@ -1,7 +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
@@ -10,12 +14,25 @@
 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];
+  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)
 {
@@ -60,12 +77,105 @@
     {
       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\n",
+  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_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)
@@ -93,6 +203,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;
@@ -104,7 +216,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);
@@ -117,15 +230,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);
 }
@@ -182,7 +295,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);
@@ -195,7 +308,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");
@@ -555,12 +668,50 @@
   return resp;
 }
 
+Seobeo_Request_Entry *GetEditor(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, "/editor/index.html", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  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
@@ -681,11 +832,23 @@
     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, 2048 + strlen(presigned.url));
-  snprintf(response_body, 2048 + strlen(presigned.url),
-           "{\"upload_url\":\"%s\",\"key\":\"%s\",\"expires\":%d}",
-           presigned.url, s3_key, g_s3_url_expires);
+  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);
 
@@ -698,6 +861,816 @@
   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);
+  snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid);
+
+  // 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 response
+  char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url));
+  snprintf(response_body, 4096 + strlen(presigned.url),
+           "{\"media_id\":%lld,\"upload_url\":\"%s\",\"expires\":%d}",
+           (long long)media_id, presigned.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;
+
+  // Open thread-local DB connection
+  Deita_Connection *db_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, g_db_path);
+  if (!db_conn || !Deita_Connection_Is_Open(db_conn))
+  {
+    printf("[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 };
+  Deita_Query_Execute_Update_Prepared(db_conn, update_processing, 1, params);
+
+  printf("[MEDIA] Processing media_id=%lld\n", (long long)ctx->media_id);
+
+  // Generate presigned GET URL for download (10 min expiry)
+  S3_Presigned_URL download_url = S3_Presign_Get(&ctx->s3_config, ctx->s3_key_original, 600);
+  if (!download_url.success)
+  {
+    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 generate download URL", media_id_str };
+    Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params);
+    printf("[MEDIA] ERROR: Failed to generate download URL for media_id=%lld\n", (long long)ctx->media_id);
+    S3_Presigned_URL_Destroy(&download_url);
+    Deita_Connection_Close(db_conn);
+    free(ctx);
+    return NULL;
+  }
+
+  // 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_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)
+  {
+    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);
+    printf("[MEDIA] ERROR: Failed to download from S3 for media_id=%lld\n", (long long)ctx->media_id);
+    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_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);
+
+  int ffmpeg_result = system(cmd);
+  if (ffmpeg_result != 0)
+  {
+    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);
+    printf("[MEDIA] ERROR: FFmpeg conversion failed for media_id=%lld\n", (long long)ctx->media_id);
+    unlink(tmp_input);
+    unlink(tmp_output);
+    Deita_Connection_Close(db_conn);
+    free(ctx);
+    return NULL;
+  }
+
+  // Upload processed file to S3
+  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 *update_error =
+      "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?";
+    const char *error_params[] = { "Failed to upload processed file", media_id_str };
+    Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params);
+    printf("[MEDIA] ERROR: Failed to upload processed file for media_id=%lld\n", (long long)ctx->media_id);
+    unlink(tmp_input);
+    unlink(tmp_output);
+    Deita_Connection_Close(db_conn);
+    free(ctx);
+    return NULL;
+  }
+
+  // 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);
+
+  printf("[MEDIA] Successfully processed media_id=%lld\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;
+  }
+
+  // If content_type starts with "image/", spawn background processing thread
+  if (strncmp(content_type_copy, "image/", 6) == 0)
+  {
+    // 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);
+    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->s3_config = g_s3_config;
+
+    // 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: Failed to spawn processing thread for media_id=%lld\n", (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] Spawned processing thread 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_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_processed = Deita_Result_Set_Get_Text(p_result, 2);
+  const char *error_message = Deita_Result_Set_Get_Text(p_result, 3);
+
+  // Build CloudFront URL if status is 'finished' and s3_key_processed exists
+  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 JSON response
+  char *response_body = Dowa_Arena_Allocate(arena, 2048);
+  if (strlen(processed_url) > 0)
+  {
+    snprintf(response_body, 2048,
+             "{\"id\":%lld,\"status\":\"%s\",\"processed_url\":\"%s\",\"error_message\":%s}",
+             (long long)id, status, processed_url,
+             error_message ? "\"" : "null");
+    if (error_message)
+    {
+      // Append error message if exists
+      size_t len = strlen(response_body);
+      snprintf(response_body + len - 1, 2048 - len + 1, "%s\"}", error_message);
+    }
+  }
+  else
+  {
+    snprintf(response_body, 2048,
+             "{\"id\":%lld,\"status\":\"%s\",\"processed_url\":null,\"error_message\":%s}",
+             (long long)id, status,
+             error_message ? "\"" : "null");
+    if (error_message)
+    {
+      size_t len = strlen(response_body);
+      snprintf(response_body + len - 1, 2048 - len + 1, "%s\"}", error_message);
+    }
+  }
+
+  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
@@ -742,6 +1715,9 @@
   printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n",
          g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)");
 
+  // Initialize database
+  init_database();
+
   Seobeo_Router_Init();
 
   Seobeo_Router_Register("GET", "/", GetHomePage);
@@ -767,6 +1743,15 @@
   // -- 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);
@@ -775,8 +1760,21 @@
   Seobeo_Router_Register("GET", "/talk", GetTalk);
   Seobeo_Router_Register("GET", "/talk/index.html", GetRedirectTalk);
 
+  // -- Editor (legacy) --/
+  Seobeo_Router_Register("GET", "/editor", GetEditor);
+  Seobeo_Router_Register("GET", "/editor/index.html", GetRedirectEditor);
+
+  // -- 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);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/editor/index.html	Sun Feb 15 07:07:50 2026 -0800
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  {{/parts/base_head.html}}
+  <title>Editor | MrJuneJune</title>
+  <style>
+    .editor-page {
+      max-width: 900px;
+      margin: 0 auto;
+      padding: 20px;
+    }
+    .editor-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 20px;
+    }
+    .editor-header h1 {
+      margin: 0;
+    }
+    .doc-selector {
+      display: flex;
+      gap: 10px;
+      align-items: center;
+    }
+    .doc-selector input {
+      padding: 8px 12px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      font-size: 14px;
+    }
+    .doc-selector button {
+      padding: 8px 16px;
+      background: #0078ff;
+      color: white;
+      border: none;
+      border-radius: 4px;
+      cursor: pointer;
+    }
+    .doc-selector button:hover {
+      background: #0066dd;
+    }
+    .auth-section {
+      margin-bottom: 20px;
+      padding: 16px;
+      background: #f5f5f5;
+      border-radius: 4px;
+    }
+    .auth-section input {
+      width: 300px;
+      padding: 8px 12px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      font-size: 14px;
+    }
+    .auth-section label {
+      display: block;
+      margin-bottom: 8px;
+      font-weight: bold;
+    }
+  </style>
+</head>
+<body>
+  {{/parts/header.html}}
+
+  <main class="editor-page">
+    <div class="editor-header">
+      <h1>Rich Editor</h1>
+      <div class="doc-selector">
+        <input type="text" id="doc-id" placeholder="Document ID" value="default">
+        <button onclick="loadDocument()">Load</button>
+      </div>
+    </div>
+
+    <div class="auth-section">
+      <label for="auth-token">Auth Token:</label>
+      <input type="password" id="auth-token" placeholder="Enter your auth token">
+    </div>
+
+    <div id="editor-container"></div>
+  </main>
+
+  {{/parts/footer.html}}
+
+  <script src="/public/js/rich_editor.js"></script>
+  <script>
+    let editor = null;
+
+    function getAuthToken() {
+      return document.getElementById('auth-token').value;
+    }
+
+    function getDocId() {
+      return document.getElementById('doc-id').value || 'default';
+    }
+
+    async function uploadFile(file) {
+      const token = getAuthToken();
+      if (!token) {
+        alert('Please enter your auth token');
+        throw new Error('No auth token');
+      }
+
+      // Get presigned upload URL
+      const response = await fetch('/api/s3/upload-url', {
+        method: 'POST',
+        headers: {
+          'Authorization': 'Bearer ' + token,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          filename: file.name,
+          content_type: file.type
+        })
+      });
+
+      if (!response.ok) {
+        const error = await response.json();
+        throw new Error(error.error || 'Failed to get upload URL');
+      }
+
+      const data = await response.json();
+
+      // 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');
+      }
+
+      return {
+        url: data.public_url,
+        key: data.key
+      };
+    }
+
+    async function saveContent(content) {
+      const token = getAuthToken();
+      if (!token) {
+        console.warn('No auth token, skipping save');
+        return;
+      }
+
+      const response = await fetch('/api/editor/save', {
+        method: 'POST',
+        headers: {
+          'Authorization': 'Bearer ' + token,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          doc_id: getDocId(),
+          content: content
+        })
+      });
+
+      if (!response.ok) {
+        const error = await response.json();
+        throw new Error(error.error || 'Failed to save');
+      }
+    }
+
+    async function loadDocument() {
+      const token = getAuthToken();
+      if (!token) {
+        alert('Please enter your auth token');
+        return;
+      }
+
+      const docId = getDocId();
+
+      try {
+        const response = await fetch('/api/editor/load/' + encodeURIComponent(docId), {
+          headers: {
+            'Authorization': 'Bearer ' + token
+          }
+        });
+
+        if (!response.ok) {
+          const error = await response.json();
+          alert('Error: ' + (error.error || 'Failed to load'));
+          return;
+        }
+
+        const data = await response.json();
+        editor.setContent(data.content || '');
+        console.log('Loaded document:', docId);
+      } catch (error) {
+        console.error('Load error:', error);
+        alert('Failed to load document');
+      }
+    }
+
+    // Initialize editor
+    document.addEventListener('DOMContentLoaded', function() {
+      editor = RichEditor.init('editor-container', {
+        uploadCallback: uploadFile,
+        saveCallback: saveContent,
+        debounceMs: 1500,
+        placeholder: 'Start writing... (paste images, use /upload for files)'
+      });
+
+      // Try to load saved token from localStorage
+      const savedToken = localStorage.getItem('editor-auth-token');
+      if (savedToken) {
+        document.getElementById('auth-token').value = savedToken;
+      }
+
+      // Save token to localStorage on change
+      document.getElementById('auth-token').addEventListener('change', function() {
+        localStorage.setItem('editor-auth-token', this.value);
+      });
+    });
+  </script>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/notes/editor.js	Sun Feb 15 07:07:50 2026 -0800
@@ -0,0 +1,204 @@
+console.log("june");
+
+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 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 createResp = 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 (!createResp.ok) {
+    const error = await createResp.json();
+    throw new Error(error.error || 'Failed to create media record');
+  }
+
+  const { media_id, upload_url } = await createResp.json();
+
+  // 2. Upload to S3
+  const uploadResp = await fetch(upload_url, {
+    method: 'PUT',
+    headers: { 'Content-Type': file.type },
+    body: file
+  });
+
+  if (!uploadResp.ok) {
+    throw new Error('S3 upload failed');
+  }
+
+  // 3. Mark uploaded
+  await fetch(`/api/media/${media_id}/uploaded`, {
+    method: 'POST',
+    headers: { 'Authorization': 'Bearer ' + token }
+  });
+
+  // 4. Poll for images, immediate return for non-images
+  if (file.type.startsWith('image/')) {
+    return await pollForProcessedImage(media_id);
+  } else {
+    // For non-images, return the original S3 URL
+    const s3_url = upload_url.split('?')[0];
+    return { url: s3_url };
+  }
+}
+
+async function pollForProcessedImage(mediaId) {
+  const token = getAuthToken();
+  const maxAttempts = 60; // 2 minutes max
+
+  for (let i = 0; i < maxAttempts; i++) {
+    await new Promise(r => setTimeout(r, 2000)); // 2 sec interval
+
+    const resp = await fetch(`/api/media/${mediaId}/status`, {
+      headers: { 'Authorization': 'Bearer ' + token }
+    });
+
+    if (!resp.ok) continue;
+
+    const { status, processed_url, error_message } = await resp.json();
+
+    if (status === 'finished') return { url: processed_url };
+    if (status === 'error') throw new Error(error_message || 'Processing failed');
+  }
+
+  throw new Error('Processing timeout');
+}
+
+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);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/notes/index.html	Sun Feb 15 07:07:50 2026 -0800
@@ -0,0 +1,331 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  {{/parts/base_head.html}}
+  <title>Notes</title>
+  <style>
+    .notes-page {
+      max-width: 900px;
+      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="showNewNoteDialog()" class="btn-primary">+ New Note</button>
+        <a href="/notes/" id="home-link">Home</a>
+        <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>
+
+    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 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');
+      }
+
+      //  Get s3 bucket URL
+      const response = await fetch('/api/s3/upload-url', {
+        method: 'POST',
+        headers: {
+          'Authorization': 'Bearer ' + token,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          filename: file.name,
+          content_type: file.type
+        })
+      });
+
+      if (!response.ok) {
+        const error = await response.json();
+        throw new Error(error.error || 'Failed to get upload URL');
+      }
+
+      const data = await response.json();
+
+      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');
+      }
+
+      return { url: data.public_url, key: data.key };
+    }
+
+    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);
+    });
+  </script>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/notes/login.html	Sun Feb 15 07:07:50 2026 -0800
@@ -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/rich_editor/BUILD	Sun Feb 15 07:07:50 2026 -0800
@@ -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 Feb 15 07:07:50 2026 -0800
@@ -0,0 +1,602 @@
+/**
+ * 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: 'H1', title: 'Heading 1' },
+      { cmd: 'h2', label: 'H2', title: 'Heading 2' },
+      { cmd: 'h3', label: 'H3', title: 'Heading 3' },
+      { cmd: 'p', label: 'ΒΆ', title: 'Paragraph' },
+      { cmd: 'ul', label: 'β€’', title: 'Bullet List' },
+      { cmd: 'ol', label: '1.', title: 'Numbered List' },
+      { cmd: 'separator' },
+      { cmd: 'bold', label: 'B', title: 'Bold' },
+      { cmd: 'italic', label: 'I', title: 'Italic' },
+      { cmd: 'separator' },
+      { cmd: 'link', label: 'πŸ”—', title: 'Insert Link' },
+      { cmd: 'notelink', label: 'πŸ“', title: 'Link to Note' },
+      { cmd: 'upload', label: 'πŸ“Ž', title: 'Upload File' }
+    ];
+
+    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';
+      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 createStyles() {
+    if (document.getElementById('rich-editor-styles')) return;
+
+    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;
+      }
+
+      .rich-editor-btn:hover {
+        background: #e9e9e9;
+      }
+
+      .rich-editor-btn:active {
+        background: #ddd;
+      }
+
+      .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;
+      }
+
+      .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;
+      }
+
+      .rich-editor-status {
+        padding: 4px 8px;
+        font-size: 12px;
+        color: #666;
+        background: #f9f9f9;
+        border-top: 1px solid #eee;
+      }
+
+      .rich-editor-file-input {
+        display: none;
+      }
+    `;
+    document.head.appendChild(style);
+  }
+
+  class Editor {
+    constructor(elementId, options) {
+      this.options = { ...DEFAULT_OPTIONS, ...options };
+      this.container = document.getElementById(elementId);
+
+      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;
+          }
+        }
+      });
+    }
+
+    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();
+    }
+
+    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;
+            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;
+      }
+
+      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();
+    }
+  }
+
+  return {
+    init: function(elementId, options) {
+      return new Editor(elementId, options);
+    }
+  };
+})();
+
+// Export for module systems
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = RichEditor;
+}
--- a/s3/s3_uploader.c	Sat Feb 14 16:32:24 2026 -0800
+++ b/s3/s3_uploader.c	Sun Feb 15 07:07:50 2026 -0800
@@ -388,40 +388,23 @@
   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);
 
-  if (content_type && strcmp(method, "PUT") == 0)
-  {
-    char *encoded_content_type = s3__uri_encode_strict(content_type, p_arena);
-    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"
-             "&x-amz-content-type=%s",
-             S3_ALGORITHM,
-             encoded_credential,
-             ts.datetime,
-             expires_str,
-             signed_headers,
-             encoded_content_type);
-  }
-  else
-  {
-    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,
-             signed_headers);
-  }
+  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);