import Link from 'next/link'; export const dynamic = 'force-dynamic'; import { prisma } from '@/lib/prisma'; import { getServiceAddress } from '@/lib/service-address'; import Redis from 'ioredis'; import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3'; import { createDemoTenantAction, clearDemoDataAction } from './actions'; // Dynamic rendering handled via async data async function checkPostgres() { try { const raw = await prisma.$queryRaw>`SELECT 1 as status;`; return { ok: true, message: `Connected (SELECT 1 -> ${raw[0]?.status ?? 1})` }; } catch (err: any) { return { ok: false, message: err.message }; } } async function checkRedis() { const redisUrl = process.env.REDIS_URL; if (!redisUrl) return { ok: false, message: 'REDIS_URL not configured' }; const client = new Redis(redisUrl, { lazyConnect: true, maxRetriesPerRequest: 1, connectTimeout: 2000, commandTimeout: 2000, retryStrategy: () => null, }); // Register before ping starts the connection. Surface failures in the health // card instead of ioredis logging an unhandled error to the dev overlay. let connectionError: Error | undefined; client.on('error', (error: Error) => { connectionError = error; }); try { const pong = await client.ping(); return { ok: pong === 'PONG', message: `Connected (PING -> ${pong})` }; } catch (err: unknown) { const message = connectionError?.message ?? (err instanceof Error ? err.message : 'Redis connection failed'); return { ok: false, message }; } finally { client.disconnect(); } } async function checkS3() { const endpoint = process.env.S3_ENDPOINT; const accessKeyId = process.env.S3_ACCESS_KEY; const secretAccessKey = process.env.S3_SECRET_KEY; if (!endpoint || !accessKeyId || !secretAccessKey) { return { ok: false, message: 'MinIO credentials missing' }; } const s3 = new S3Client({ endpoint, region: 'us-east-1', credentials: { accessKeyId, secretAccessKey }, forcePathStyle: true, }); try { const res = await s3.send(new ListBucketsCommand({})); const count = res.Buckets?.length ?? 0; return { ok: true, message: `Connected (${count} buckets listed)` }; } catch (err: any) { return { ok: false, message: err.message }; } } export default async function Home() { const [pgStatus, redisStatus, s3Status] = await Promise.all([ checkPostgres(), checkRedis(), checkS3(), ]); const [tenants, userCount, sessionCount, bookingCount, waiverCount] = await Promise.all([ prisma.tenant.findMany({ include: { users: true, classSessions: true, bookings: true, eWaivers: true, }, orderBy: { createdAt: 'desc' }, take: 5, }), prisma.user.count(), prisma.classSession.count(), prisma.booking.count(), prisma.eWaiver.count(), ]); return (
{/* Header */}
Milestone 1 Verified

Fitness App POC & Multi-Tenant Engine

Next.js 16 (App Router) • Prisma ORM • PostgreSQL • Redis • MinIO S3

{tenants.length > 0 && (
)}
{/* Live Service Health Grid */}
{/* PostgreSQL Card */}
Database

PostgreSQL

{getServiceAddress(process.env.DATABASE_URL)}

{pgStatus.message}

{/* Redis Card */}
Cache / Queue

Redis 7

{getServiceAddress(process.env.REDIS_URL)}

{redisStatus.message}

{/* MinIO S3 Card */}
Object Storage

MinIO S3

{getServiceAddress(process.env.S3_ENDPOINT)}

{s3Status.message}

{/* Multi-Tenant Extension Card */}
Prisma Client

$extends Filter

lib/prisma.ts

tenantId auto-scoping active

{/* Database Metric Stats */}

Live Schema Metrics

{tenants.length}
Tenants (Gyms)
{userCount}
Users
{sessionCount}
Class Sessions
{bookingCount}
Bookings
{waiverCount}
E-Waivers
{/* Tenants Section */}

Registered Tenants & Scoped Records

Showing recent tenants
{tenants.length === 0 ? (

No tenants registered in the database yet.

Click "+ Seed Demo Tenant" in the header to create an isolated gym tenant with sample users and classes.

) : (
{tenants.map((t) => (

{t.name}

slug: {t.slug} • domain: {t.domain ?? 'n/a'} • ID: {t.id}
{t.users.length} Users {t.classSessions.length} Classes {t.bookings.length} Bookings
{/* Users breakdown */}
Tenant Users:
{t.users.map((u) => (
{u.name} ({u.email}) {u.role}
))}
Class Sessions:
{t.classSessions.map((cs) => (
{cs.title} Cap: {cs.capacity}
))}
Live Virtual Classroom:
Student Schedule & Live Room → Instructor Portal →
))}
)}
); }