LemonWeb

July 2, 2026 ยท View on GitHub

Phoenix web interface for the Lemon platform. Provides a real-time dashboard for interacting with Lemon agents via LiveView and optional token-based access control.

Architecture Overview

LemonWeb is a Phoenix 1.7 application inside an Elixir umbrella project. It uses Phoenix LiveView for interactive pages, Bandit as the HTTP server, vendored Phoenix JavaScript from umbrella dependencies, and Tailwind CSS loaded from CDN for styling.

OTP Supervision Tree

LemonWeb.Application (:one_for_one)
  |-- LemonWeb.Telemetry     (telemetry supervisor)
  |-- LemonWeb.Endpoint       (Bandit HTTP + LiveView WebSocket)

Request Pipeline

HTTP Request
  |
  v
LemonWeb.Endpoint
  |-- Plug.Static          (serves /assets, favicon.ico, robots.txt)
  |-- Phoenix.CodeReloader (dev only)
  |-- Phoenix.LiveReloader (dev only)
  |-- Plug.RequestId
  |-- Plug.Telemetry
  |-- Plug.Parsers         (urlencoded, multipart, JSON)
  |-- Plug.MethodOverride
  |-- Plug.Head
  |-- Plug.Session         (cookie store, key: "_lemon_web_key")
  |-- LemonWeb.Router

WebSocket

A single LiveView socket is mounted at /live with cookie-based session info:

socket "/live", Phoenix.LiveView.Socket,
  websocket: [connect_info: [session: @session_options]],
  longpoll: false

All LiveView pages communicate over this socket. There are no custom Phoenix Channels.

Route Inventory

Authenticated Browser Pipeline (:browser)

Includes RequireAccessToken plug. When LEMON_WEB_ACCESS_TOKEN is set, requests must present a valid token.

PathLiveViewActionDescription
/SessionLive:indexDashboard home; generates an isolated session key per browser tab
/sessions/:session_keySessionLive:showDashboard bound to a specific session key

Query Parameters

  • /?agent_id=<id> -- Sets the agent for the auto-generated session (default: "default")
  • /?token=<token> -- Authenticates the request (stripped from URL by client-side JS after consumption)
  • /sessions/:session_key?token=<token> -- Same token authentication for named sessions

LiveView Pages

SessionLive (/, /sessions/:session_key)

The primary dashboard page. Provides a chat-style interface for sending prompts to Lemon agents and receiving streaming responses.

Features:

  • Real-time streaming of assistant responses via PubSub deltas
  • Multi-file upload (up to 5 files, 20 MB each) with progress tracking and cancellation
  • Tool call visualization in collapsible detail panels
  • System notifications for run lifecycle events (started, completed, failed)
  • Message history capped at 250 messages

Session key resolution:

  1. If params["session_key"] is present and valid, use it directly
  2. Otherwise, generate an isolated key: agent:<agent_id>:web:browser:unknown:tab-<random>
  3. Client-side JS in app.js also generates a stable per-tab session key stored in sessionStorage

PubSub events handled:

  • :run_started -- Displays "Run started" system message
  • :delta -- Streams text into the current assistant message bubble
  • :engine_action -- Renders a tool call detail panel
  • :run_completed -- Finalizes the assistant message; shows error if the run failed

Submission flow:

  1. User enters prompt and/or uploads files
  2. Files are persisted to the uploads directory with timestamped names
  3. Prompt is enriched with file paths and submitted via LemonRouter.submit/1
  4. Response streams back through PubSub events

Components

LiveView Components (under lib/lemon_web/live/components/)

ComponentModulePurpose
MessageComponentLemonWeb.Live.Components.MessageComponentRenders chat bubbles for user, assistant, system, and tool call messages. Delegates tool calls to ToolCallComponent.
FileUploadComponentLemonWeb.Live.Components.FileUploadComponentDrag-and-drop file upload area with per-file progress bars, error messages, and cancel buttons.
ToolCallComponentLemonWeb.Live.Components.ToolCallComponentCollapsible <details> panel showing tool name, phase, detail payload (as formatted JSON), status, and optional message. Auto-opens when phase is started or updated.

Core Components (lib/lemon_web/components/core_components.ex)

Shared function components auto-imported into all LiveViews:

ComponentDescription
<.button>Slate-900 rounded button with hover and disabled states
<.input>Text input with focus ring styling
<.flash_group>Renders flash messages as colored banners (error: rose, success: emerald)

Layouts (lib/lemon_web/components/layouts/)

  • root.html.heex -- HTML shell with <head> (meta, CSRF token, Tailwind CDN, app.js), renders @inner_content
  • app.html.heex -- Passthrough layout, renders @inner_content directly

Static Assets and Frontend

LemonWeb uses a small static frontend strategy with no local build tools (no esbuild, no Node.js):

  • Tailwind CSS: Loaded from https://cdn.tailwindcss.com in the root layout
  • Phoenix JS: Vendored from deps/phoenix/priv/static/phoenix.mjs into priv/static/assets/vendor/phoenix.mjs
  • Phoenix LiveView JS: Vendored from deps/phoenix_live_view/priv/static/phoenix_live_view.esm.js into priv/static/assets/vendor/phoenix_live_view.esm.js
  • app.js (priv/static/assets/app.js): Client-side entry point that initializes the LiveSocket, generates stable per-tab session keys via sessionStorage, normalizes agent IDs, and strips token params from the URL after authentication

Static files are served by Plug.Static at / for paths matching ~w(assets favicon.ico robots.txt).

Authentication

Authentication is handled by the LemonWeb.Plugs.RequireAccessToken plug, which is optional and only active when a token is configured.

