Elonn Docs Platform Documentation

web

Status: Current implementation reference for the browser runtime. This page is not the runtime contract and is not design authority for new runtime dataset work.

web.elonn.local is the browser runtime for Elonn. Production is web.elonn.com.

Launch web runtime →

The web runtime is a single-page application. One PHP route renders the shell (templates/runtime.php); all navigation, surface management, and service interaction happen client-side in runtime.js. The server has no routing logic beyond /health and /ready. The runtime does not own identity, social, messages, calendar, maps, or surface state — it receives a world contract and renders from it.

The browser runtime is intentionally a demo of service continuity, not a claim that desktop web can match the Android field runtime. It consumes the same world, maps, social, time, and surface services while rendering a more suggestive field model.

The Runtime Contract is the normative, platform-neutral runtime contract. This page describes the current browser runtime and its migration gaps. Web does not define requirements for Android, iPad, XR, desktop, or future runtimes.

Presence identifier

web.elonn — the string recorded in a member's presence record while they are active in the browser runtime.

User experience model

A member opens the runtime and sees a spatial environment with two simultaneous layers:

The field fills the background — a 360-degree panoramic plane of geographic objects rendered from real nearby-place data. The member can pan left and right through a synthetic horizon landscape, with a north reference anchored into the panorama. The field is always present behind the carry layer.

The carry layer sits in front of the field and is where the member does most of their work. It consists of a center surface stack flanked by two side rails and bounded above and below by docks.

The experience is surface-centric: the member navigates by moving between surfaces (not pages), and surfaces retain their scroll position, selected object, and filter state across focus changes. The center stack shows the focused surface prominently with neighbors partially visible above and below. Dragging or flicking moves focus to the next surface. The lower dock shows one button per surface plus a Field toggle. The right rail shows service navigation for the focused surface, or field controls when Field is active.

Layout regions

The runtime has six named regions, all rendered simultaneously:

1. Field surface (.field-surface)

Full-screen background layer. Always rendered; always behind the carry layer. Contains:

  • .field-surface__panorama — synthetic horizon / landscape panorama for demo orientation context
  • .field-surface__north — a world-anchored north reference in the panorama
  • .field-surface__objects — DOM container for field objects positioned by projection math
  • .field-surface__viewport — the visible window into the panorama, controlled by panning
  • .field-surface__canvas — the full 4912px-wide panoramic canvas that shifts on pan

The member pans via pointer drag, mouse drag, keyboard (arrow keys), or device orientation (gyroscope). Panning is constrained to the panoramic width defined by field_width in the world contract.

The current implementation uses real nearby-place objects from Maps/World, but the visible horizon is still a demo landscape. If browser geolocation is unavailable, web falls back to the last stored location; if none exists yet, it falls back to the Dawson City demo area so the field still renders live markers.

2. Center surface stack (.surface-stack--carry)

The primary workspace. Renders the carry_primary stack from surface_runtime. Surfaces stack vertically with the focused surface centered, neighbors partially visible above and below.

Each surface is an absolutely positioned element transformed by its offset from focus:

  • translateY(offset × 82px), scale(1 − |offset| × 0.1)
  • Opacity: max(0.34, 1 − |offset| × 0.42)

Gesture model (pointer events):

  • Drag threshold: 0.18 × viewport height
  • Velocity threshold: ±0.65 px/ms
  • Exceeding either threshold snaps focus to the next or previous surface
  • Drag under 4% of viewport height with no velocity: tap, focuses that surface directly

3. Left rail (.context-panel--left)

Fixed, top: 156px to bottom: 132px, left: 18px, width min(320px, calc(50vw − 72px)).

Renders the left_context stack using the same surface stack controller as the center stack. Contains runtime-and-user surfaces: profile, account, settings, docs, system status, diagnostics, connected services, and help. These are available regardless of which carry surface has focus.

On mobile (≤760px), collapses to a 34px header strip. Toggle button applies is-left-rail-collapsed to <body>.

4. Right rail (.context-panel--right)

Same dimensions as the left rail, mirrored to the right edge.

Renders the right_context stack. Has two modes driven by activeWorkspaceKind:

  • Normal mode — service navigation for the focused carry surface. For Social, treats conversations, communities, and events as one service family, not separate entries. Rail items focus an existing carry surface for that service; they do not create new surfaces.
  • Field mode (activeWorkspaceKind === 'field') — field control cards starting with Places, then Gatherings, Communities, People. Controls filter the geographic objects visible in the field.

Mode switches when the member selects Field from the lower dock or returns to a carry surface.

5. Lower dock (.carry-dock)

Fixed, bottom: 6px, height 48px (38px on mobile). One button per surface in the carry_primary stack plus a Field toggle as the first item.

  • Selecting a surface button: exits field workspace, expands carry stack, focuses that surface.
  • Selecting Field: activates field mode, switches the right rail to field controls.
  • Active button carries .is-active.

Dock is fully dynamic — renderSurfaceDock() rebuilds it whenever the surface set or focus changes.

6. Upper dock (.top-dock)

Fixed, top: 6px, height 48px. Reserved for future global runtime signals: search, notifications, collaboration state, voice/video indicators, system alerts, XR controls. Currently rendered as blank placeholders. Keep it minimal.

World contract

The runtime authenticates through GET {world}/world/identity/me, then fetches GET {world}/world/session with credentials: 'include' on load.

Top-level keys:

Key Description
surface_runtime The primary payload: surfaces, stacks, commands, and viewports
field_runtime Field objects projected by bearing and distance
services.identity Member identity objects including .members array
services.social Social objects: conversations, events, communities, presence, etc.
services.maps Maps data including field width and nearby-place context
maps.field_width Panoramic width in abstract units (default: 4912)
layout Fallback layout data
contract Named runtime contract metadata
session Session-level metadata

surface_runtime structure:

{
  "surfaces": [...],
  "stacks": [...],
  "viewports": [...]
}

Stack object fields:

Field Values Description
stack_id carry_primary, left_context, right_context Canonical stack identifiers
stack_type carry_stack, service_nav_stack, field_controls_stack Stack role
region main, context Spatial region
display full, rail Render mode — controls transform scale and opacity
slot left, right, center Position in layout
orientation vertical Scroll/drag axis
focused_surface_id string Which surface has focus on load
surface_order string[] Ordered surface IDs

Surface object fields:

Field Description
surface_id Unique identifier
surface_type e.g. field_controls_card, service_nav_panel
title Display name used in dock and stack bar
service_binding {service_id, resource_type, resource_id}
content_source {kind, url} — kind is inline or runtime_panel
runtime_state State data restored by the runtime on focus
placement {mode, stack_id}

Surface stack controller

One controller (createSurfaceStackController()) is instantiated for every stack: center carry, left rail, right rail. It is not stack-specific — the same code drives all three.

Returned interface:

{
  render(),        // mount surfaces to DOM, attach gesture listeners
  focus(id),       // change focused surface by surface_id
  currentSurface() // returns the currently focused surface object
  destroy()        // remove listeners, clean up
}

State tracked per controller: focused surface ID, drag start position, drag velocity, pointer capture ID, mounted DOM nodes.

Reuse this controller for any new stack type — siderail stacks, field stacks, modal stacks.

Field layer

Rendered as DOM, not canvas or WebGL. Each geographic object is an absolutely positioned .field-object element within .field-surface__objects.

Runtime Contract data source: field_runtime.objects[] in GET {world}/world/session.

The browser refreshes field controls and objects through GET {world}/world/session with location and filter inputs. It does not consume raw provider objects or classify provider tags.

The web field keeps two related values in shell._fieldState:

  • headingDegrees — heading from browser device orientation when available
  • headingOffsetDegrees — member-controlled panorama offset from drag/keyboard input

The visible field heading is the sum of those two values. Markers, the north tick, and the synthetic horizon all share that same world model.

