Engineeringpanel (dev)

panel (dev)

The super-admin control-plane frontend: a separate Next.js app for NexSchool platform operators to manage tenants, plans, and audit logs against /api/platform/*.

App structure

The panel (panel/) is a standalone Next.js 16 App Router app — same stack as admin-web (React 19, TypeScript 5, Tailwind 4, TanStack Query 5, React Hook Form + Zod, Radix UI, Sonner) but with no Firebase (platform admins get no push). It runs on port 3001 locally; production is panel.nexschool.com.

Routes live under panel/src/app/, split into two route groups:

panel/src/app/
├── layout.tsx
├── globals.css
├── (auth)/
│   └── login/page.tsx
└── (dashboard)/
    ├── layout.tsx
    └── dashboard/
        ├── page.tsx                 Platform overview
        ├── settings/page.tsx        Global platform settings
        ├── tenants/
        │   ├── page.tsx             Tenant list
        │   └── [id]/
        │       ├── page.tsx         Tenant detail
        │       └── tenant-detail-view.tsx
        ├── audit/page.tsx           Audit log viewer
        └── notification-templates/
            └── page.tsx             Platform notification templates

Supporting folders: components/ (forms/, layout/, providers/, tables/, ui/), services/ (API clients for /api/platform/*), and types/.

The dashboard landing page (/dashboard) surfaces total tenants, active vs suspended counts, new signups this month, and recent audit entries.

Platform API & auth

The panel calls the same Flask API as admin-web but only hits /api/platform/* and /api/auth/*. There is no tenant-scoped data here, so — unlike admin-web — panel requests do not send X-Tenant-ID, and the TanStack Query tenant-scoping rules do not apply.

Login uses the shared /api/auth/login endpoint with a platform flag:

// Panel uses a separate auth store. No X-Tenant-ID header for platform routes.
const response = await api.post('/api/auth/login', {
  email,
  password,
  is_platform: true,   // signals platform admin login
});
// JWT payload: { user_id, is_platform_admin: true }

The backend authorizes /api/platform/* routes by checking the is_platform_admin flag in the JWT — not by tenant membership.

Endpoints the panel uses:

Authentication:
  POST /api/auth/login          (platform admin credentials)
  POST /api/auth/logout

Platform management:
  GET  /api/platform/tenants
  POST /api/platform/tenants
  GET  /api/platform/tenants/<id>
  PUT  /api/platform/tenants/<id>
  POST /api/platform/tenants/<id>/suspend
  POST /api/platform/tenants/<id>/activate
  GET  /api/platform/plans
  POST /api/platform/plans
  PUT  /api/platform/plans/<id>
  GET  /api/platform/audit-logs
  GET  /api/platform/notification-settings
  PUT  /api/platform/notification-settings
  GET  /api/platform/notification-templates
  POST /api/platform/notification-templates
  PUT  /api/platform/notification-templates/<id>

Key env vars:

NEXT_PUBLIC_API_URL=http://localhost/api
NEXT_PUBLIC_GATEWAY_ORIGIN=http://panel.localhost

Tenant management

/dashboard/tenants lists every tenant (school) with:

  • Search by name, subdomain, or contact email — server-side via ?search= on GET /api/platform/tenants (case-insensitive contains). The input is debounced and resets to page 1 on change.
  • A Create new tenant action.
  • A status filter exists on the API (?status=) but is not yet exposed in the UI.

/dashboard/tenants/[id] (rendered through tenant-detail-view.tsx) shows school information, current plan assignment, feature-flag overrides, subscription status, per-tenant student/teacher/class counts, recent audit logs for that tenant, and Suspend / Activate actions (backed by POST .../suspend and POST .../activate).

Tenant shape (panel view):

{
  "id": 5,
  "name": "Greenfield Academy",
  "subdomain": "greenfield",
  "status": "active",
  "plan": { "id": 2, "name": "growth" },
  "is_setup_complete": true,
  "trial_ends_at": null,
  "contact_email": "admin@greenfield.edu",
  "feature_flags": {},
  "stats": {
    "total_students": 450,
    "total_teachers": 32,
    "total_classes": 18
  },
  "created_at": "2024-01-15T09:00:00Z"
}

Plans

Subscription plans are managed platform-wide via GET/POST /api/platform/plans and PUT /api/platform/plans/<id>, and assigned to tenants from the tenant detail view. A plan defines pricing and capacity limits plus a feature set:

{
  "id": 2,
  "name": "growth",
  "price_monthly": 2999.00,
  "max_students": 1000,
  "max_teachers": 100,
  "features_json": {
    "features": ["attendance", "fees_management", "timetable", "transport", "notifications"]
  }
}

The features list drives the tenant’s feature flags; see /product/subscriptions for plan/tier semantics.

Audit

/dashboard/audit is a read-only platform audit trail — who did what to which tenant — filterable by tenant, action type, and date range, sourced from GET /api/platform/audit-logs. Entries are non-editable.

From the tenant detail view, an operator can open the tenant’s admin-web in one click (“open admin-web” login link), so support and setup work can be done as that tenant without a manual re-login. This crosses the control-plane boundary from panel into the tenant app; the flow and its guardrails are described in /architecture/control-plane.