Behavior:

  1. If config :lemon_web, :access_token is nil or "", all requests pass through (no gate)
  2. When a token is configured, it is checked from three sources (in order):
    • Authorization: Bearer <token> header
    • ?token=<token> query parameter
    • Session marker (SHA256 hash stored in cookie under :lemon_web_auth)
  3. On valid token: a SHA256 hash is stored in the session so subsequent requests skip the token check
  4. On invalid or missing token: responds with 401 Unauthorized and halts

Token comparison uses constant-time comparison via Plug.Crypto.secure_compare/2.

File Uploads

Configured in SessionLive.mount/3:

SettingValue
Accepted typesAny (:any)
Max entries per submission5
Max file size20 MB
Upload modeAuto-upload (auto_upload: true)

Upload directory: Configured via config :lemon_web, :uploads_dir or defaults to System.tmp_dir!/0 <> "/lemon_web_uploads". Files are named {timestamp_ms}-{unique_id}-{sanitized_filename}.

Configuration

Environment Variables

VariablePurposeDefault
LEMON_WEB_ACCESS_TOKENDashboard access tokennil (no auth)
LEMON_WEB_UPLOADS_DIRDirectory for uploaded filesSystem.tmp_dir!/0 <> "/lemon_web_uploads"
LEMON_WEB_HOSTProduction hostname"localhost"
LEMON_WEB_PORTHTTP port for unified runtime and production4080
LEMON_WEB_SECRET_KEY_BASEProduction secret key (required in prod)--
PHX_SERVERSet to "1" or "true" to start the HTTP server in prod--

Application Config

# config/config.exs
config :lemon_web, LemonWeb.Endpoint,
  url: [host: "localhost"],
  render_errors: [formats: [html: LemonWeb.ErrorHTML, json: LemonWeb.ErrorJSON], layout: false],
  pubsub_server: LemonCore.PubSub,
  live_view: [signing_salt: "lemonwebsigningsalt"]

config :lemon_web, :access_token, nil
config :lemon_web, :uploads_dir, Path.join(System.tmp_dir!(), "lemon_web_uploads")

Per-Environment Defaults

EnvironmentPortServerSecret Key
dev (mix phx.server)4080 (127.0.0.1)Phoenix server taskHardcoded dev key
dev (bin/lemon)LEMON_WEB_PORT / --web-port (127.0.0.1)Enabled by runtime bootHardcoded dev key
test4082 (127.0.0.1)server: falseHardcoded test key
prodLEMON_WEB_PORT (0.0.0.0)Enabled via PHX_SERVERLEMON_WEB_SECRET_KEY_BASE

Error Handling

  • LemonWeb.ErrorHTML -- Renders HTML error pages from error_html/ templates (404: "Page not found", 500: "Something went wrong")
  • LemonWeb.ErrorJSON -- Returns JSON error responses using Phoenix status message mapping: %{errors: %{detail: "..."}}

Dependencies

Umbrella Dependencies

AppPurpose
lemon_corePubSub (LemonCore.Bus), session keys (LemonCore.SessionKey), events (LemonCore.Event), map helpers
lemon_routerRequest routing (LemonRouter.submit/1) for submitting prompts to agents

External Dependencies

PackageVersionPurpose
phoenix~> 1.7.14Web framework
phoenix_live_view~> 1.0Real-time interactive UI
phoenix_html~> 4.1HTML helpers and form builders
phoenix_live_reload~> 1.5Dev-only live reload
bandit~> 1.5HTTP server (replaces Cowboy)
jason~> 1.4JSON encoding/decoding
gettext~> 0.26Internationalization
lazy_html>= 0.1.0Test-only HTML parsing

Running

# Start the entire umbrella (includes lemon_web)
mix phx.server

# Or start with an interactive shell
iex -S mix phx.server

# Run lemon_web tests only
mix test apps/lemon_web

# Access the dashboard
open http://localhost:4080

File Organization

apps/lemon_web/
|-- mix.exs
|-- lib/
|   |-- lemon_web.ex                              # Module macros (:live_view, :router, :html, etc.)
|   |-- lemon_web/
|       |-- application.ex                         # OTP application supervisor
|       |-- endpoint.ex                            # Phoenix endpoint (Bandit, sessions, static)
|       |-- router.ex                              # Route definitions and pipelines
|       |-- telemetry.ex                           # Telemetry supervisor
|       |-- gettext.ex                             # i18n backend
|       |-- plugs/
|       |   |-- require_access_token.ex            # Optional token authentication plug
|       |-- live/
|       |   |-- session_live.ex                    # Main dashboard LiveView
|       |   |-- components/
|       |       |-- file_upload_component.ex        # File upload UI
|       |       |-- message_component.ex            # Chat message bubbles
|       |       |-- tool_call_component.ex          # Tool call detail panels
|       |-- components/
|       |   |-- core_components.ex                 # Shared button, input, flash components
|       |   |-- layouts.ex                         # Layout module (embeds templates)
|       |   |-- layouts/
|       |       |-- root.html.heex                 # HTML document shell
|       |       |-- app.html.heex                  # App layout (passthrough)
|       |-- controllers/
|           |-- error_html.ex                      # HTML error renderer
|           |-- error_json.ex                      # JSON error renderer
|           |-- error_html/
|               |-- 404.html.heex                  # Not found page
|               |-- 500.html.heex                  # Server error page
|-- priv/
|   |-- static/
|   |   |-- assets/
|   |       |-- app.js                             # Client-side JS (LiveSocket init, session keys)
|   |-- gettext/
|       |-- .keep
|-- test/
    |-- test_helper.exs
    |-- lemon_web_test.exs                         # Smoke tests (app starts, modules load)
    |-- lemon_web/
        |-- live/
            |-- games_live_test.exs                # LiveView integration tests for games pages