new changes
This commit is contained in:
@@ -523,11 +523,14 @@ export async function updateDatabasePassword(dbId: number, password: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export type SslAlertSite = { site: string; primary: string; days_left: number | null | undefined }
|
||||
|
||||
export async function getDashboardStats() {
|
||||
return apiRequest<{
|
||||
site_count: number
|
||||
ftp_count: number
|
||||
database_count: number
|
||||
ssl_alerts: { expired: SslAlertSite[]; expiring: SslAlertSite[] }
|
||||
system: {
|
||||
cpu_percent: number
|
||||
memory_percent: number
|
||||
@@ -536,10 +539,20 @@ export async function getDashboardStats() {
|
||||
disk_percent: number
|
||||
disk_used_gb: number
|
||||
disk_total_gb: number
|
||||
inode_percent: number | null
|
||||
inode_used: number | null
|
||||
inode_total: number | null
|
||||
}
|
||||
}>('/dashboard/stats')
|
||||
}
|
||||
|
||||
export async function getFirewallBackendStatus() {
|
||||
return apiRequest<{
|
||||
ufw: { detected: boolean; active: boolean | null; summary_line: string }
|
||||
firewalld: { detected: boolean; running: boolean; state: string | null }
|
||||
}>('/firewall/status')
|
||||
}
|
||||
|
||||
export async function applyCrontab() {
|
||||
return apiRequest<{ status: boolean; msg: string; count: number }>('/crontab/apply', { method: 'POST' })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { getDashboardStats } from '../api/client'
|
||||
import { PageHeader, AdminCard, AdminAlert } from '../components/admin'
|
||||
|
||||
@@ -6,6 +7,7 @@ interface Stats {
|
||||
site_count: number
|
||||
ftp_count: number
|
||||
database_count: number
|
||||
ssl_alerts?: { expired: { site: string; primary: string; days_left?: number | null }[]; expiring: { site: string; primary: string; days_left?: number | null }[] }
|
||||
system: {
|
||||
cpu_percent: number
|
||||
memory_percent: number
|
||||
@@ -14,6 +16,9 @@ interface Stats {
|
||||
disk_percent: number
|
||||
disk_used_gb: number
|
||||
disk_total_gb: number
|
||||
inode_percent?: number | null
|
||||
inode_used?: number | null
|
||||
inode_total?: number | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +52,39 @@ export function DashboardPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const ssl = stats.ssl_alerts || { expired: [], expiring: [] }
|
||||
const inodePct = stats.system.inode_percent
|
||||
const inodeWarn = typeof inodePct === 'number' && inodePct >= 85
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
|
||||
{ssl.expired.length > 0 ? (
|
||||
<AdminAlert variant="danger" className="mb-3">
|
||||
<strong>Expired certificates:</strong>{' '}
|
||||
{ssl.expired.map((s) => `${s.site} (${s.primary})`).join('; ')}.{' '}
|
||||
<Link to="/ssl_domain" className="alert-link">
|
||||
Renew in Domains & SSL
|
||||
</Link>
|
||||
</AdminAlert>
|
||||
) : null}
|
||||
{ssl.expiring.length > 0 ? (
|
||||
<AdminAlert variant="warning" className="mb-3">
|
||||
<strong>Certificates expiring within 14 days:</strong>{' '}
|
||||
{ssl.expiring.map((s) => `${s.site} (${s.days_left ?? '?'}d)`).join('; ')}.{' '}
|
||||
<Link to="/ssl_domain" className="alert-link">
|
||||
Open Domains & SSL
|
||||
</Link>
|
||||
</AdminAlert>
|
||||
) : null}
|
||||
{inodeWarn ? (
|
||||
<AdminAlert variant="warning" className="mb-3">
|
||||
Root filesystem inode usage is high ({inodePct}%). Large numbers of small files can exhaust inodes before
|
||||
disk space — consider cleanup or archiving.
|
||||
</AdminAlert>
|
||||
) : null}
|
||||
|
||||
<div className="row g-3 mb-4">
|
||||
<div className="col-md-4 d-flex">
|
||||
<StatCard iconClass="ti ti-world" title="Websites" value={stats.site_count} />
|
||||
@@ -78,7 +113,11 @@ export function DashboardPage() {
|
||||
iconClass="ti ti-database-export"
|
||||
title="Disk"
|
||||
value={`${stats.system.disk_percent}%`}
|
||||
subtitle={`${stats.system.disk_used_gb} / ${stats.system.disk_total_gb} GB`}
|
||||
subtitle={
|
||||
typeof stats.system.inode_percent === 'number'
|
||||
? `${stats.system.disk_used_gb} / ${stats.system.disk_total_gb} GB · inodes ${stats.system.inode_percent}%`
|
||||
: `${stats.system.disk_used_gb} / ${stats.system.disk_total_gb} GB`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import Modal from 'react-bootstrap/Modal'
|
||||
import { apiRequest, applyFirewallRules } from '../api/client'
|
||||
import { apiRequest, applyFirewallRules, getFirewallBackendStatus } from '../api/client'
|
||||
import { PageHeader, AdminButton, AdminAlert, AdminTable, EmptyState } from '../components/admin'
|
||||
|
||||
interface FirewallRule {
|
||||
@@ -11,6 +11,11 @@ interface FirewallRule {
|
||||
ps: string
|
||||
}
|
||||
|
||||
interface FirewallBackendStatus {
|
||||
ufw: { detected: boolean; active: boolean | null; summary_line: string }
|
||||
firewalld: { detected: boolean; running: boolean; state: string | null }
|
||||
}
|
||||
|
||||
export function FirewallPage() {
|
||||
const [rules, setRules] = useState<FirewallRule[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -19,11 +24,18 @@ export function FirewallPage() {
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [creatingError, setCreatingError] = useState('')
|
||||
const [applying, setApplying] = useState(false)
|
||||
const [backend, setBackend] = useState<FirewallBackendStatus | null>(null)
|
||||
|
||||
const loadRules = () => {
|
||||
setLoading(true)
|
||||
apiRequest<FirewallRule[]>('/firewall/list')
|
||||
.then(setRules)
|
||||
Promise.all([
|
||||
apiRequest<FirewallRule[]>('/firewall/list'),
|
||||
getFirewallBackendStatus().catch(() => null),
|
||||
])
|
||||
.then(([list, st]) => {
|
||||
setRules(list)
|
||||
setBackend(st)
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
@@ -107,6 +119,44 @@ export function FirewallPage() {
|
||||
|
||||
{error ? <AdminAlert className="mb-3">{error}</AdminAlert> : null}
|
||||
|
||||
{backend ? (
|
||||
<div className="card border-0 shadow-sm mb-3">
|
||||
<div className="card-body py-3">
|
||||
<h6 className="text-muted text-uppercase small mb-2">Host firewall (live)</h6>
|
||||
<div className="d-flex flex-wrap gap-3 align-items-center">
|
||||
<div>
|
||||
<span className="text-muted small me-2">UFW</span>
|
||||
{!backend.ufw.detected ? (
|
||||
<span className="badge text-bg-secondary">Not detected</span>
|
||||
) : backend.ufw.active === true ? (
|
||||
<span className="badge text-bg-success">Active</span>
|
||||
) : backend.ufw.active === false ? (
|
||||
<span className="badge text-bg-warning text-dark">Inactive</span>
|
||||
) : (
|
||||
<span className="badge text-bg-secondary">Unknown</span>
|
||||
)}
|
||||
{backend.ufw.summary_line ? (
|
||||
<span className="small text-muted ms-2 font-monospace">{backend.ufw.summary_line}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted small me-2">firewalld</span>
|
||||
{!backend.firewalld.detected ? (
|
||||
<span className="badge text-bg-secondary">Not detected</span>
|
||||
) : backend.firewalld.running ? (
|
||||
<span className="badge text-bg-info text-dark">Running</span>
|
||||
) : (
|
||||
<span className="badge text-bg-secondary">Not running</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="small text-muted mb-0 mt-2">
|
||||
Panel "Apply to UFW" runs <code>ufw</code> only. If you use firewalld, sync rules there separately.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="alert alert-warning small mb-3">
|
||||
Rules are stored in the panel. Click "Apply to UFW" to run <code className="font-monospace">ufw allow/deny</code> for each rule.
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user