Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ Designed for developers who value both speed and quality, this template provides
<img width="500" height="360" alt="login-1" src="https://github.com/user-attachments/assets/c344ffe2-0b93-47b3-82ae-85f07c8e28c0" />
<img width="500" height="360" alt="register-1" src="https://github.com/user-attachments/assets/603f7ef2-73fd-4cee-905e-b416a2238142" />



## Quick Start

### 1. Create Your Project
Expand All @@ -34,29 +32,29 @@ pnpm install | yarn install | npm install
1. Create Environment File

```bash
cp .env.example .env
cp .env.example .env
```

2. Generate Application Key

```bash
node ace generate:key
node ace generate:key
```

### 4. Database Setup

1. Run Migrations

```bash
ace migration:run
node ace migration:run
```

### 5. Start Development

Open a terminal and run the following command:

```bash
pn dev
pnpm dev
```

Visit [localhost:3333](http://localhost:3333) and both your frontend and backend will be running together.
Expand Down
13 changes: 13 additions & 0 deletions app/controllers/health_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { HttpContext } from '@adonisjs/core/http'

export default class HealthController {
/**
* Get server health status
*/
async index({ response }: HttpContext) {
return response.json({
status: 'ok',
timestamp: new Date().toISOString(),
})
}
}
102 changes: 102 additions & 0 deletions resources/js/components/common/health-check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import axiosInstance from '@/lib/api-client'

interface HealthStatus {
status: string
timestamp: string
}

export function HealthCheck() {
const [health, setHealth] = useState<HealthStatus | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

useEffect(() => {
const fetchHealth = async () => {
try {
const response = await axiosInstance.get('/health')
setHealth(response.data)
setError(null)
} catch (err) {
setError('Failed to fetch health status')
console.error('Health check failed:', err)
} finally {
setLoading(false)
}
}

fetchHealth()
}, [])

const getStatusVariant = (status: string) => {
switch (status?.toLowerCase()) {
case 'ok':
return 'default'
case 'error':
return 'destructive'
default:
return 'secondary'
}
}

const formatTimestamp = (timestamp: string) => {
try {
return new Date(timestamp).toLocaleString()
} catch {
return timestamp
}
}

if (loading) {
return (
<Card>
<CardHeader>
<CardTitle>Server Health</CardTitle>
<CardDescription>Checking server status...</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-32" />
</div>
</CardContent>
</Card>
)
}

return (
<Card>
<CardHeader>
<CardTitle>Server Health</CardTitle>
<CardDescription>
{error ? 'Unable to connect to server' : 'Current server status and timestamp'}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Status:</span>
{error ? (
<Badge variant="destructive">Error</Badge>
) : (
<Badge variant={getStatusVariant(health?.status || 'unknown')}>
{health?.status || 'Unknown'}
</Badge>
)}
</div>
{!error && health?.timestamp && (
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Last checked:</span>
<span className="text-sm text-muted-foreground">
{formatTimestamp(health.timestamp)}
</span>
</div>
)}
</div>
</CardContent>
</Card>
)
}
3 changes: 2 additions & 1 deletion resources/js/pages/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PageLayout } from '@/components/layout/page-layout'
import { HealthCheck } from '@/components/common/health-check'
import { type BreadcrumbItem } from '@/types'

const breadcrumbs: BreadcrumbItem[] = [
Expand All @@ -15,7 +16,7 @@ export default function DashboardPage() {
<PageLayout breadcrumbs={breadcrumbs} pageTitle="Dashboard">
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<HealthCheck />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
Expand Down
4 changes: 4 additions & 0 deletions start/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

const SessionController = () => import('#controllers/session_controller')
const HealthController = () => import('#controllers/health_controller')
import router from '@adonisjs/core/services/router'
import { middleware } from './kernel.js'
const RegisteredUsersController = () => import('#controllers/registered_users_controller')
Expand Down Expand Up @@ -36,6 +37,9 @@ router

// API routes
// router.any('/api/*', [TrpcController, 'handle']).as('api')

router.get('/health', [HealthController, 'index']).prefix('api')

router
.group(() => {
router.post('/login', [SessionController, 'store']).as('login')
Expand Down
16 changes: 16 additions & 0 deletions tests/functional/health.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { test } from '@japa/runner'

test.group('Health check', () => {
test('should return server health status', async ({ assert }) => {
const response = await fetch('http://localhost:3333/api/health')
const body = (await response.json()) as { status: string; timestamp: string }

assert.equal(response.status, 200)
assert.equal(body.status, 'ok')
assert.isString(body.timestamp)

// Verify timestamp is in ISO format
const timestamp = new Date(body.timestamp)
assert.isFalse(Number.isNaN(timestamp.getTime()))
})
})