feat(mobile): swap admin page headers to PageHeader
Mechanical sweep replacing the plain h1+p header markup with the mobile-aware PageHeader primitive across 12 admin pages: index, backup, branding, documenso, email, import, invitations, monitoring, onboarding, reminders, reports, webhooks. Webhooks "Add Webhook" button preserved via the actions slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
export default function BackupManagementPage() {
|
export default function BackupManagementPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader title="Backup Management" description="Manage system backups and restoration" />
|
||||||
<h1 className="text-2xl font-bold text-foreground">Backup Management</h1>
|
|
||||||
<p className="text-muted-foreground">Manage system backups and restoration</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
||||||
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
|
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
SettingsFormCard,
|
SettingsFormCard,
|
||||||
type SettingFieldDef,
|
type SettingFieldDef,
|
||||||
} from '@/components/admin/shared/settings-form-card';
|
} from '@/components/admin/shared/settings-form-card';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
const FIELDS: SettingFieldDef[] = [
|
const FIELDS: SettingFieldDef[] = [
|
||||||
{
|
{
|
||||||
@@ -47,13 +48,10 @@ const FIELDS: SettingFieldDef[] = [
|
|||||||
export default function BrandingSettingsPage() {
|
export default function BrandingSettingsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-2xl font-semibold">Branding</h1>
|
title="Branding"
|
||||||
<p className="text-sm text-muted-foreground">
|
description="Logo, primary color, app name, and email header/footer HTML used by the branded auth shell and outgoing email templates."
|
||||||
Logo, primary color, app name, and email header/footer HTML used by the branded auth shell
|
/>
|
||||||
and outgoing email templates.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SettingsFormCard
|
<SettingsFormCard
|
||||||
title="Identity"
|
title="Identity"
|
||||||
description="App name, logo, and primary color."
|
description="App name, logo, and primary color."
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
type SettingFieldDef,
|
type SettingFieldDef,
|
||||||
} from '@/components/admin/shared/settings-form-card';
|
} from '@/components/admin/shared/settings-form-card';
|
||||||
import { DocumensoTestButton } from '@/components/admin/documenso/documenso-test-button';
|
import { DocumensoTestButton } from '@/components/admin/documenso/documenso-test-button';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
const API_FIELDS: SettingFieldDef[] = [
|
const API_FIELDS: SettingFieldDef[] = [
|
||||||
{
|
{
|
||||||
@@ -48,13 +49,10 @@ const EOI_FIELDS: SettingFieldDef[] = [
|
|||||||
export default function DocumensoSettingsPage() {
|
export default function DocumensoSettingsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-2xl font-semibold">Documenso & EOI</h1>
|
title="Documenso & EOI"
|
||||||
<p className="text-sm text-muted-foreground">
|
description="API credentials and default EOI generation pathway. Use the test-connection button to verify a saved configuration before relying on it."
|
||||||
API credentials and default EOI generation pathway. Use the test-connection button to
|
/>
|
||||||
verify a saved configuration before relying on it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SettingsFormCard
|
<SettingsFormCard
|
||||||
title="Documenso API"
|
title="Documenso API"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
SettingsFormCard,
|
SettingsFormCard,
|
||||||
type SettingFieldDef,
|
type SettingFieldDef,
|
||||||
} from '@/components/admin/shared/settings-form-card';
|
} from '@/components/admin/shared/settings-form-card';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
const FIELDS: SettingFieldDef[] = [
|
const FIELDS: SettingFieldDef[] = [
|
||||||
{
|
{
|
||||||
@@ -79,13 +80,10 @@ const FIELDS: SettingFieldDef[] = [
|
|||||||
export default function EmailSettingsPage() {
|
export default function EmailSettingsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-2xl font-semibold">Email Settings</h1>
|
title="Email Settings"
|
||||||
<p className="text-sm text-muted-foreground">
|
description="Per-port outgoing email configuration. SMTP credentials and the From address default to environment variables when these fields are blank."
|
||||||
Per-port outgoing email configuration. SMTP credentials and the From address default to
|
/>
|
||||||
environment variables when these fields are blank.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SettingsFormCard
|
<SettingsFormCard
|
||||||
title="From address & signature"
|
title="From address & signature"
|
||||||
description="Identity headers and shared HTML used by system-generated emails."
|
description="Identity headers and shared HTML used by system-generated emails."
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
export default function DataImportPage() {
|
export default function DataImportPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader title="Data Import" description="Import data from external sources" />
|
||||||
<h1 className="text-2xl font-bold text-foreground">Data Import</h1>
|
|
||||||
<p className="text-muted-foreground">Import data from external sources</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
||||||
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
|
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { InvitationsManager } from '@/components/admin/invitations/invitations-manager';
|
import { InvitationsManager } from '@/components/admin/invitations/invitations-manager';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
export default function InvitationsPage() {
|
export default function InvitationsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-2xl font-semibold">Invitations</h1>
|
title="Invitations"
|
||||||
<p className="text-sm text-muted-foreground">
|
description="Send a single-use invitation to a new CRM user. The recipient sets their own password via the link in the email."
|
||||||
Send a single-use invitation to a new CRM user. The recipient sets their own password via
|
/>
|
||||||
the link in the email.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<InvitationsManager />
|
<InvitationsManager />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
export default function OnboardingPage() {
|
export default function OnboardingPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader title="Onboarding" description="Guided setup for new port configurations" />
|
||||||
<h1 className="text-2xl font-bold text-foreground">Onboarding</h1>
|
|
||||||
<p className="text-muted-foreground">Guided setup for new port configurations</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
||||||
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
|
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
interface AdminSection {
|
interface AdminSection {
|
||||||
href: string;
|
href: string;
|
||||||
@@ -165,13 +166,10 @@ export default async function AdminLandingPage({
|
|||||||
const { portSlug } = await params;
|
const { portSlug } = await params;
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-2xl font-semibold">Administration</h1>
|
title="Administration"
|
||||||
<p className="text-sm text-muted-foreground">
|
description="Per-port configuration and system administration. Each card below opens a dedicated settings page."
|
||||||
Per-port configuration and system administration. Each card below opens a dedicated
|
/>
|
||||||
settings page.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{SECTIONS.map((s) => {
|
{SECTIONS.map((s) => {
|
||||||
const Icon = s.icon;
|
const Icon = s.icon;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
SettingsFormCard,
|
SettingsFormCard,
|
||||||
type SettingFieldDef,
|
type SettingFieldDef,
|
||||||
} from '@/components/admin/shared/settings-form-card';
|
} from '@/components/admin/shared/settings-form-card';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
const DEFAULT_FIELDS: SettingFieldDef[] = [
|
const DEFAULT_FIELDS: SettingFieldDef[] = [
|
||||||
{
|
{
|
||||||
@@ -53,14 +54,10 @@ const DIGEST_FIELDS: SettingFieldDef[] = [
|
|||||||
export default function ReminderSettingsPage() {
|
export default function ReminderSettingsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-2xl font-semibold">Reminders</h1>
|
title="Reminders"
|
||||||
<p className="text-sm text-muted-foreground">
|
description="Default reminder behaviour for new interests and the optional daily-digest delivery window. Individual users can still configure their own digest preferences in Notifications → Preferences."
|
||||||
Default reminder behaviour for new interests and the optional daily-digest delivery
|
/>
|
||||||
window. Individual users can still configure their own digest preferences in Notifications
|
|
||||||
→ Preferences.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SettingsFormCard
|
<SettingsFormCard
|
||||||
title="Defaults for new interests"
|
title="Defaults for new interests"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
|
|
||||||
export default function ScheduledReportsPage() {
|
export default function ScheduledReportsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-2xl font-bold text-foreground">Scheduled Reports</h1>
|
title="Scheduled Reports"
|
||||||
<p className="text-muted-foreground">Configure and manage automated report delivery</p>
|
description="Configure and manage automated report delivery"
|
||||||
</div>
|
/>
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
|
||||||
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 3</p>
|
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 3</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -36,7 +37,11 @@ export default function WebhooksPage() {
|
|||||||
const [deleteTarget, setDeleteTarget] = useState<Webhook | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Webhook | null>(null);
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
const [regenerating, setRegenerating] = useState<string | null>(null);
|
const [regenerating, setRegenerating] = useState<string | null>(null);
|
||||||
const [newSecret, setNewSecret] = useState<{ webhookId: string; secret: string; masked: string } | null>(null);
|
const [newSecret, setNewSecret] = useState<{
|
||||||
|
webhookId: string;
|
||||||
|
secret: string;
|
||||||
|
masked: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const loadWebhooks = useCallback(async () => {
|
const loadWebhooks = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -98,15 +103,20 @@ export default function WebhooksPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<PageHeader
|
||||||
<div>
|
title="Webhooks"
|
||||||
<h1 className="text-2xl font-bold text-foreground">Webhooks</h1>
|
description="Configure outgoing webhook integrations"
|
||||||
<p className="text-muted-foreground">Configure outgoing webhook integrations</p>
|
actions={
|
||||||
</div>
|
<Button
|
||||||
<Button onClick={() => { setEditTarget(null); setFormOpen(true); }}>
|
onClick={() => {
|
||||||
Add Webhook
|
setEditTarget(null);
|
||||||
</Button>
|
setFormOpen(true);
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
Add Webhook
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||||
@@ -116,7 +126,13 @@ export default function WebhooksPage() {
|
|||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Add a webhook to receive real-time notifications of CRM events.
|
Add a webhook to receive real-time notifications of CRM events.
|
||||||
</p>
|
</p>
|
||||||
<Button className="mt-4" onClick={() => { setEditTarget(null); setFormOpen(true); }}>
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
onClick={() => {
|
||||||
|
setEditTarget(null);
|
||||||
|
setFormOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Add Webhook
|
Add Webhook
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,17 +157,16 @@ export default function WebhooksPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Button
|
<Button variant="ghost" size="sm" onClick={() => handleToggleActive(webhook)}>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleToggleActive(webhook)}
|
|
||||||
>
|
|
||||||
{webhook.isActive ? 'Disable' : 'Enable'}
|
{webhook.isActive ? 'Disable' : 'Enable'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => { setEditTarget(webhook); setFormOpen(true); }}
|
onClick={() => {
|
||||||
|
setEditTarget(webhook);
|
||||||
|
setFormOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -163,11 +178,7 @@ export default function WebhooksPage() {
|
|||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="ghost" size="sm" onClick={() => toggleExpand(webhook.id)}>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => toggleExpand(webhook.id)}
|
|
||||||
>
|
|
||||||
{expandedId === webhook.id ? 'Collapse' : 'Details'}
|
{expandedId === webhook.id ? 'Collapse' : 'Details'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,18 +239,26 @@ export default function WebhooksPage() {
|
|||||||
onSuccess={loadWebhooks}
|
onSuccess={loadWebhooks}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => { if (!open) setDeleteTarget(null); }}>
|
<AlertDialog
|
||||||
|
open={!!deleteTarget}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setDeleteTarget(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete Webhook</AlertDialogTitle>
|
<AlertDialogTitle>Delete Webhook</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Delete "{deleteTarget?.name}"? This will also delete all delivery history. This action
|
Delete "{deleteTarget?.name}"? This will also delete all delivery history.
|
||||||
cannot be undone.
|
This action cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground">
|
<AlertDialogAction
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="bg-destructive text-destructive-foreground"
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { apiFetch } from '@/lib/api/client';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { ServiceHealthCard } from './service-health-card';
|
import { ServiceHealthCard } from './service-health-card';
|
||||||
import { QueueOverview } from './queue-overview';
|
import { QueueOverview } from './queue-overview';
|
||||||
|
import { PageHeader } from '@/components/shared/page-header';
|
||||||
import type {
|
import type {
|
||||||
HealthStatus,
|
HealthStatus,
|
||||||
QueueStatus,
|
QueueStatus,
|
||||||
@@ -17,16 +18,14 @@ import type {
|
|||||||
export function SystemMonitoringDashboard() {
|
export function SystemMonitoringDashboard() {
|
||||||
const { data: healthData } = useQuery({
|
const { data: healthData } = useQuery({
|
||||||
queryKey: ['system', 'health'],
|
queryKey: ['system', 'health'],
|
||||||
queryFn: () =>
|
queryFn: () => apiFetch<{ data: HealthStatus }>('/api/v1/admin/health').then((r) => r.data),
|
||||||
apiFetch<{ data: HealthStatus }>('/api/v1/admin/health').then((r) => r.data),
|
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: queuesData } = useQuery({
|
const { data: queuesData } = useQuery({
|
||||||
queryKey: ['system', 'queues'],
|
queryKey: ['system', 'queues'],
|
||||||
queryFn: () =>
|
queryFn: () => apiFetch<{ data: QueueStatus[] }>('/api/v1/admin/queues').then((r) => r.data),
|
||||||
apiFetch<{ data: QueueStatus[] }>('/api/v1/admin/queues').then((r) => r.data),
|
|
||||||
staleTime: 10_000,
|
staleTime: 10_000,
|
||||||
refetchInterval: 10_000,
|
refetchInterval: 10_000,
|
||||||
});
|
});
|
||||||
@@ -47,11 +46,10 @@ export function SystemMonitoringDashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Page header */}
|
<PageHeader
|
||||||
<div>
|
title="System Monitoring"
|
||||||
<h1 className="text-2xl font-bold text-foreground">System Monitoring</h1>
|
description="Real-time health, queue status and connection tracking"
|
||||||
<p className="text-muted-foreground">Real-time health, queue status and connection tracking</p>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Service health */}
|
{/* Service health */}
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
@@ -79,10 +77,7 @@ export function SystemMonitoringDashboard() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[1, 2, 3, 4].map((i) => (
|
||||||
<div
|
<div key={i} className="h-[88px] w-[160px] rounded-xl border bg-card animate-pulse" />
|
||||||
key={i}
|
|
||||||
className="h-[88px] w-[160px] rounded-xl border bg-card animate-pulse"
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -124,9 +119,7 @@ export function SystemMonitoringDashboard() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-3xl font-bold">
|
<p className="text-3xl font-bold">{queues.reduce((sum, q) => sum + q.active, 0)}</p>
|
||||||
{queues.reduce((sum, q) => sum + q.active, 0)}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
@@ -141,10 +134,7 @@ export function SystemMonitoringDashboard() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-5 gap-3">
|
<div className="grid grid-cols-5 gap-3">
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
<div
|
<div key={i} className="h-[110px] rounded-xl border bg-card animate-pulse" />
|
||||||
key={i}
|
|
||||||
className="h-[110px] rounded-xl border bg-card animate-pulse"
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -159,8 +149,7 @@ export function SystemMonitoringDashboard() {
|
|||||||
function RecentErrorsPanel() {
|
function RecentErrorsPanel() {
|
||||||
const { data: errorsData } = useQuery({
|
const { data: errorsData } = useQuery({
|
||||||
queryKey: ['system', 'errors'],
|
queryKey: ['system', 'errors'],
|
||||||
queryFn: () =>
|
queryFn: () => apiFetch<{ data: RecentError[] }>('/api/v1/admin/errors').then((r) => r.data),
|
||||||
apiFetch<{ data: RecentError[] }>('/api/v1/admin/errors').then((r) => r.data),
|
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user