ADR-0023: Build-Time Client-Side Script Injection
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 Method | Model | outlet() Called? |
|---|---|---|
| REST API (non-streaming) | jorvis_cloud | ❌ NO |
| REST API (streaming) | jorvis_cloud | ❌ NO |
| Browser UI (Playwright) | jorvis_cloud | ❌ NO |
| REST API | gemma3: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
- Storage: Open WebUI User Settings API (
/api/v1/users/user/settings) - Namespace:
jorvis_extensions(isolated from Open WebUI settings) - Per-model config: Different settings per model (jorvis_cloud vs gemma3:12b)
- 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.jspattern becomes the standard
Migration Plan
| Phase | Action | Timeline |
|---|---|---|
| 1 | Create modular structure in deploy/jorvis-brand/static/modules/ | Week 1 |
| 2 | Implement config-loader.js with API integration | Week 1 |
| 3 | Migrate KPI cards (kpi-cards.js) | Week 1 |
| 4 | Migrate visualizations (visuals-toolkit.js) | Week 2 |
| 5 | Implement Admin UI (admin-ui.js) | Week 2 |
| 6 | Deactivate outlet-based filters | Week 3 |
| 7 | Delete deprecated filters after stable period | Week 4+ |
Related
- 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