ADR-0023: Build-Time Client-Side Script Injection

Status: Accepted
Date: 2026-01-26
Author: Architect (Rovo3)
Supersedes: N/A

Context

Open WebUI provides a Filter mechanism with outlet() functions that can inject HTML/JS into chat responses. However, our investigation revealed that outlet() is NOT called for external/pipe models like jorvis_cloud.

Problem Statement

  • Jorvis primary model (jorvis_cloud) is registered as an external model in Open WebUI
  • Filter outlet() functions work for local Ollama models but NOT for external models
  • All our UX enhancement filters (KPI cards, visualizations, UI customizations) rely on outlet()
  • This means critical features like ECharts rendering and Smart Infographic do not work

Evidence

Test MethodModeloutlet() Called?
REST API (non-streaming)jorvis_cloud❌ NO
REST API (streaming)jorvis_cloud❌ NO
Browser UI (Playwright)jorvis_cloud❌ NO
REST APIgemma3:12b (local)❌ NO

Working Example

ios_audio_fix and pwa_install_overlay work because they are injected at Docker build time via pwa-cta.js, not through the outlet() mechanism.

Decision

Adopt Build-Time Client-Side Script Injection as the primary mechanism for UI enhancements in Jorvis.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     BUILD TIME (Docker)                          │
├─────────────────────────────────────────────────────────────────┤
│  Dockerfile                                                      │
│    └── COPY deploy/jorvis-brand/static/modules/* → /app/static/ │
│    └── sed inject <script src="jorvis-bundle.js">               │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                     RUNTIME (Browser)                            │
├─────────────────────────────────────────────────────────────────┤
│  jorvis-bundle.js                                                │
│    ├── config-loader.js      ← Config from Open WebUI API       │
│    ├── admin-ui.js           ← Settings panel (Admin only)      │
│    ├── kpi-cards.js          ← KPI rendering                    │
│    ├── visuals-toolkit.js    ← ECharts + Mermaid                │
│    └── ui-customizer.js      ← UI tweaks                        │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                     STORAGE (Open WebUI API)                     │
├─────────────────────────────────────────────────────────────────┤
│  /api/v1/users/user/settings                                     │
│  { "jorvis_extensions": { "models": { ... } } }                  │
└─────────────────────────────────────────────────────────────────┘

Modular File Structure

deploy/jorvis-brand/static/
├── modules/
│   ├── config-loader.js       # Configuration management
│   ├── admin-ui.js            # Settings panel
│   ├── kpi-cards.js           # Smart Infographic
│   ├── visuals-toolkit.js     # ECharts + Mermaid
│   ├── ui-customizer.js       # UI element hiding
│   └── pwa-cta.js             # PWA Install Banner
├── jorvis-bundle.js           # Concatenated bundle
└── styles/
    └── jorvis-ui.css          # Shared styles

Configuration Management

  1. Storage: Open WebUI User Settings API (/api/v1/users/user/settings)
  2. Namespace: jorvis_extensions (isolated from Open WebUI settings)
  3. Per-model config: Different settings per model (jorvis_cloud vs gemma3:12b)
  4. Hot reload: Hybrid approach (instant for toggles, refresh for structural changes)

Admin UI

  • Location: Injected into Open WebUI Settings tab
  • Fallback: Floating panel if Settings injection fails
  • Access: Admin users only

Model Detection

  • Primary: localStorage.getItem('selectedModel')
  • Authoritative: Intercept fetch() calls to /api/chat/completions
  • Fallback: MutationObserver on DOM

Consequences

Positive

  • ✅ Works for ALL models (external, local, pipes)
  • ✅ Single mechanism, easier to maintain
  • ✅ Full control over injection timing
  • ✅ Per-user and per-model configuration
  • ✅ Hot reload for most settings

Negative

  • ❌ Requires Docker image rebuild for code changes
  • ❌ More complex build process
  • ❌ Admin UI must be maintained separately from Open WebUI

Neutral

  • Outlet-based filters will be deprecated (except text-modifying ones like markdown_normalizer)
  • Existing pwa-cta.js pattern becomes the standard

Migration Plan

PhaseActionTimeline
1Create modular structure in deploy/jorvis-brand/static/modules/Week 1
2Implement config-loader.js with API integrationWeek 1
3Migrate KPI cards (kpi-cards.js)Week 1
4Migrate visualizations (visuals-toolkit.js)Week 2
5Implement Admin UI (admin-ui.js)Week 2
6Deactivate outlet-based filtersWeek 3
7Delete deprecated filters after stable periodWeek 4+
  • UX-P3: Visuals Toolkit (ECharts rendering) — blocked by this issue
  • UX-P4: Smart Infographic (KPI cards) — blocked by this issue
  • ADR-0022: Documentation Stack
  • TASK_BOARD: Phase UX tasks

References

  • Open WebUI Filter documentation
  • deploy/jorvis-brand/static/pwa-cta.js — working example of build-time injection
  • Playwright test evidence showing outlet() not called