Projection — bearing to panoramic X coordinate:

northMeters  = (objectLat − userLat) × 111132
eastMeters   = (objectLon − userLon) × 111320 × cos(userLat × π/180)
distance     = sqrt(north² + east²)
bearing      = atan2(east, north) × 180/π
projectedX   = ((bearing + 180) % 360) / 360 × fieldWidth
projectedY   = clamp(120 + distance × 0.28, 84, 520)

The panorama offset is then computed from the current field heading so browser panning behaves like a 360-degree world turn, not a decorative canvas drag.

Field object DOM structure:

.field-object
  .field-object__target   (44px circle, dot center)
  .field-object__stem     (18px horizontal connector)
  .field-object__label
    .field-object__eyebrow  (category: coffee, food, parks…)
    .field-object__title
    .field-object__subtitle (label + distance)

Places filter: filterGeoObjectsForPlaces() filters the full geo object list by the categories active in fieldControlState.placesCategories. Category is derived from OSM tags (amenity, shop, leisure, etc.). Toggling a category on the Places card re-runs the filter without re-fetching.

Location fallback order:

  1. Live browser geolocation
  2. Last successful location stored in localStorage
  3. Dawson City / Yukon demo coordinates for first-run fallback

Panel loading

Some surfaces expose service-owned content via content_source.kind === 'runtime_panel' and content_source.url. The runtime fetches the URL as JSON with credentials: 'include' and renders the result by kind (social, messages, time, find). There are no iframes.

Panel URLs are constructed by world and relayed through content_source.url on a surface object. The runtime does not construct panel URLs independently.

Flow:

  1. Surface with content_source.kind === 'runtime_panel' comes into focus
  2. loadRuntimePanel(url, title) fetches JSON from the panel URL
  3. 401 → redirect to login; other errors → show unavailable message
  4. renderRuntimePanelPayload(kind, payload) dispatches to the appropriate render function
  5. Rendered HTML is injected into .carry-panel__body

Examples:

Service Panel URL pattern
Social /world/panels/social?view=conversations
Messages /world/panels/messages
Time /world/panels/time
Find /world/panels/find

Inspector and rail panels are stack items within siderails, not primary workspace surfaces. A surface needs a runtime panel content source only when it requires identity, placement, sharing, or persistence that the host service must own.

Surface state persistence

Current implementation: surface.elonn.local is the source of truth for the surface layer. It is upstream of world, and world reads from surface to build the surface_runtime portion of the current compatibility contract. This describes current implementation reality, not the target runtime dataset architecture.

What surface.elonn.local owns:

  • Surface records — what surfaces exist, their type, service binding, content source, and permissions
  • Stack records — the three canonical stacks (carry_primary, left_context, right_context) with their focus state and ordered membership
  • Surface placement metadata — placement mode (stacked, docked, detached, field-anchored) per surface
  • Surface sessions — a session token per surface tracking its lifecycle

How it fits into the request flow:

surface.elonn.local  ←→  world.elonn.local  →  web runtime
       ↑                                              |
       └──────────────────────────────────────────────┘
                   (focus / reorder writes)
  1. World fetches GET /runtime from surface to get the current surface_runtime payload
  2. World embeds that payload in the /world/session response
  3. The web runtime renders the carry layer from surface_runtime
  4. When focus changes or surfaces are reordered, the runtime posts to World-owned surface command URLs and World relays the write back to Surface with trusted member context.

What is not yet persisted: Granular surface state — scroll position, selected object, active filters — is currently held in memory (surfacePayloadCache) and lost on reload. Field pan position and heading are kept in shell._fieldState; browser location fallback is kept in localStorage. These will eventually be written to surface_runtime on surface state change and restored on mount, making the carry layer feel stateful across sessions.

