Skip to content

feat: Multi-View Dashboard Composition — tabbed views from a single config #33

@alohays

Description

@alohays

Problem

monitor-forge currently renders a fixed 2-pane layout: map on the left, single sidebar (380px) on the right. All panels stack vertically in one scrollable sidebar. There is no way to organize panels into logical groups or switch between different "views" of the same data.

Current limitations:

  1. Flat panel array: panels[] in config has a position field for ordering, but all panels render into the same <aside class="forge-sidebar"> container
  2. Single initialization path: App.ts creates one PanelManager that manages all panels in one container
  3. No layout flexibility: CSS has --panel-width: 380px hardcoded, no grid/multi-column support
  4. No view switching: No tabs, no dropdown, no URL-based view routing

Real-world pain: A geopolitics dashboard might need an "Overview" view (news + instability index), an "Intelligence" view (AI brief + entity tracker), and a "Sources" view (service status + detailed feeds). Today, all 5-6 panels stack into one long sidebar, forcing users to scroll constantly.

Competitor comparison: worldmonitor solves this with VITE_VARIANT — 5 separate dashboard builds from one codebase. But this requires separate build & deploy per variant. monitor-forge can do better: multiple views within a single deployment, switchable at runtime, defined in config.

Solution

1. Schema Extension: views Array

Add an optional views field to the config schema:

// forge/src/config/schema.ts
const ViewSchema = z.object({
  name: z.string().regex(/^[a-z0-9-]+$/),
  displayName: z.string(),
  panels: z.array(z.string()),       // panel names to include
  icon: z.string().optional(),        // emoji or icon identifier
  default: z.boolean().optional(),    // first view to show
});

// In MonitorForgeConfigSchema:
views: z.array(ViewSchema).optional(),

Config example:

{
  "panels": [
    { "name": "news", "type": "news-feed", ... },
    { "name": "ai-brief", "type": "ai-brief", ... },
    { "name": "entities", "type": "entity-tracker", ... },
    { "name": "instability", "type": "instability-index", ... },
    { "name": "health", "type": "service-status", ... }
  ],
  "views": [
    {
      "name": "overview",
      "displayName": "Overview",
      "panels": ["news", "instability"],
      "default": true
    },
    {
      "name": "intelligence",
      "displayName": "Intelligence",
      "panels": ["ai-brief", "entities"]
    },
    {
      "name": "ops",
      "displayName": "Operations",
      "panels": ["health"]
    }
  ]
}

2. View Switcher UI

Add a tab bar or pill selector in the header area:

┌──────────────────────────────────────────────────────┐
│  🌐 Korean Tech Watch       [Overview] [Intel] [Ops] │
├────────────────────────────────┬─────────────────────┤
│                                │  ┌─ News Feed ────┐ │
│           MAP                  │  │ ...             │ │
│                                │  └─────────────────┘ │
│                                │  ┌─ Instability ──┐ │
│                                │  │ ...             │ │
│                                │  └─────────────────┘ │
└────────────────────────────────┴─────────────────────┘
  • Tabs render in <header class="forge-header"> alongside existing forge-header-actions
  • Active view highlighted with --accent color
  • View switching is instant — panels are pre-rendered but hidden via CSS display: none
  • Keyboard shortcut: 1/2/3 to switch views

3. PanelManager Refactor

// Current: PanelManager renders all panels into sidebar
// New: PanelManager assigns panels to view containers

class PanelManager {
  private views: Map<string, HTMLElement>;  // view name → container

  initializeViews(viewConfigs: ViewConfig[]) {
    for (const view of viewConfigs) {
      const container = document.createElement('div');
      container.className = 'forge-view';
      container.dataset.view = view.name;
      // Only show default view initially
      container.style.display = view.default ? 'flex' : 'none';
      this.sidebar.appendChild(container);
      this.views.set(view.name, container);
    }
  }

  switchView(viewName: string) {
    this.views.forEach((el, name) => {
      el.style.display = name === viewName ? 'flex' : 'none';
    });
    // Update URL hash
    history.replaceState(null, '', `#view=${viewName}`);
  }
}

4. Backward Compatibility

  • If views is omitted from config → all panels render in sidebar as before (single implicit "main" view)
  • No breaking changes to existing configs
  • position field still respected within each view

5. URL Hash Routing

6. CLI Commands

# Add a new view
forge view add intelligence --display-name "Intelligence" --panels ai-brief,entities

# List views
forge view list
# ┌────────────────┬─────────────────┬──────────────────────┬─────────┐
# │ Name           │ Display Name    │ Panels               │ Default │
# ├────────────────┼─────────────────┼──────────────────────┼─────────┤
# │ overview       │ Overview        │ news, instability    │ ✓       │
# │ intelligence   │ Intelligence    │ ai-brief, entities   │         │
# │ ops            │ Operations      │ health               │         │
# └────────────────┴─────────────────┴──────────────────────┴─────────┘

# Remove a view (panels are NOT deleted, just unassigned)
forge view remove ops

# Set default view
forge view set-default intelligence

Implementation Notes

Files to modify:

  • forge/src/config/schema.ts — add ViewSchema and views field
  • forge/src/commands/ — add view.ts command (add/remove/list/set-default)
  • src/App.ts — initialize views, wire view switcher
  • src/core/panels/PanelManager.ts — view container management, switchView()
  • src/styles/base.css — view tab styles, .forge-view container

Files to create:

  • forge/src/commands/view.ts — CLI command implementation

Validation rules:

  • Every panel name in views[].panels must exist in panels[]
  • A panel can appear in multiple views (useful for "health" panel in every view)
  • At most one view can have default: true
  • View names must be unique

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestfrontendFrontend engine changeshigh-impactHigh-impact feature for project growthpriority: mediumShould fix, but not urgent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions