A demo-ready dashboard showcasing how managed service customers can monitor their subscribed IT services. Built around the Zero Outage strategy (99.999% availability target) and the 3P pillars: People, Processes, Platforms. Glasspane provides AI-powered insights alongside real-time operational metrics in a unified transparency portal.
Your End-to-End Digital Health Dashboard
This MVP uses mock data to demonstrate the look, feel, and interaction model of a widget-driven, role-based transparency portal with AI-powered insights.
⚠️ Security NoticeThe default configuration is UNAUTHENTICATED. The
/api/ai/*routes accept acustomerIdfrom the request body without verifying caller identity. This is acceptable for the public demo (which uses mock data only), but must not be deployed with real customer data without enabling authentication.See the Security section below before deploying with real data.
npm install --legacy-peer-deps
npm run devOpen http://localhost:3000 — redirects to the C-Level dashboard view.
Note:
--legacy-peer-depsis required because Tremor v3 declares a peer dependency on React 18, while this project uses React 19 / Next.js 16. Tremor works fine at runtime.
The AI routes under app/api/ai/* are provider-agnostic. The current runtime supports:
mockwatsonxopenrouterlm-studioollamaopenaianthropicgeminibedrock
Current model-backed tasks:
summarychatinsightsrisk-briefingsla-riskcost-forecastcapacity-plannerroot-cause-patternschange-impact
You can route each task independently:
AI_PROVIDER=mock
AI_FALLBACKS=mock
AI_SUMMARY_PROVIDER=openrouter
AI_SUMMARY_MODEL=openai/gpt-4o-mini
AI_CHAT_PROVIDER=anthropic
AI_CHAT_MODEL=claude-3-5-sonnet-latest
AI_CHAT_FALLBACKS=openai,mock
AI_INSIGHTS_PROVIDER=lm-studio
AI_INSIGHTS_MODEL=qwen2.5-7b-instruct
AI_INSIGHTS_FALLBACKS=ollama,mock
AI_RISK_BRIEFING_PROVIDER=openai
AI_RISK_BRIEFING_MODEL=gpt-4o-mini
AI_RISK_BRIEFING_FALLBACKS=anthropic,mock
AI_SLA_RISK_PROVIDER=openrouter
AI_SLA_RISK_MODEL=openai/gpt-4o-mini
AI_SLA_RISK_FALLBACKS=mock
AI_COST_FORECAST_PROVIDER=openrouter
AI_COST_FORECAST_MODEL=openai/gpt-4o-mini
AI_CAPACITY_PLANNER_PROVIDER=anthropic
AI_CAPACITY_PLANNER_MODEL=claude-3-5-sonnet-latest
AI_ROOT_CAUSE_PATTERNS_PROVIDER=openai
AI_ROOT_CAUSE_PATTERNS_MODEL=gpt-4o-mini
AI_CHANGE_IMPACT_PROVIDER=lm-studio
AI_CHANGE_IMPACT_MODEL=qwen2.5-7b-instruct
AI_SUMMARY_PROVIDER=gemini
AI_SUMMARY_MODEL=gemini-2.5-flash
AI_CHAT_PROVIDER=bedrock
AI_CHAT_MODEL=openai.gpt-oss-20b-1:0
AI_CHAT_FALLBACKS=openai,mock
AI_SHARED_CACHE_BACKEND=upstash
AI_SHARED_CACHE_URL=https://<your-upstash-host>
AI_SHARED_CACHE_TOKEN=<your-upstash-token>
AI_SHARED_CACHE_PREFIX=glasspane:ai
AI_ROUTE_AUTH_ENABLED=true
AI_ROUTE_API_KEYS_JSON=[{"id":"ops-team","token":"replace-me","allowedCustomers":["cust-001"],"requestsPerMinute":60,"routeLimits":{"chat":20}}]
AI_ROUTE_STANDARD_LIMIT_PER_MINUTE=20
AI_ROUTE_ENTERPRISE_LIMIT_PER_MINUTE=60
AI_ROUTE_ENTERPRISE_PREMIUM_LIMIT_PER_MINUTE=120
AI_ROUTE_QUOTA_WINDOW_MS=60000Provider-specific env vars:
WATSONX_API_KEY,WATSONX_PROJECT_ID,WATSONX_REGION,WATSONX_MODEL_IDOPENROUTER_API_KEY,OPENROUTER_BASE_URL,OPENROUTER_MODEL_ID,OPENROUTER_SITE_URL,OPENROUTER_APP_NAMELM_STUDIO_BASE_URL,LM_STUDIO_MODEL_ID,LM_STUDIO_API_KEYOLLAMA_BASE_URL,OLLAMA_MODEL_ID,OLLAMA_API_KEYOPENAI_API_KEY,OPENAI_BASE_URL,OPENAI_MODEL_IDANTHROPIC_API_KEY,ANTHROPIC_BASE_URL,ANTHROPIC_MODEL_ID,ANTHROPIC_VERSIONGEMINI_API_KEY,GEMINI_BASE_URL,GEMINI_MODEL_IDBEDROCK_API_KEY,BEDROCK_BASE_URL,BEDROCK_MODEL_ID,BEDROCK_REGIONAI_SHARED_CACHE_BACKEND,AI_SHARED_CACHE_URL,AI_SHARED_CACHE_TOKEN,AI_SHARED_CACHE_PREFIXAI_ROUTE_AUTH_ENABLED,AI_ROUTE_API_KEYS_JSON,AI_ROUTE_QUOTA_WINDOW_MSAI_ROUTE_STANDARD_LIMIT_PER_MINUTE,AI_ROUTE_ENTERPRISE_LIMIT_PER_MINUTE,AI_ROUTE_ENTERPRISE_PREMIUM_LIMIT_PER_MINUTEAI_ROUTE_SUMMARY_LIMIT_PER_MINUTE,AI_ROUTE_CHAT_LIMIT_PER_MINUTE,AI_ROUTE_INSIGHTS_LIMIT_PER_MINUTE, and the equivalent per-taskAI_ROUTE_*_LIMIT_PER_MINUTEoverrides
The UI talks only to internal routes, so provider credentials remain server-side.
All /app/api/ai/* routes now add a request ID to responses. When AI_ROUTE_AUTH_ENABLED=true or AI_ROUTE_API_KEYS_JSON is configured, the routes require Authorization: Bearer <token> or x-ai-api-key, validate the requested customerId, and apply tenant-aware quotas per route + customerId.
/app/api/ai/chat now supports Server-Sent Events. AiChatPanel opts into streaming automatically, and the router falls back to chunked non-stream responses when the selected provider does not expose a native streaming API.
The AI task layer now uses a shared cache helper in /Users/thorstenmeyer/Dev/Transparency/lib/ai/cache.ts backed by /Users/thorstenmeyer/Dev/Transparency/lib/ai/shared-store.ts. By default it uses in-process memory, and it can switch to Upstash Redis REST for multi-instance shared caching without changing task code.
Run the lightweight AI regression suite with:
npm run test:ai| Layer | Technology | Purpose |
|---|---|---|
| Framework | Next.js 16 (App Router) + TypeScript | SSR-ready, file-based routing |
| UI Components | Tremor + shadcn/ui primitives | Dashboard charts and UI elements |
| Charts | Recharts (via Tremor) | AreaChart, BarChart, DonutChart, LineChart |
| Styling | Tailwind CSS 4 | Utility-first CSS with custom indigo theme |
| Layout | CSS Grid (12-column) | Responsive widget grid |
| Icons | @remixicon/react | Tremor's default icon set |
| Element | Value |
|---|---|
| Primary | #4F46E5 (Indigo) |
| Hover | #4338CA |
| Light tint | #EEF2FF |
| Success | #00C26D |
| Danger | #F2321E |
| Warning | #FC7E21 |
| Info | #3950EA |
| Font | Inter / system-ui |
CSS variables and Tailwind theme tokens are defined in app/globals.css.
Tremor v3 constructs Tailwind class names dynamically at runtime using template literals (e.g. `fill-${color}-500`). Tailwind CSS v4's JIT scanner only detects class names that appear as complete strings in source files, so these dynamic classes are never generated — causing all chart colors to render as black (SVG default fill).
app/globals.css includes a @source inline("...") directive that explicitly safelists every color utility Tremor needs:
fill-{color}-500andstroke-{color}-500— SVG chart fills and strokesbg-{color}-{50,100,200,500,600}— backgrounds at various shadestext-{color}-{500,700,900}— text colorsborder-{color}-500andring-{color}-300— borders and focus rings
This covers 22 Tailwind colors (blue, cyan, slate, gray, red, orange, amber, emerald, violet, fuchsia, rose, indigo, green, pink, sky, teal, purple, yellow, zinc, neutral, stone, lime).
Tremor also uses semantic class names like fill-tremor-content, bg-tremor-background, text-tremor-label, etc. These require two things in globals.css:
- Safelist entries — the semantic classes are included in the
@source inline(...)block - Theme definitions — the
@theme inlineblock defines the corresponding CSS custom properties (--color-tremor-brand,--color-tremor-background,--color-tremor-content, etc.) and font sizes (--text-tremor-label,--text-tremor-default,--text-tremor-title,--text-tremor-metric)
If you use a Tremor color not already in the safelist (e.g. "warmGray"), add its utilities to the @source inline(...) block in globals.css or the chart will render without color.
Single /dashboard page with tab-based view switching stored in URL search params (?view=c-level). Three views:
- C-Level — Executive KPIs, SLA gauges, cost overview, risk score
- Business — Operational metrics, tickets, changes, project delivery
- Technical — System status, latency, resource utilization, vulnerabilities
Each widget is a self-contained "use client" React component wrapped by WidgetShell (consistent card styling, loading skeleton, error boundary). Widgets are:
- Lazy-loaded via
React.lazy()throughconfig/widget-registry.ts - Configured per view in
config/view-configs.ts(widget ID, size, title) - Filtered by customer — only widgets relevant to subscribed service categories appear
data/mock/*.json → lib/services/*-service.ts → Widget components (useEffect + useState)
↑
async functions
(swap for real APIs later)
CustomerProvider wraps the dashboard layout. The useCustomer() hook provides the active customer to all widgets. Switching customers in the CustomerSelector dropdown re-renders all widget data.
Transparency/
├── app/
│ ├── layout.tsx # Root layout (html, body, metadata)
│ ├── page.tsx # Landing → redirect to /dashboard
│ ├── globals.css # Tailwind + brand CSS variables
│ └── dashboard/
│ ├── layout.tsx # Sidebar + Header + CustomerProvider
│ ├── page.tsx # Widget grid (reads ?view= param)
│ └── loading.tsx # Skeleton loader
├── components/
│ ├── layout/
│ │ ├── Sidebar.tsx # Logo, navigation, customer selector
│ │ ├── Header.tsx # View tabs, avatar, notifications
│ │ ├── ViewTabs.tsx # C-Level / Business / Technical switcher
│ │ └── CustomerSelector.tsx # Dropdown to switch customer context
│ └── widgets/
│ ├── WidgetShell.tsx # Card wrapper (title, loading, error)
│ ├── WidgetGrid.tsx # CSS Grid renderer: config → components
│ ├── SortableWidget.tsx # Drag-and-drop widget wrapper
│ ├── AiChatPanel.tsx # Conversational AI panel
│ ├── shared/ # KpiCard, StatusBadge, TrendIndicator
│ ├── c-level/ # 13 widget components (incl. AI + optimization)
│ ├── business/ # 14 widget components (incl. AI)
│ ├── technical/ # 17 widget components (incl. AI)
│ └── ai/ # 9 AI-powered widget components
├── config/
│ ├── widget-registry.ts # Widget ID → lazy component mapping (44 widgets)
│ ├── view-configs.ts # Widget layout arrays per view
│ ├── navigation.ts # Sidebar nav items
│ └── theme.ts # Brand color tokens
├── lib/
│ ├── customer-context.tsx # React Context for active customer
│ ├── sidebar-context.tsx # React Context for sidebar collapsed state
│ ├── use-widget-order.ts # Hook for persisted drag-and-drop widget order
│ └── services/ # 8 service modules (async data access)
│ ├── customer-service.ts
│ ├── service-service.ts
│ ├── kpi-service.ts
│ ├── incident-service.ts
│ ├── zero-outage-service.ts
│ ├── cost-service.ts
│ ├── security-service.ts
│ └── infrastructure-service.ts
├── data/
│ └── mock/ # 7 JSON files with mock data
│ ├── customers.json
│ ├── services.json
│ ├── kpis.json
│ ├── incidents.json
│ ├── zero-outage.json
│ ├── costs.json
│ ├── security.json
│ └── infrastructure.json
├── types/ # 8 TypeScript definition files
│ ├── index.ts # Barrel export
│ ├── customer.ts
│ ├── service.ts
│ ├── kpi.ts
│ ├── incident.ts
│ ├── zero-outage.ts
│ ├── widget.ts
│ ├── infrastructure.ts
│ └── security.ts
└── public/
└── logo.png
44 widgets are registered across 3 role-based views. AI-powered widgets are shared across views and powered by the AI service layer.
| Widget | Size | Visualization |
|---|---|---|
| SLA Compliance Gauge | medium | DonutChart with 99.999% target ring |
| Zero Outage Score | medium | Large metric + 3 CategoryBars (People/Processes/Platforms) |
| Service Health Overview | large | Grid of service cards with status badges |
| Cost Overview | medium | BarChart by service category + month-over-month trend |
| Risk Score | small | Single number with risk distribution CategoryBar |
| Major Incidents Summary | medium | BarList of P1-P4 incidents with counts |
| Digital Transformation Progress | medium | ProgressBars for migration milestones |
| Security Posture | small | DonutChart of vulnerabilities + overall score |
| Optimization | medium | Cost and performance optimization recommendations |
| AI Summary | medium | AI-generated executive summary of service health |
| AI Risk Briefing | medium | AI-assessed risk factors and mitigation priorities |
| AI Cost Forecast | medium | AI-predicted cost trends and budget recommendations |
| AI Predictions | medium | AI-driven forecasts for key operational metrics |
| Widget | Size | Visualization |
|---|---|---|
| Service Utilization | medium | Stacked BarChart by category |
| Ticket Volume Trends | medium | AreaChart (opened vs resolved, 12 months) |
| MTTR Trends | medium | LineChart by severity |
| Change Success Rate | small | Donut gauge with trend arrow |
| Project Delivery Status | large | Table with progress bars |
| SLA Compliance by Service | large | BarList sorted by SLA % |
| Top Open Issues | medium | Table with severity badges |
| Pending Changes | medium | Table with date/risk/service |
| Zero Outage Pillars | medium | Three CategoryBars with metrics |
| Service Availability Trend | full | AreaChart with 99.999% reference line |
| AI Summary | medium | AI-generated operational summary |
| AI Anomalies | medium | AI-detected anomalies in service behaviour |
| AI SLA Risk Advisor | medium | AI-ranked SLA breach risk per service |
| AI Change Impact | medium | AI-predicted impact assessment for pending changes |
| Widget | Size | Visualization |
|---|---|---|
| System Status Grid | full | Tile grid with colored status indicators |
| Uptime by Service | large | BarList with SLA target comparison |
| Latency Metrics | medium | LineChart (p50/p95/p99) |
| Incidents by Severity | medium | Stacked BarChart (P1-P4) |
| Patch Compliance | small | DonutChart |
| Resource Utilization | large | Triple AreaChart (CPU/Memory/Disk) |
| Network Throughput | medium | AreaChart (inbound/outbound) |
| Certificate Expiry | medium | Table with countdown badges |
| Vulnerability Summary | medium | DonutChart + top CVE list |
| Backup Success Rate | small | Success rate + timestamp per service |
| Change Calendar | medium | Heatmap grid by day/risk |
| Error Rate by Service | medium | LineChart per service |
| DNS Resolution Time | small | Sparkline with 7-day trend |
| AI Anomalies | medium | AI-detected infrastructure anomalies |
| AI Root Cause Patterns | medium | AI-identified recurring failure patterns |
| AI Capacity Planner | medium | AI-driven capacity forecasting and scaling advice |
| AI Predictions | medium | AI-driven forecasts for infrastructure metrics |
| Widget | Purpose |
|---|---|
| AiSummaryWidget | Executive / operational health summary |
| AiAnomaliesWidget | Anomaly detection across services |
| AiPredictionsWidget | Predictive metric forecasting |
| AiRiskBriefingWidget | Risk factor analysis and prioritisation |
| AiCostForecastWidget | Cost trend predictions and budget guidance |
| AiRootCausePatternsWidget | Recurring failure pattern analysis |
| AiSlaRiskAdvisorWidget | SLA breach risk ranking |
| AiCapacityPlannerWidget | Capacity forecasting and scaling recommendations |
| AiChangeImpactWidget | Change risk and impact assessment |
Two mock customers demonstrate service filtering:
Muster AG (Manufacturing, Enterprise Premium)
- Subscribed: Cloud, SAP, Security, Connectivity, Workplace
- 5 services, full Zero Outage coverage
TechVision GmbH (Technology, Standard)
- Subscribed: Cloud, AI & Data, IoT
- 3 services, technology-focused portfolio
Each customer has 12 months of KPI history, realistic incident records, infrastructure metrics, cost breakdowns, and Zero Outage pillar scores.
The widget grid uses a 12-column CSS Grid with predefined size classes:
| Size | Desktop (>1280px) | Tablet (>768px) | Mobile |
|---|---|---|---|
| small | 3 columns | 4 columns | 6 → 12 columns |
| medium | 4 columns | 6 columns | 12 columns |
| large | 6 columns | 6 columns | 12 columns |
| full | 12 columns | 12 columns | 12 columns |
-
Create the component in the appropriate view folder:
components/widgets/{view}/MyWidget.tsx -
Register it in
config/widget-registry.ts:"my-widget": () => import("@/components/widgets/{view}/MyWidget"),
-
Add it to the view config in
config/view-configs.ts:{ id: "my-widget", title: "My Widget", size: "medium", category: "kpi" }
The widget will automatically appear in the dashboard grid, be lazy-loaded, and wrapped with the WidgetShell card styling.
- Empty state: Return
<div />(notnull) when data is loading or empty. Returningnullunmounts the widget shell, causing RechartsResponsiveContainerto lose its dimensions and logwidth(0)/height(0)warnings on re-mount (e.g. when switching customers). - Chart colors: Use high-contrast color pairs. Avoid pairing a color with
"gray"— use"slate"or a distinct hue instead. Verify the colors you use are included in the safelist inglobals.css. - DonutChart center labels: If you render a custom overlay in the center of a
DonutChart, passshowLabel={false}andshowTooltip={false}to prevent Tremor's built-in center label from overlapping. - X-axis labels: Tremor's bar/line charts may skip labels when the axis is crowded. Add
tickGap={2}to force all labels to display.
Each service file in lib/services/ exposes async functions that currently read from JSON files. To connect real APIs:
- Replace the JSON import with a
fetch()call - Keep the same function signature and return type
- All widgets continue working without changes
Example migration:
// Before (mock)
import kpisData from "@/data/mock/kpis.json";
export async function getCurrentSla(customerId: string): Promise<number> {
return data[customerId]?.currentSla ?? 0;
}
// After (real API)
export async function getCurrentSla(customerId: string): Promise<number> {
const res = await fetch(`/api/kpis/${customerId}/sla`);
const data = await res.json();
return data.currentSla;
}-
Tremor DonutChart
wrapperStylewarning — In development mode, React 19 logs a warning:"Invalid prop wrapperStyle supplied to React.Fragment". This is a known Tremor v3 bug wherewrapperStyleis passed to a<React.Fragment>. It is cosmetic, dev-only, and does not affect functionality. It will be resolved when Tremor releases a React 19-compatible version. -
--legacy-peer-depsrequired — Tremor v3 has not updated its peer dependency to include React 19. The library works correctly; only the peer dependency declaration is outdated.
| Command | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Production build (TypeScript check + optimize) |
npm run start |
Serve production build |
npm run lint |
Run ESLint |
Out of the box, Glasspane runs in unauthenticated demo mode. The /api/ai/* routes accept a customerId from the request body and serve data for that tenant without verifying caller identity.
This is intentional for the public demo — all data is mock data and the goal is to let anyone explore the product. The server logs a security warning at startup when running in this mode.
| Scenario | Risk in demo mode | Risk with real data |
|---|---|---|
| Public demo with mock data | Acceptable | N/A |
| Internal demo with mock data | Acceptable | N/A |
| Production with real customer data | Not acceptable | Cross-tenant data exposure — any caller can pass any customerId |
Before deploying with real customer data, enable authentication:
# Required: turns on auth enforcement
AI_ROUTE_AUTH_ENABLED=true
# Required: define API keys with tenant scoping and per-tenant quotas
AI_ROUTE_API_KEYS_JSON=[
{
"id": "ops-team",
"token": "<generate-a-strong-secret>",
"allowedCustomers": ["cust-001", "cust-002"],
"requestsPerMinute": 60,
"routeLimits": {"chat": 20}
}
]When auth is enabled:
- All
/api/ai/*requests must includeAuthorization: Bearer <token>orx-ai-api-key - The token's
allowedCustomerslist constrains which tenants the caller can access - Per-tenant rate limits are enforced via the shared cache layer
- Requests without a valid token return 401/403
The current API-key model is suitable for machine-to-machine integration. A future release should add session-based authentication (e.g., NextAuth) so that browser-based access binds the customerId to a logged-in user identity, rather than being declared in the request body.
If you intend to deploy Glasspane with real customer data and need a session-based auth implementation before this lands, contributions are welcome — see the GitHub repository.
If you discover a security vulnerability, please open a private security advisory on the GitHub repository rather than a public issue.
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only).
Copyright (c) 2026 Thorsten Meyer