Session startup

  1. authenticateRuntime()GET {world}/world/identity/me with credentials: 'include'
    • 401 → redirect to elonn.com/account/login?return_to={current_url}
    • Other error → renderRuntimeUnavailable()
  2. loadRuntime()GET {world}/world/session
  3. renderWorld() — extract surface_runtime, field_runtime, services, and compatibility maps data from the contract
  4. renderSurfaceRuntime() — instantiate surface stack controllers for carry_primary, left_context, right_context
  5. renderSurfaceDock() — build lower dock buttons from carry_primary surface order
  6. renderWorkspaceRightRail() — build right rail service nav from focused carry surface
  7. renderField() — request geolocation, fetch nearby geo objects, project and render field objects
  8. Left rail renders runtime/user panel stack from left_context

JavaScript architecture

All client-side logic lives in a single IIFE in public/assets/js/runtime.js (~5600 lines). There is no module system, no build step, no framework. Key sections:

Responsibility Key functions
Auth & startup authenticateRuntime(), loadRuntime(), renderWorld()
Surface stack createSurfaceStackController(), renderSurfaceRuntime()
Field renderField(), renderGeographicField(), filterGeoObjectsForPlaces(), attachFieldControls()
Panels loadRuntimePanel(), renderRuntimePanelPayload()
Dock renderSurfaceDock()
Rails renderWorkspaceRightRail(), renderFieldControlsStack()
Forms handleRuntimeSubmit() — routes all form submissions by action
Social writes Create/update conversation, community, event; reply, invite
Utilities fetchJson(), resolveRuntimeUrl(), replaceChildren()

Global state variables: currentIdentity, latestRuntimeContext, latestCarrySurfaces, focusedCarrySurfaceId, activeWorkspaceKind, surfaceStackControllers (map of stack ID → controller), surfacePayloadCache, latestSocialObjects, latestGeoObjects, latestGeoLocation.

CSS architecture

One stylesheet: public/assets/css/runtime.css (~1430 lines). Dark theme; color palette:

  • Background: #06090a
  • Primary text: #eefaf8
  • Muted text: #d0ddd5
  • Accent: #d7fff3, #b8fff3
  • Borders: rgba(215, 255, 243, 0.18–0.76)

CSS custom properties:

--runtime-dock-gap: 6px        /* 4px on mobile */
--runtime-dock-height: 48px    /* 38px on mobile */
--runtime-rail-header-height: 34px
--runtime-carry-clearance: 28px

Key state classes on <body>:

Class Effect
is-left-rail-collapsed Left rail collapses to 34px header
is-right-rail-collapsed Right rail collapses to 34px header
is-panning Field surface shows grab cursor

Notes for additional runtimes

Any runtime implementing the Elonn world model must:

  1. Authenticate through world.elonn.com/world/identity/me — cookie or bearer token.
  2. Fetch the world contract from world.elonn.com/world/session — the contract is the source of truth for surfaces, stacks, field data, and service objects.
  3. Render the three canonical stacks by stack_id: carry_primary (center workspace), left_context (runtime/user rail), right_context (service nav or field controls rail).
  4. Implement a reusable stack controller — focus index, drag/gesture handling, transform layout, mounted node lifecycle. The same controller should drive all stack types.
  5. Render the field layer from field_runtime.objects[] using the bearing projection model — or a runtime-appropriate equivalent (ARCore anchors on Android, SceneKit/RealityKit on iPadOS).
  6. Load panels via content_source.url when content_source.kind === 'runtime_panel' — fetch JSON, render by kind. Do not embed panel logic in the runtime.
  7. Report runtime in presence — use a distinct identifier per platform (web.elonn, android.elonn, ipad.elonn, etc.).
  8. Route all service writes through world — runtimes do not call social, messages, maps, time, or surface directly.

Canonical terms

  • web — the browser runtime service
  • runtime — the execution environment
  • carry — user-following layer the web runtime renders
  • field — spatial layer the web runtime renders
  • panel — service-owned content loaded inside a surface
  • surface — contextual presentation layer within a stack
  • stack — ordered container for surfaces with one focus surface
  • dock — attached horizontal region following the runtime viewport
  • finding — discovered or computed result delivered to a surface