The NDSS CRM dashboard is the central command centre for managing every aspect of NDIS and disability care operations. Upon successful authentication, users are presented with a role-specific dashboard that surfaces the most relevant information, alerts, and quick actions for their position within the organisation. This chapter provides an exhaustive breakdown of every dashboard component, navigation mechanism, and interactive widget available within the platform. The dashboard is built using Next.js, React, and TypeScript on the front end, with Python and PHP microservices powering data aggregation and PostgreSQL storing all persistent state.
NDSS CRM provides 20 distinct dashboard views, each tailored to a specific user role within the system. When a user logs in, the platform determines their primary role from the user_roles table in PostgreSQL and renders the corresponding dashboard layout. The dashboard framework is component-based, using React server components for initial data fetching and client components for interactive widgets.
The following table enumerates every dashboard variant, including the role it serves, the primary data focus, and the number of widgets rendered by default.
| # | Dashboard Name | Role | Primary Focus | Default Widgets |
|---|---|---|---|---|
| 1 | Administrator Dashboard | Super Admin | Full system overview, all metrics | 12 |
| 2 | Organisation Admin Dashboard | Org Admin | Organisation-level KPIs | 10 |
| 3 | Operations Manager Dashboard | Ops Manager | Rostering, compliance, incidents | 9 |
| 4 | Finance Dashboard | Finance Officer | Revenue, invoices, claims | 8 |
| 5 | HR Manager Dashboard | HR Manager | Staff, onboarding, training | 8 |
| 6 | Support Coordinator Dashboard | Coordinator | Client plans, goals, progress | 7 |
| 7 | Team Leader Dashboard | Team Leader | Team shifts, performance | 7 |
| 8 | Rostering Officer Dashboard | Roster Officer | Shift allocation, availability | 8 |
| 9 | Compliance Officer Dashboard | Compliance | Incidents, audits, certifications | 7 |
| 10 | Clinical Lead Dashboard | Clinical Lead | Care plans, health records | 6 |
| 11 | Support Worker Dashboard | Support Worker | My shifts, my clients | 5 |
| 12 | Intake Officer Dashboard | Intake Officer | Referrals, assessments | 6 |
| 13 | Training Coordinator Dashboard | Training Coord | Courses, completions, overdue | 6 |
| 14 | SIL Coordinator Dashboard | SIL Coord | Group homes, SIL rosters | 7 |
| 15 | Client Portal Dashboard | Client/Guardian | My plan, bookings, progress | 5 |
| 16 | Plan Manager Dashboard | Plan Manager | Funding, claims, budgets | 7 |
| 17 | Front Desk Dashboard | Receptionist | Visitors, calls, messages | 5 |
| 18 | Auditor Dashboard | External Auditor | Read-only compliance view | 4 |
| 19 | Specialist Services Dashboard | Specialist | Specialist appointments | 5 |
| 20 | Executive Dashboard | Executive | High-level analytics only | 6 |
Each dashboard is composed of modular widget components that are registered in the dashboard_widgets configuration table. The widget registry is managed via a Python-based configuration service that determines which widgets are available for each role. The rendering pipeline works as follows:
Dashboard widget configurations are cached in Redis with a 5-minute TTL. Changes to widget layouts via the Admin Settings panel take effect within this window. The caching layer is implemented in Python using the redis-py library, while the PHP services use predis/predis.
// Dashboard Data Flow (Simplified)
// Next.js Server Component → Python Aggregation Service → PostgreSQL
// Widget Rendering Pipeline
export async function DashboardPage() {
const session = await getServerSession();
const role = session.user.role;
// Fetch widget config from Python service
const widgetConfig = await fetch(
`${PYTHON_SERVICE_URL}/api/dashboard/widgets?role=${role}`
);
// Fetch aggregated data
const dashboardData = await fetch(
`${PYTHON_SERVICE_URL}/api/dashboard/data?role=${role}&org_id=${session.user.org_id}`
);
return (
<DashboardLayout>
{widgetConfig.widgets.map(widget => (
<DashboardWidget
key={widget.id}
type={widget.type}
config={widget.config}
data={dashboardData[widget.dataKey]}
/>
))}
</DashboardLayout>
);
}
The Administrator Dashboard is the most comprehensive view in NDSS CRM, providing a complete operational snapshot. It is available to users with the super_admin or org_admin role. This section details every element visible on the administrator's default dashboard.
The following wireframe illustrates the complete Administrator Dashboard layout, including the top bar, stat cards, activity feed, and analytics panels.
The stat cards at the top of the Administrator Dashboard provide an at-a-glance summary of key operational metrics. Each card is a self-contained React component that fetches its data independently, enabling partial page updates without full re-renders.
| Stat Card | Data Source | Refresh Interval | Trend Indicator | Click Action |
|---|---|---|---|---|
| Active Clients | clients table, status = 'active' |
5 minutes | +8% vs last month | Navigate to Client List |
| Active Staff | staff table, status = 'active' |
5 minutes | +2% vs last month | Navigate to Staff Directory |
| Shifts Today | shifts table, date = CURRENT_DATE |
1 minute | None (absolute count) | Navigate to Today's Roster |
| Open Incidents | incidents table, status IN ('open','investigating') |
2 minutes | -15% vs last month | Navigate to Incidents List |
| Revenue (AU$) | Python aggregation of invoices + payments tables |
15 minutes | +12% vs last month | Navigate to Finance Overview |
| NDIS Claims Pending | ndis_claims table, status = 'pending' |
5 minutes | None (absolute count) | Navigate to Claims Queue |
The Preview Role button allows administrators to temporarily view the dashboard as any other role in the system. This is invaluable for troubleshooting permission issues, verifying that role-specific widgets display correctly, and training staff on what their dashboard will look like. When activated, a prominent banner appears at the top of the page indicating the previewed role, along with an "Exit Preview" button to return to the admin view.
// Preview Role API Endpoint (Python Flask)
@app.route('/api/admin/preview-role', methods=['POST'])
def preview_role():
admin_id = get_current_user_id()
target_role = request.json.get('role')
# Validate admin has permission to preview
if not has_permission(admin_id, 'admin.preview_role'):
return jsonify({'error': 'Insufficient permissions'}), 403
# Create temporary session overlay
session_token = create_preview_session(admin_id, target_role)
return jsonify({
'preview_token': session_token,
'role': target_role,
'expires_in': 3600 # 1 hour
})
The Smart Alerts Panel is a priority-ranked feed of actionable items that require immediate attention. Alerts are generated by a Python background worker that runs every 60 seconds, scanning multiple database tables for threshold violations, upcoming deadlines, and anomalous conditions.
| Category | Severity | Trigger Condition | Example Alert | Action Link |
|---|---|---|---|---|
| Expired Certifications | Critical | Staff certification expiry_date < CURRENT_DATE |
"3 staff members have expired First Aid certificates" | Staff → Qualifications |
| Expiring Certifications | Warning | Certification expires within 30 days | "Sarah Chen's NDIS Worker Screening expires in 12 days" | Staff Profile → Qualifications |
| Unallocated Shifts | Warning | Shift staff_id IS NULL and date <= CURRENT_DATE + 7 |
"2 shifts in next 24 hours have no staff assigned" | Rostering → Unallocated |
| Overdue Invoices | Warning | Invoice due_date < CURRENT_DATE and status = 'unpaid' |
"5 invoices totalling AU$12,340 are overdue" | Finance → Invoices |
| Compliance Deadlines | Info | Audit/review due within 14 days | "Quarterly compliance audit due on 15 Apr 2024" | Compliance → Audits |
| NDIS Plan Expiry | Critical | Client NDIS plan expires within 30 days | "James Thompson's NDIS plan expires in 8 days" | Client Profile → Funding |
| Incomplete Timesheets | Warning | Shift completed but no timesheet submitted within 48 hours | "7 timesheets pending submission from last week" | Rostering → Timesheets |
| Funding Budget Alert | Critical | Client funding utilisation exceeds 85% | "Maria Garcia: Core Supports at 92% utilisation" | Client Profile → Funding |
Alerts are scored using a weighted priority algorithm implemented in Python. The scoring factors include:
# Alert Priority Scoring (Python)
def calculate_alert_priority(alert):
base_score = SEVERITY_WEIGHTS[alert.severity] # Critical=100, Warning=60, Info=20
# Time decay: alerts become more urgent as deadlines approach
if alert.deadline:
days_remaining = (alert.deadline - datetime.now()).days
time_factor = max(0, 1 - (days_remaining / 30))
base_score += time_factor * 40
# Impact factor: number of affected entities
impact_factor = min(alert.affected_count / 10, 1.0)
base_score += impact_factor * 20
# Financial impact (if applicable)
if alert.financial_amount:
financial_factor = min(alert.financial_amount / 50000, 1.0)
base_score += financial_factor * 30
return min(base_score, 200) # Cap at 200
The Activity Feed provides a chronological timeline of all system events relevant to the current user's scope. Events are streamed via Supabase / Oracle Realtime and stored in the activity_log table in PostgreSQL. The feed supports infinite scroll pagination, loading 25 events per batch.
| Event Type | Icon | Description | Example |
|---|---|---|---|
client.created | 👤 | New client record created | "New client Maria Garcia added by Jane Smith" |
client.updated | ✏ | Client record modified | "James Thompson NDIS plan updated" |
shift.created | 📅 | New shift scheduled | "Shift created: J. Thompson, Mon 8 Apr 9:00-13:00" |
shift.completed | ✅ | Shift marked as completed | "Shift #SH-0234 completed by Mark Wilson" |
shift.cancelled | ❌ | Shift was cancelled | "Shift #SH-0235 cancelled (client request)" |
invoice.created | 💰 | New invoice generated | "Invoice #INV-0042 created: AU$1,250" |
invoice.approved | ✅ | Invoice approved for payment | "Invoice #INV-0042 approved by Finance" |
incident.reported | ⚠ | New incident report filed | "Incident #INC-020 reported at SIL House Alpha" |
incident.resolved | ✅ | Incident closed and resolved | "Incident #INC-019 resolved and closed" |
staff.certified | 📜 | Staff certification updated | "Sarah Chen completed CPR certification" |
referral.received | 📥 | New client referral received | "New referral: Maria Garcia from GP Dr. Patel" |
system.backup | 💾 | System backup completed | "Nightly backup completed successfully" |
Users can filter the activity feed by event category using the tab bar above the timeline. Available filter groups:
client.* events onlyshift.* events onlyinvoice.*, payment.* eventsincident.*, audit.*, certification.* eventssystem.* events// Activity Feed Filter Component (TypeScript/React)
interface ActivityFilter {
label: string;
eventTypes: string[];
icon: React.ReactNode;
}
const ACTIVITY_FILTERS: ActivityFilter[] = [
{ label: 'All', eventTypes: ['*'], icon: <ListIcon /> },
{ label: 'Clients', eventTypes: ['client.*'], icon: <UserIcon /> },
{ label: 'Shifts', eventTypes: ['shift.*'], icon: <CalendarIcon /> },
{ label: 'Finance', eventTypes: ['invoice.*', 'payment.*'], icon: <DollarIcon /> },
{ label: 'Compliance', eventTypes: ['incident.*', 'audit.*'], icon: <ShieldIcon /> },
{ label: 'System', eventTypes: ['system.*'], icon: <ServerIcon /> },
];
-- PostgreSQL: activity_log table
CREATE TABLE activity_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organisations(id),
event_type VARCHAR(100) NOT NULL,
actor_id UUID REFERENCES users(id),
actor_name VARCHAR(255),
entity_type VARCHAR(50), -- 'client', 'shift', 'invoice', etc.
entity_id UUID,
summary TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
INDEX idx_activity_org_created (org_id, created_at DESC),
INDEX idx_activity_event_type (event_type)
);
NDSS CRM uses Recharts (a React charting library) to render interactive data visualisations within dashboard widgets. All chart data is aggregated by a Python data pipeline that runs scheduled queries against PostgreSQL and caches results in Redis. PHP services supplement this by providing legacy financial report data through a RESTful interface.
| Chart Widget | Chart Type | Data Source | Default Period | Available To Roles |
|---|---|---|---|---|
| Monthly Revenue | Bar Chart | Invoices + Payments | Last 12 months | Admin, Finance, Executive |
| Shift Completion Rate | Line Chart | Shifts table | Last 8 weeks | Admin, Ops Manager, Roster Officer |
| Client Growth | Area Chart | Clients table | Last 12 months | Admin, Executive |
| Funding Utilisation | Stacked Bar | NDIS Claims + Budgets | Current quarter | Admin, Finance, Plan Manager |
| Incident Trends | Line Chart | Incidents table | Last 6 months | Admin, Compliance, Ops Manager |
| Staff Availability Heatmap | Heatmap | Availability table | Current week | Admin, Roster Officer |
| Service Category Breakdown | Pie/Donut Chart | Shifts + Service types | Current month | Admin, Ops Manager |
| Timesheet Approval Rate | Gauge Chart | Timesheets table | Current pay period | Admin, Finance, HR |
// Revenue Bar Chart Component (TypeScript + Recharts)
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
interface RevenueData {
month: string;
revenue: number;
claims: number;
}
export function RevenueChart({ data }: { data: RevenueData[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={(v) => `$${(v/1000).toFixed(0)}k`} />
<Tooltip
formatter={(value: number) => [`AU$${value.toLocaleString()}`, 'Revenue']}
/>
<Bar dataKey="revenue" fill="#E8672A" radius={[4,4,0,0]} />
<Bar dataKey="claims" fill="#1a1f2e" radius={[4,4,0,0]} />
</BarChart>
</ResponsiveContainer>
);
}
The NDSS CRM navigation system is a collapsible sidebar that organises all platform modules into logical groups. The sidebar is persistent on desktop viewports (greater than 1024px) and collapses into a hamburger menu on tablet and mobile devices. Navigation items are rendered conditionally based on the user's role permissions.
The sidebar is divided into the following section groups. Each section contains navigation items that link to specific platform modules.
Not all sidebar items are visible to every role. The following matrix shows which navigation groups are accessible by role:
| Navigation Group | Admin | Ops Manager | Coordinator | Support Worker | Finance | HR | Compliance |
|---|---|---|---|---|---|---|---|
| People | All | All | Clients only | My Clients | None | Staff only | None |
| Operations | All | All | Rostering | My Shifts | Finance | None | Compliance |
| Human Resources | All | HR Hub | None | My Training | None | All | Training Records |
| Communication | All | All | Messaging | Messaging | Reports | None | Reports |
| Administration | All | Settings | None | None | None | None | None |
The sidebar supports three states: expanded (full width with labels, 260px), collapsed (icon-only, 64px), and hidden (off-screen on mobile). Users can toggle between expanded and collapsed using the hamburger menu icon or the keyboard shortcut Ctrl + B (Windows/Linux) or Cmd + B (macOS). The collapse state is persisted to localStorage so it survives page refreshes.
// Sidebar State Management (TypeScript)
type SidebarState = 'expanded' | 'collapsed' | 'hidden';
interface SidebarContext {
state: SidebarState;
toggle: () => void;
expand: () => void;
collapse: () => void;
}
// Keyboard shortcut handler
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
e.preventDefault();
toggleSidebar();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
The Global Search modal provides a universal search experience across all NDSS CRM entities. It is triggered by pressing Cmd+K (macOS) or Ctrl+K (Windows/Linux), or by clicking the search bar in the top navigation. The search uses a full-text search index in PostgreSQL combined with a Python search service that handles ranking and result aggregation.
| Entity | Searchable Fields | Index Type | Result Display |
|---|---|---|---|
| Clients | Name, NDIS number, phone, email, suburb | GIN (tsvector) | Name, ID, NDIS number, location |
| Staff | Name, email, phone, role | GIN (tsvector) | Name, role, employment type, status |
| Shifts | Shift ID, client name, staff name, date | B-tree + GIN | Shift ID, client, date/time, status |
| Invoices | Invoice number, client name, amount | B-tree + GIN | Invoice number, client, amount, status |
| Incidents | Incident ID, description, location | GIN (tsvector) | Incident ID, summary, severity |
| Documents | File name, tags, associated entity | GIN (tsvector) | File name, type, upload date |
# Python Search Service (Flask)
from flask import Flask, request, jsonify
import psycopg2
from psycopg2.extras import RealDictCursor
@app.route('/api/search', methods=['GET'])
def global_search():
query = request.args.get('q', '')
org_id = request.args.get('org_id')
limit = int(request.args.get('limit', 20))
if len(query) < 2:
return jsonify({'results': []})
results = {}
# Search clients
clients = db.execute("""
SELECT id, first_name, last_name, ndis_number, suburb, state
FROM clients
WHERE org_id = %s
AND search_vector @@ plainto_tsquery('english', %s)
ORDER BY ts_rank(search_vector, plainto_tsquery('english', %s)) DESC
LIMIT %s
""", (org_id, query, query, limit))
results['clients'] = clients
# Search staff
staff = db.execute("""
SELECT id, first_name, last_name, role, employment_type, status
FROM staff
WHERE org_id = %s
AND search_vector @@ plainto_tsquery('english', %s)
ORDER BY ts_rank(search_vector, plainto_tsquery('english', %s)) DESC
LIMIT %s
""", (org_id, query, query, limit))
results['staff'] = staff
# Search shifts
shifts = db.execute("""
SELECT s.id, s.date, s.start_time, s.end_time, s.status,
c.first_name || ' ' || c.last_name AS client_name
FROM shifts s
JOIN clients c ON s.client_id = c.id
WHERE s.org_id = %s
AND (s.id::text ILIKE %s OR c.first_name ILIKE %s OR c.last_name ILIKE %s)
ORDER BY s.date DESC
LIMIT %s
""", (org_id, f'%{query}%', f'%{query}%', f'%{query}%', limit))
results['shifts'] = shifts
return jsonify({'results': results, 'total': sum(len(v) for v in results.values())})
The Quick Actions Floating Action Button (FAB) is a persistent circular button positioned in the bottom-right corner of the viewport. When clicked, it expands to reveal a set of shortcut actions for common tasks. The FAB is role-aware, showing only the actions that the current user has permission to perform.
| Action | Icon | Keyboard Shortcut | Available To | Navigates To |
|---|---|---|---|---|
| New Client | 👤+ | Alt+C | Admin, Coordinator, Intake | Client creation form |
| New Shift | 📅+ | Alt+S | Admin, Roster Officer, Coordinator | Shift creation form |
| New Invoice | 💰+ | Alt+I | Admin, Finance | Invoice creation form |
| New Incident | ⚠+ | Alt+N | All staff roles | Incident report form |
| New Staff | 👥+ | Alt+T | Admin, HR | Staff onboarding form |
| New Referral | 📥+ | Alt+R | Admin, Intake | Referral intake form |
// Quick Actions FAB Component (TypeScript/React)
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { usePermissions } from '@/hooks/usePermissions';
interface QuickAction {
id: string;
label: string;
icon: string;
href: string;
permission: string;
}
const QUICK_ACTIONS: QuickAction[] = [
{ id: 'new-client', label: 'New Client', icon: 'UserPlus', href: '/clients/new', permission: 'clients.create' },
{ id: 'new-shift', label: 'New Shift', icon: 'CalendarPlus', href: '/rostering/new', permission: 'shifts.create' },
{ id: 'new-invoice', label: 'New Invoice', icon: 'Receipt', href: '/finance/invoices/new', permission: 'invoices.create' },
{ id: 'new-incident', label: 'New Incident', icon: 'AlertTriangle', href: '/compliance/incidents/new', permission: 'incidents.create' },
];
export function QuickActionsFAB() {
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
const { hasPermission } = usePermissions();
const availableActions = QUICK_ACTIONS.filter(a => hasPermission(a.permission));
return (
<div className="fixed bottom-6 right-6 z-50">
{isOpen && availableActions.map((action, i) => (
<button
key={action.id}
onClick={() => router.push(action.href)}
className="fab-action-item"
style={{ animationDelay: `${i * 50}ms` }}
>
<span className="fab-label">{action.label}</span>
<Icon name={action.icon} />
</button>
))}
<button
className="fab-main"
onClick={() => setIsOpen(!isOpen)}
style={{ background: '#E8672A' }}
>
{isOpen ? '×' : '+'}
</button>
</div>
);
}
The Notification Center is accessed by clicking the bell icon in the top navigation bar. It displays a badge count of unread notifications and opens a dropdown panel with categorised notification items. Notifications are delivered in real-time via Supabase / Oracle Realtime and also stored in PostgreSQL for persistence.
| Category | Example Notification | Priority | Auto-dismiss |
|---|---|---|---|
| Shifts | "You have been assigned to a new shift on 10 Apr, 9:00 AM" | High | No |
| Shifts | "Shift #SH-0240 has been cancelled by the coordinator" | High | No |
| Compliance | "Your First Aid certificate expires in 14 days" | Critical | No |
| Compliance | "Incident #INC-021 requires your review" | High | No |
| Finance | "Invoice #INV-0050 has been approved for payment" | Medium | After 7 days |
| Finance | "NDIS claim batch #CB-012 submitted successfully" | Medium | After 7 days |
| System | "Scheduled maintenance window: Sun 12 Apr, 2:00-4:00 AM" | Low | After event |
| System | "New platform version 1.1.0 available" | Low | After 14 days |
-- PostgreSQL: notifications table
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organisations(id),
user_id UUID NOT NULL REFERENCES users(id),
category VARCHAR(50) NOT NULL, -- 'shifts', 'compliance', 'finance', 'system'
title VARCHAR(255) NOT NULL,
body TEXT,
priority VARCHAR(20) DEFAULT 'medium', -- 'critical', 'high', 'medium', 'low'
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMPTZ,
action_url TEXT,
auto_dismiss_at TIMESTAMPTZ,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
INDEX idx_notif_user_unread (user_id, is_read, created_at DESC),
INDEX idx_notif_category (user_id, category, created_at DESC)
);
Individual notifications can be marked as read by clicking them (which also navigates to the relevant action URL) or by clicking the "x" dismiss button. The "Mark all as read" link at the top of the panel performs a bulk update. The read state is synchronised in real-time across all open browser tabs via a Supabase / Oracle Realtime subscription on the notifications table.
The Role Switcher is an administrative tool that allows users with the super_admin role to temporarily adopt the perspective of any other role in the system. Unlike the "Preview Role" button on the dashboard (which only changes the dashboard view), the Role Switcher affects the entire application experience, including navigation, permissions, and data visibility.
| Constraint | Description |
|---|---|
| Access Control | Only super_admin users can access the Role Switcher |
| Session Duration | Preview sessions expire after 1 hour |
| Write Protection | All write operations are blocked during preview mode. Attempting to create, update, or delete records will show a "Preview Mode: Read Only" message. |
| Visual Indicator | An orange banner is displayed at the top of every page during preview mode |
| Audit Trail | All preview sessions are logged in the admin_audit_log table with the admin user ID, target role, start time, and end time |
| Concurrent Sessions | Only one preview session can be active per admin user at a time |
// Role Switcher Middleware (Next.js)
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const previewRole = request.cookies.get('newdawnss_preview_role')?.value;
const previewExpiry = request.cookies.get('newdawnss_preview_expiry')?.value;
if (previewRole && previewExpiry) {
// Check if preview session has expired
if (new Date(previewExpiry) < new Date()) {
const response = NextResponse.next();
response.cookies.delete('newdawnss_preview_role');
response.cookies.delete('newdawnss_preview_expiry');
return response;
}
// Inject preview role into request headers for downstream use
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-preview-role', previewRole);
requestHeaders.set('x-preview-mode', 'true');
return NextResponse.next({
request: { headers: requestHeaders },
});
}
return NextResponse.next();
}
The Role Switcher is a powerful administrative tool. All preview sessions are recorded in the audit log. Administrators should use this feature responsibly and only for legitimate purposes such as troubleshooting, training, or verifying role configurations. The preview session is strictly read-only to prevent accidental data modifications.